Skip to content

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.

Payment rails are continuous payment streams between clients and service providers, created automatically when services like storage are initiated.

Rails ensure reliable payments through a simple lockup mechanism:

When you create a data set (storage), the system calculates how much balance you need to maintain:

  • Formula: lockup = paymentRate × lockupPeriod (e.g., 10 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
  • 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)
  • 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

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)

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

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)

Check rails where you’re the payer:

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(` End epoch: ${rail.endEpoch}`)
}
}

Check rails where you’re receiving payments:

const payeeRails = await synapse.payments.getRailsAsPayee()
console.log(`You have ${payeeRails.length} incoming payment rails`)

For detailed information about a specific rail:

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 is the process of executing the accumulated payments in a rail. Until settled, payments accumulate but aren’t transferred.

  • Gas Efficiency: Batches many epochs of payments into one transaction
  • Flexibility: Allows validators to adjust payments if needed
  • Finality: Makes funds available for withdrawal

Settlement operations require a network fee that is burned (permanently removed from circulation), effectively paying the Filecoin network for providing the settlement service:

  • Amount: 0.0013 FIL (defined as SETTLEMENT_FEE constant)
  • Mechanism: The fee is burned to Filecoin’s burn actor, f099 (also known as address 0xff00000000000000000000000000000000000063), reducing FIL supply
  • Purpose: This burn mechanism compensates the network for processing and securing payment settlements
  • Automatic: The SDK automatically includes this fee when calling settlement methods
import { SETTLEMENT_FEE } from '@filoz/synapse-sdk'
console.log(`Settlement fee: ${ethers.formatEther(SETTLEMENT_FEE)} FIL`)
// This fee is burned to the network, not paid to any party

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:

// Automatically handles both active and terminated rails
const tx = await synapse.payments.settleAuto(railId)
await tx.wait()
console.log('Rail settled successfully')
// For active rails, you can specify the epoch to settle up to
const tx = await synapse.payments.settleAuto(railId, 1000)
await tx.wait()

For more control, you can use the specific settlement methods:

Settle up to the current epoch:

// Settle a specific rail (requires settlement fee)
const tx = await synapse.payments.settle(railId)
await tx.wait()
console.log('Rail settled successfully')

Settle up to a specific past epoch (partial settlement):

// Settle up to epoch 1000 (must be less than or equal to current epoch)
// Useful for:
// - Partial settlements to manage cash flow
// - Testing settlement calculations
// - Settling up to a specific accounting period
const provider = synapse.getProvider()
const currentEpoch = await provider.getBlockNumber()
const targetEpoch = Math.min(1000, currentEpoch) // Ensure it's not in the future
const tx = await synapse.payments.settle(railId, targetEpoch)
await tx.wait()

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 CannotSettleFutureEpochs error if you try to settle beyond the current epoch

When a rail is terminated, use the specific method for terminated rails:

// Check if rail is terminated
const railInfo = await synapse.payments.getRail(railId)
if (railInfo.endEpoch > 0) {
console.log(`Rail terminated at epoch ${railInfo.endEpoch}`)
// Settle the terminated rail
const tx = await synapse.payments.settleTerminatedRail(railId)
await tx.wait()
console.log('Terminated rail settled and closed')
}

Check settlement amounts before executing:

// Preview settlement to current epoch
const amounts = await synapse.payments.getSettlementAmounts(railId)
console.log('Settlement preview:')
console.log(` Total amount: ${ethers.formatUnits(amounts.totalSettledAmount, 18)} USDFC`)
console.log(` Payee receives: ${ethers.formatUnits(amounts.totalNetPayeeAmount, 18)} USDFC`)
console.log(` Operator commission: ${ethers.formatUnits(amounts.totalOperatorCommission, 18)} USDFC`)
console.log(` Settled up to epoch: ${amounts.finalSettledEpoch}`)
console.log(` Note: ${amounts.note}`)
// Preview partial settlement to a specific past epoch
const targetEpoch = 1000 // Must be less than or equal to current epoch
const partialAmounts = await synapse.payments.getSettlementAmounts(railId, targetEpoch)
console.log(`Partial settlement to epoch ${targetEpoch} would settle: ${ethers.formatUnits(partialAmounts.totalSettledAmount, 18)} USDFC`)

Service providers (payees) should settle regularly to receive accumulated earnings.

// Example: Settle all incoming rails using settleAuto
async 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(rail.railId)
// Only settle if amount exceeds threshold (e.g., $10)
const threshold = ethers.parseUnits('10', 18) // 10 USDFC
if (amounts.totalNetPayeeAmount > threshold) {
// settleAuto handles both active and terminated rails
const tx = await synapse.payments.settleAuto(rail.railId)
await tx.wait()
console.log(`Settled rail ${rail.railId} for ${ethers.formatUnits(amounts.totalNetPayeeAmount, 18)} USDFC`)
}
} catch (error) {
console.error(`Failed to settle rail ${rail.railId}:`, error)
}
}
}

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
// Example: Settle before withdrawal
async 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 tx = await synapse.payments.settleAuto(rail.railId)
await tx.wait()
}
// Now withdrawal will reflect accurate balance
const availableBalance = await synapse.payments.availableBalance()
console.log(`Available for withdrawal: ${ethers.formatUnits(availableBalance, 18)} USDFC`)
}

Common settlement errors and solutions:

try {
await synapse.payments.settle(railId)
} catch (error) {
if (error.message.includes('InsufficientNativeTokenForBurn')) {
console.error('Insufficient FIL for settlement fee (0.0013 FIL required)')
} else if (error.message.includes('NoProgressInSettlement')) {
console.error('Rail already settled to current epoch')
} else if (error.message.includes('RailNotActive')) {
console.error('Rail is not active or already terminated')
} else {
console.error('Settlement failed:', error)
}
}