Rails & Settlement
Payment rails are the core mechanism for streaming payments between parties in the Synapse ecosystem. They enable continuous, per-epoch payments for services like storage and are created automatically when you upload your first file to a storage provider.
Understanding Payment Rails
Section titled “Understanding Payment Rails”How Rails Work
Section titled “How Rails Work”Rails ensure reliable payments through a simple lockup mechanism:
1. The Lockup Requirement
Section titled “1. The Lockup Requirement”When you create a data set (storage), the system calculates how much balance you need to maintain:
- Formula:
lockup = paymentRate × lockupPeriod(e.g., 30 days worth of payments) - Example: Storing 1 GiB costs ~0.0000565 USDFC/epoch, requiring ~1.63 USDFC minimum balance
- Purpose: This protects the service provider by ensuring you always have enough for the next payment period
2. How Your Balance Works
Section titled “2. How Your Balance Works”- You deposit funds into the payments contract (e.g., 100 USDFC)
- The lockup requirement reserves part of this balance (e.g., 1.63 USDFC for 1 GiB storage)
- You can withdraw anything above the lockup requirement
- When you settle, your total balance decreases by the payment amount (lockup requirement stays the same)
3. Normal vs Abnormal Operations
Section titled “3. Normal vs Abnormal Operations”- Normal Operation: You keep settling regularly, lockup stays reserved but unused
- If you stop settling: Service continues but unpaid amounts accumulate
- If balance gets too low: Rail terminates when you can’t cover future payments
- After termination: The lockup now becomes available to pay the service provider for the period already provided
Understanding Your Balance
Section titled “Understanding Your Balance”Think of your account as having these components:
- Total Funds: All tokens you’ve deposited into the payments contract
- Lockup Requirement: The minimum balance reserved to guarantee future payments
- Available Balance:
totalFunds - lockupRequirement(this is what you can withdraw)
When Lockup Gets Used (The Safety Net)
Section titled “When Lockup Gets Used (The Safety Net)”The lockup finally gets “used” when things go wrong:
- Rail terminates (due to insufficient funds or manual termination)
- After termination, the service provider can settle and claim payment from the lockup
- This ensures the provider gets paid for services already delivered, even if the client disappears
- Example: If you had 10 days of lockup and the rail terminates, the provider can claim up to 10 days of service payments from that locked amount
For more details on the payment mechanics, see the Filecoin Pay documentation
Rail Components
Section titled “Rail Components”Each rail consists of:
- Payer: The account paying for services
- Payee: The recipient of payments (service provider)
- Operator: The contract managing the rail (e.g., WarmStorage contract)
- Payment Rate: Amount paid per epoch
- Lockup Period: How many epochs of payments to lock up in advance
- Commission: Percentage taken by the operator (in basis points)
Working with Rails
Section titled “Working with Rails”Check active payment rails to monitor ongoing commitments and verify proper service authorization.
Viewing Your Rails
Section titled “Viewing Your Rails”Check rails where you’re the payer:
// @lib: esnext,domimport { Synapse } from "@filoz/synapse-sdk";import { privateKeyToAccount } from 'viem/accounts'const synapse = Synapse.create({ account: privateKeyToAccount('0x...'), source: 'my-app' })// ---cut---const payerRails = await synapse.payments.getRailsAsPayer();console.log(`You have ${payerRails.length} outgoing payment rails`);
for (const rail of payerRails) { console.log(`Rail ${rail.railId}:`); console.log(`Active: ${!rail.isTerminated}`); if (rail.isTerminated) { console.log(`Terminated at epoch: ${rail.endEpoch}`); }}Check rails where you’re receiving payments:
// @lib: esnext,domimport { Synapse } from "@filoz/synapse-sdk";import { privateKeyToAccount } from 'viem/accounts'const synapse = Synapse.create({ account: privateKeyToAccount('0x...'), source: 'my-app' });// ---cut---const payeeRails = await synapse.payments.getRailsAsPayee();console.log(`You have ${payeeRails.length} incoming payment rails`);Getting Rail Details
Section titled “Getting Rail Details”For detailed information about a specific rail:
// @lib: esnext,domimport { Synapse } from "@filoz/synapse-sdk";import { privateKeyToAccount } from 'viem/accounts'const synapse = Synapse.create({ account: privateKeyToAccount('0x...'), source: 'my-app' });const railId = null as unknown as bigint;// ---cut---const railInfo = await synapse.payments.getRail({ railId });console.log("Rail details:", { from: railInfo.from, to: railInfo.to, rate: railInfo.paymentRate, settledUpTo: railInfo.settledUpTo, isTerminated: railInfo.endEpoch > 0,});Settlement Operations
Section titled “Settlement Operations”Settlement is the process of executing the accumulated payments in a rail. Until settled, payments accumulate but aren’t transferred.
Why Settlement is Needed
Section titled “Why Settlement is Needed”- Gas Efficiency: Batches many epochs of payments into one transaction
- Flexibility: Allows validators to adjust payments if needed
- Finality: Makes funds available for withdrawal
Performing Settlement
Section titled “Performing Settlement”Automatic Settlement (Recommended)
Section titled “Automatic Settlement (Recommended)”The simplest way to settle a rail is using settleAuto(), which automatically detects whether the rail is active or terminated and calls the appropriate method:
// @lib: esnext,domimport { Synapse } from "@filoz/synapse-sdk";import { privateKeyToAccount } from 'viem/accounts'const synapse = Synapse.create({ account: privateKeyToAccount('0x...'), source: 'my-app' });const railId = 1n;// ---cut---// Automatically handles both active and terminated railsconst hash1 = await synapse.payments.settleAuto({ railId });await synapse.client.waitForTransactionReceipt({ hash: hash1 });console.log("Rail settled successfully");
// For active rails, you can specify the epoch to settle up toconst hash2 = await synapse.payments.settleAuto({ railId, untilEpoch: 1000n });await synapse.client.waitForTransactionReceipt({ hash: hash2 });console.log("Rail settled successfully up to epoch 1000");Manual Settlement Methods
Section titled “Manual Settlement Methods”For more control, you can use the specific settlement methods:
Active Rails
Section titled “Active Rails”Settle up to the current epoch:
// @lib: esnext,domimport { Synapse } from "@filoz/synapse-sdk";import { privateKeyToAccount } from 'viem/accounts'const synapse = Synapse.create({ account: privateKeyToAccount('0x...'), source: 'my-app' });const railId = 1n;// ---cut---// Settle a specific rail (requires settlement fee)const hash = await synapse.payments.settle({ railId });await synapse.client.waitForTransactionReceipt({ hash });console.log("Rail settled successfully");Settle up to a specific past epoch (partial settlement):
Useful for:
- Partial settlements to manage cash flow
- Testing settlement calculations
- Settling up to a specific accounting period
// @lib: esnext,domimport { Synapse } from "@filoz/synapse-sdk";import { privateKeyToAccount } from 'viem/accounts'const synapse = Synapse.create({ account: privateKeyToAccount('0x...'), source: 'my-app' });const railId = 1n;// ---cut---const targetEpoch = 1000n; // Ensure it's not in the futureconst hash = await synapse.payments.settle({ railId, untilEpoch: targetEpoch });await synapse.client.waitForTransactionReceipt({ hash });Important: The untilEpoch parameter:
- Must be less than or equal to current epoch - Cannot settle future epochs that haven’t occurred yet
- Can be in the past - Allows partial settlement up to a historical epoch
- Defaults to current epoch - If omitted, settles all accumulated payments up to now
- The contract will revert with
CannotSettleFutureEpochserror if you try to settle beyond the current epoch
Terminated Rails
Section titled “Terminated Rails”When a rail is terminated, use the specific method for terminated rails:
// @lib: esnext,domimport { Synapse } from "@filoz/synapse-sdk";import { privateKeyToAccount } from 'viem/accounts'const synapse = Synapse.create({ account: privateKeyToAccount('0x...'), source: 'my-app' });const railId = 1n;// ---cut---// Check if rail is terminatedconst railInfo = await synapse.payments.getRail({ railId });if (railInfo.endEpoch > 0) { console.log(`Rail terminated at epoch ${railInfo.endEpoch}`); // Settle the terminated rail const hash = await synapse.payments.settleTerminatedRail({ railId }); await synapse.client.waitForTransactionReceipt({ hash }); console.log("Terminated rail settled and closed");}Preview Settlement Amounts
Section titled “Preview Settlement Amounts”// @lib: esnext,domimport { Synapse, formatUnits } from "@filoz/synapse-sdk";import { privateKeyToAccount } from 'viem/accounts'const synapse = Synapse.create({ account: privateKeyToAccount('0x...'), source: 'my-app' });const railId = 1n;// ---cut---// Preview settlement to current epochconst amounts = await synapse.payments.getSettlementAmounts({ railId });const totalSettledAmount = formatUnits(amounts.totalSettledAmount);const totalNetPayeeAmount = formatUnits(amounts.totalNetPayeeAmount);const totalOperatorCommission = formatUnits(amounts.totalOperatorCommission);
const finalSettledEpoch = amounts.finalSettledEpoch;const note = amounts.note;console.log("Settlement preview:");console.log(` Total amount: ${totalSettledAmount} USDFC`);console.log(` Payee receives: ${totalNetPayeeAmount} USDFC`);console.log(` Operator commission: ${totalOperatorCommission} USDFC`);console.log(` Settled up to epoch: ${finalSettledEpoch}`);console.log(` Note: ${note}`);
// Preview partial settlement to a specific past epochconst untilEpoch = 1000n; // Must be less than or equal to current epochconst partialAmounts = await synapse.payments.getSettlementAmounts({ railId, untilEpoch });const partialTotalSettledAmount = formatUnits(partialAmounts.totalSettledAmount);console.log( `Partial settlement to epoch ${untilEpoch} -`, `would settle: ${partialTotalSettledAmount} USDFC`);Settlement Strategies
Section titled “Settlement Strategies”For Service Providers
Section titled “For Service Providers”Service providers (payees) should settle regularly to receive accumulated earnings.
// @lib: esnext,domimport { Synapse, parseUnits, formatUnits } from "@filoz/synapse-sdk";import { privateKeyToAccount } from 'viem/accounts'const synapse = Synapse.create({ account: privateKeyToAccount('0x...'), source: 'my-app' });// ---cut---// Example: Settle all incoming rails using settleAutoasync function settleAllIncomingRails() { const rails = await synapse.payments.getRailsAsPayee();
for (const rail of rails) { try { // Check if settlement is worthwhile const amounts = await synapse.payments.getSettlementAmounts({ railId: rail.railId });
// Only settle if amount exceeds threshold (e.g., $10) const threshold = parseUnits("10"); // 10 USDFC if (amounts.totalNetPayeeAmount > threshold) { // settleAuto handles both active and terminated rails const hash = await synapse.payments.settleAuto({ railId: rail.railId }); await synapse.client.waitForTransactionReceipt({ hash }); console.log( `Settled rail ${rail.railId} for ${formatUnits(amounts.totalNetPayeeAmount)} USDFC` ); } } catch (error) { console.error(`Failed to settle rail ${rail.railId}:`, error); } }}For Clients
Section titled “For Clients”Clients (payers) typically don’t need to settle unless:
- They want to update their available balance before withdrawal
- A rail is terminated and needs finalization
// @lib: esnext,domimport { Synapse, parseUnits, formatUnits } from "@filoz/synapse-sdk";import { privateKeyToAccount } from 'viem/accounts'const synapse = Synapse.create({ account: privateKeyToAccount('0x...'), source: 'my-app' });// ---cut---// Example: Settle before withdrawalasync function prepareForWithdrawal() { const rails = await synapse.payments.getRailsAsPayer();
// Settle all rails to update balance (settleAuto handles both active and terminated) for (const rail of rails) { const hash = await synapse.payments.settleAuto({ railId: rail.railId }); await synapse.client.waitForTransactionReceipt({ hash }); }
// Now withdrawal will reflect accurate balance const availableBalance = (await synapse.payments.accountInfo()) .availableFunds; console.log( `Available for withdrawal: ${formatUnits(availableBalance)} USDFC` );}Error Handling
Section titled “Error Handling”Common settlement errors and solutions:
// @lib: esnext,domimport { Synapse } from "@filoz/synapse-sdk";import { privateKeyToAccount } from 'viem/accounts'const synapse = Synapse.create({ account: privateKeyToAccount('0x...'), source: 'my-app' });const railId = 1n;// ---cut---try { await synapse.payments.settle({railId});} catch (error) { if (error instanceof Error && error.message.includes("InsufficientNativeTokenForBurn")) { console.error("Insufficient FIL for settlement fee (0.0013 FIL required)"); } else if (error instanceof Error && error.message.includes("NoProgressInSettlement")) { console.error("Rail already settled to current epoch"); } else if (error instanceof Error && error.message.includes("RailNotActive")) { console.error("Rail is not active or already terminated"); } else { console.error("Settlement failed:", error); }}Next Steps
Section titled “Next Steps”- Learn about Service Approvals for managing operator permissions
- Explore Storage Management which creates payment rails automatically
- See Payment Operations for comprehensive payment management guide