# Storage Costs

Understanding storage costs helps you budget effectively and avoid service interruptions. This guide covers the SDK APIs for calculating costs, funding your account, and querying account state.

:::tip[How It Works]
For a conceptual overview of how payments work: deposits, payment rails, lockup, and what happens when funds run low, see the [Payments & Storage cookbook](/cookbooks/payments-and-storage/).
:::

Storage operates on a **pay-per-epoch** model where you deposit USDFC tokens and set allowances that control how much the storage service can spend.

:::tip[What is an Epoch?]
An **epoch** is Filecoin's block time, which is 30 seconds. Storage costs are calculated per epoch, ensuring you only pay for the storage time you actually use.
:::

## Pricing Components

Pricing has two recurring rates and a set of small one-time fees.

### Recurring rates

| Component | Cost | Notes |
| -------------- | ------------------ | -------------------------------------------------------- |
| **Storage** | $2.50/TiB/month per copy | Charged only while pieces exist. Empty data sets have no recurring cost. |
| **Proving Service** | $0.12/data set/month | Flat per-data-set fee, added on top of the storage rate. |
| **CDN egress** | up to $14/TiB (≈ $0.014/GiB) downloaded | Optional, via Filecoin Beam. About half this when served from cache. |

:::note[Be aware]
"Month" means **30 days** here (86,400 epochs), not a calendar month.
:::

The monthly rate for an active data set is `(FWSS-priced bytes / TiB) × $2.50 + $0.12`. There is no minimum floor: a data set with no pieces has no recurring cost, and the rate activates when the first piece is added. PDP counts the data-bearing Merkle-tree leaves of each piece, then FWSS converts the aggregate leaf count using the Filecoin `127 / 128` ratio. This priced size can exceed the exact raw payload size by up to 31 bytes per piece.

### One-time fees

Each on-chain operation pays a small fee to the storage provider, covering the gas that provider spends on your behalf:

| Operation | Fee |
| -------------- | ------------------ |
| **Create data set** | $0.025 |
| **Add pieces** | $0.008 + $0.003 per piece |
| **Schedule piece removals** | $0.007 per call |
| **Terminate service** | $0.006 for client-authorized, provider-relayed termination; no fee for direct payer or provider termination |

These are small one-time charges paid from a refundable lifecycle reserve (~$0.50) held while the data set is active, not added separately to the required deposit on every call. See [How the fees and lockup work](#how-the-fees-and-lockup-work) below.

:::note[CDN setup]
Enabling CDN adds two refundable lockups on first use: ~0.7 USDFC for egress credits and ~0.3 USDFC for cache-miss credits. Reusing an existing CDN data set incurs no new setup. These are lockups, not fees, and are returned when the rail is finalized if unused.
:::

### Real-World Cost Examples

These examples use plain arithmetic for clarity. For programmatic cost calculation, use `getUploadCosts()` — see [Querying Upload Costs](#querying-upload-costs) below.

#### Example 1: NFT Collection (10,000 × 5 KiB ≈ 48.82 MiB)

```ts twoslash
// @lib: esnext,dom
// 48.82 MiB is tiny, so the storage rate is negligible.
// The recurring cost is dominated by the flat proving fee.
const STORAGE_PRICE_PER_TIB_PER_MONTH = 2.5;
const PROVING_FEE_PER_MONTH = 0.12;

const storageTiB = 48.82 / 1024 / 1024; // ≈ 0.0000466 TiB
const storageCostPerMonth = storageTiB * STORAGE_PRICE_PER_TIB_PER_MONTH; // ≈ $0.00012

// Per copy, per month
const costPerMonth = storageCostPerMonth + PROVING_FEE_PER_MONTH; // ≈ 0.1201 USDFC
const costFor24Months = costPerMonth * 24; // ≈ 2.883 USDFC
```

| Duration | Per copy |
| --------- | ---------- |
| 1 month | ≈ 0.120 USDFC |
| 24 months | ≈ 2.88 USDFC |

The default 2-copy configuration roughly doubles the recurring cost. One-time fees apply once: $0.025 to create the data set plus the per-batch add-pieces fee.

---

#### Example 2: User Content Platform with CDN

- **Storage:** 1,000 users × 100 MiB ≈ 100,000 MiB
- **Traffic:** 1,000 users × 100 MiB/month ≈ 100,000 MiB/month egress

```ts twoslash
// @lib: esnext,dom
const STORAGE_PRICE_PER_TIB_PER_MONTH = 2.5; // $2.50/TiB/month
const PROVING_FEE_PER_MONTH = 0.12; // flat per-data-set
const CDN_EGRESS_PRICE_PER_TIB = 14; // up to $14/TiB on a cache miss
const storageMiB = 100_000;
const egressMiB = 100_000;

// Storage: 100,000 MiB ≈ 0.0953 TiB
const storageTiB = storageMiB / 1024 / 1024;

// Egress: 100,000 MiB ≈ 0.0953 TiB
const egressTiB = egressMiB / 1024 / 1024;

// Storage cost per month: 0.0953 TiB × $2.50 ≈ $0.238/month
const storageCostPerMonth = storageTiB * STORAGE_PRICE_PER_TIB_PER_MONTH;

// Egress cost per month (worst case, all cache misses): 0.0953 TiB × $14 ≈ $1.334/month
const egressCostPerMonth = egressTiB * CDN_EGRESS_PRICE_PER_TIB;

// Total cost per month: $0.238 + $0.12 proving + $1.335 egress ≈ $1.694/month
const totalCostPerMonth = storageCostPerMonth + PROVING_FEE_PER_MONTH + egressCostPerMonth;

// Total cost for 24 months: $1.694/month × 24 ≈ $40.65
const totalCostFor24Months = totalCostPerMonth * 24;
```

| Cost Component | Per Month | 24 Months |
| -------------- | ---------------- | ----------------- |
| Storage | ≈ 0.238 USDFC | ≈ 5.72 USDFC |
| Proving Service | ≈ 0.120 USDFC | ≈ 2.88 USDFC |
| CDN Egress | ≈ 1.335 USDFC | ≈ 32.04 USDFC |
| **Total** | **≈ 1.694 USDFC** | **≈ 40.65 USDFC** |

Egress here is the worst case where every byte is a cache miss. When content is served from the Filecoin Beam cache, egress is roughly half this.

## How the fees and lockup work

Most developers can stop at the tables above. This section explains the mechanics behind the per-operation fees and the lockup reserve for anyone who needs the detail.

**The proving fee is additive, not a floor.** The rate is `(FWSS-priced bytes / TiB) × $2.50 + $0.12`, so storage scales with size and the proving fee is a flat addition per data set. A data set with no pieces has no recurring charge until the first piece is added.

**Add-pieces pricing scales with batch size.** Each `addPieces` call costs `$0.008 + $0.003 × N`, where `N` is the number of pieces in the batch. The base fee is shared across the batch, so adding many pieces in one call is cheaper per piece than adding them one at a time. A single-piece call costs `$0.011`, and one SDK commit accepts at most 40 pieces.

**Fees are paid from a lifecycle reserve, not added twice to deposits.** When a data set is created, ~$0.50 of USDFC is locked as a lifecycle reserve on the data set's payment rail. Before pending one-time fees are paid, FWSS checks whether the reserve is below `pending fees + $0.025`. If so, it raises the fixed lockup enough that paying those fees leaves the reserve at its $0.50 target. The remaining reserve is refundable when the rail is finalized.

<details>
<summary>Why the reserve exists: wind-down after termination</summary>

Filecoin Pay does not allow raising a rail's fixed lockup once the rail has been terminated. The lifecycle reserve gives you headroom to run wind-down operations (such as scheduling piece removals) after termination, when the reserve can no longer be refilled.

If you expect to need more wind-down operations than the reserve covers, account for them before terminating. A client-authorized termination relayed by the provider charges the $0.006 termination fee, pays pending fees, and releases the remaining reserve rather than replenishing it. Direct payer and provider termination paths do not charge this termination fee.

For the full lockup model (streaming versus fixed lockup, rail settlement, finalization), see the [Payments & Storage cookbook](/cookbooks/payments-and-storage/) and the [Filecoin Pay spec](https://github.com/FilOzone/filecoin-pay/blob/main/SPEC.md).

</details>

## Querying Upload Costs

Use `getUploadCosts()` to preview costs without executing any transaction. This is useful for displaying pricing in a UI or letting users confirm before proceeding. The result reads the on-chain price list, so it reflects the rates, one-time fees, and lockups described above.

Getting the costs for uploading to an existing data set:

```ts twoslash
// @lib: esnext,dom
// @noErrors
import { Synapse, SIZE_CONSTANTS } from "@filoz/synapse-sdk"
import { getDataSetLeafCount } from '@filoz/synapse-core/pdp-verifier'
import { getDataSet } from '@filoz/synapse-core/warm-storage'
import { privateKeyToAccount } from 'viem/accounts'
const synapse = Synapse.create({ account: privateKeyToAccount('0x...'), source: 'my-app' })
// ---cut---
// Read the existing data set so reserve replenishment is estimated correctly.
const dataSet = await getDataSet(synapse.readClient, { dataSetId: 42n })
if (dataSet == null) throw new Error('Data set not found')
const dataSetLeafCount = await getDataSetLeafCount(synapse.readClient, { dataSetId: 42n })

const { rates, fees, depositNeeded } = await synapse.storage.getUploadCosts({
  isNewDataSet: false,
  dataSetLeafCount,
  currentLifecycleReserveBalance: dataSet.lifecycleReserveBalance,
  pendingOneTimePayments: dataSet.pendingOneTimePayments,
  pieceSizes: [100n * SIZE_CONSTANTS.MiB],
})
// fees.addPiecesFee is the one-time fee for this upload
// Pass every known raw piece size when committing a multi-piece batch.
```

Getting the costs for uploading to a new data set:

```ts twoslash
// @lib: esnext,dom
// @noErrors
import { Synapse, formatUnits, SIZE_CONSTANTS } from "@filoz/synapse-sdk"
import { privateKeyToAccount } from 'viem/accounts'
const synapse = Synapse.create({ account: privateKeyToAccount('0x...'), source: 'my-app' })
// ---cut---
const { rates, fees, lockups, depositNeeded, needsFwssMaxApproval, ready } = await synapse.storage.getUploadCosts({
  pieceSizes: [100n * SIZE_CONSTANTS.MiB],
  isNewDataSet: true,
  withCDN: true,
})

// Storage rate per epoch (30 seconds)
console.log("Rate per epoch:", formatUnits(rates.perEpoch), "USDFC")
// Storage rate per month
console.log("Rate per month:", formatUnits(rates.perMonth), "USDFC")
// One-time fees (create data set + add pieces)
console.log("One-time fees:", formatUnits(fees.total), "USDFC")
// Locked while the data set is active (lifecycle reserve, streaming, CDN)
console.log("Total lockup:", formatUnits(lockups.total), "USDFC")
// USDFC to deposit
console.log("Deposit needed:", formatUnits(depositNeeded), "USDFC")
// Whether FWSS needs to be approved
console.log("Needs approval:", needsFwssMaxApproval)
// Whether the account is ready to upload
console.log("Ready to upload:", ready)
```

## Upload Preparation

Before uploading, the **WarmStorage operator** must be approved and your account must be funded. FWSS uses a 30-day lockup (prepayment) period. If your account becomes underfunded, the provider may terminate the storage agreement, after which you have 30 days before data removal.

:::tip[Typical Flow]
**Query costs** → **prepare transaction** → **execute** → **upload**. The `prepare()` method handles the middle steps in one call.
:::

`prepare()` computes costs and returns a ready-to-execute transaction. This is the recommended approach — it handles deposit calculation, operator approval, and contract call routing in one step.

```ts twoslash
// @lib: esnext,dom
import { Synapse, formatUnits, SIZE_CONSTANTS } from "@filoz/synapse-sdk";
import { privateKeyToAccount } from 'viem/accounts'
const synapse = Synapse.create({ account: privateKeyToAccount('0x...'), source: 'my-app' });
// ---cut---
const { costs, transaction } = await synapse.storage.prepare({
  pieceSizes: [SIZE_CONSTANTS.GiB],
})

// Inspect costs
const { rates, depositNeeded } = costs // Upload costs breakdown
console.log("Rate per month:", formatUnits(rates.perMonth))
console.log("Deposit needed:", formatUnits(depositNeeded))

// Execute if the account isn't ready
if (transaction) {
  const { depositAmount, includesApproval } = transaction
  console.log("Deposit amount:", formatUnits(depositAmount))
  console.log("Includes approval:", includesApproval)

  const { hash } = await transaction.execute()
  console.log("Transaction confirmed:", hash)
}

// Now safe to upload
```

When storing data across multiple providers, pass an array of contexts. The SDK aggregates per-context lockup while computing account-level values (debt, runway, buffer) once.

```ts twoslash
// @lib: esnext,dom
import { Synapse, formatUnits, SIZE_CONSTANTS, TIME_CONSTANTS } from "@filoz/synapse-sdk"
import { privateKeyToAccount } from 'viem/accounts'
const synapse = Synapse.create({ account: privateKeyToAccount('0x...'), source: 'my-app' })

const contexts = await synapse.storage.createContexts({ copies: 3 })

// 1-year runway
const oneYear = TIME_CONSTANTS.EPOCHS_PER_MONTH * 12n

// Prepare a single deposit covering all 3 copies for 1 year
const { costs, transaction } = await synapse.storage.prepare({
  context: contexts,
  pieceSizes: [50n * SIZE_CONSTANTS.GiB],
  extraRunwayEpochs: oneYear,
})

const { rates, depositNeeded, ready } = costs
console.log("Monthly rate (per copy):", formatUnits(rates.perMonth), "USDFC")
console.log("Total deposit needed:", formatUnits(depositNeeded), "USDFC")
console.log("Account ready:", ready)

if (transaction) {
  const { hash } = await transaction.execute()
  console.log("Funded:", hash)
}
```

`prepare()` returns:

- **`costs`**: The full `UploadCosts` breakdown
- **`transaction`**: A transaction object with `execute()`, or `null` if the account is already ready

The transaction automatically picks the right contract call:

- **Needs deposit + approval**: calls `depositWithPermitAndApproveOperator`
- **Needs approval only**: calls `approveService`
- **Needs deposit only**: calls `depositWithPermit`
- **Already ready**: returns `transaction: null`

## Account Queries

Read-only APIs for account-level cost visibility.

```ts twoslash
// @noErrors
// Aggregate rate across all active rails
const { ratePerEpoch, ratePerMonth } = await synapse.payments.totalAccountRate()

// Sum of lockupFixed across all rails (including terminated but not finalized)
const { totalFixedLockup } = await synapse.payments.totalAccountLockup()

// Total storage size across all live datasets
import { getAccountTotalStorageSize } from '@filoz/synapse-core/warm-storage'
const { totalSizeBytes, datasetCount } = await getAccountTotalStorageSize(client, {
  address: '0x...',
})
```

All rate values come in both per-epoch (contract-native) and per-month (`ratePerEpoch * EPOCHS_PER_MONTH`) units. Note that `ratePerMonth` from `totalAccountRate()` is computed as `ratePerEpoch * EPOCHS_PER_MONTH` — this is slightly less than the full-precision `rates.perMonth` returned by `getUploadCosts()` due to integer truncation. See [Rate Precision](/cookbooks/payments-and-storage/#rate-precision) for details.

:::tip[API Reference]
For the full API — including pure calculation functions and lower-level utilities — see the [synapse-core pay reference](/reference/filoz/synapse-core/pay/toc/) and [synapse-core warm-storage reference](/reference/filoz/synapse-core/warm-storage/toc/).
:::

## Next Steps

- [Payments & Storage](/cookbooks/payments-and-storage/) — Deep dive into the payment model mechanics
- [StorageManager API Reference](/reference/filoz/synapse-sdk/storage/classes/storagemanager/) — Full SDK storage API
- [PaymentsService API Reference](/reference/filoz/synapse-sdk/payments/classes/paymentsservice/) — Full SDK payments API
- [Upload Pipeline](/developer-guides/storage/upload-pipeline/) — Upload workflows from simple to advanced
- [Storage Operations](/developer-guides/storage/storage-operations/) — Contexts, data sets, and retrieval