Skip to main content
Implement fiat-to-crypto and crypto-to-fiat flows with blockchain minting, burning, and asynchronous settlement management.
This guide demonstrates how to build stablecoin on-ramp and off-ramp operations using Formance Ledger. Learn to manage the bridge between traditional banking and blockchain, handling payment authorizations, mint/burn operations, and multi-phase settlements.

What are On-Ramp and Off-Ramp Operations?

On-ramp and off-ramp operations represent the bridge between traditional fiat banking systems and blockchain-based digital assets, specifically stablecoins. These operations are fundamental to platforms that provide crypto services while maintaining fiat currency reserves.

On-Ramp (Fiat to Crypto)

An on-ramp is the process where end users make a payment (card, wire transfer, etc.) to your platform, and in return receive stablecoins. Similar to payment acceptance, this involves real-time crediting based on payment authorization, followed by asynchronous settlement:
  1. Payment Authorization: User initiates payment (card/instant payment), receives immediate confirmation
  2. Immediate Credit: User’s stablecoin balance credited instantly based on technical payment confirmation
  3. Mint Operation: Platform mints stablecoins on blockchain to back the user’s balance
  4. Bank Settlement: Fiat eventually settles to your bank account (T+1 to T+3 depending on payment method)
The critical accounting challenge is providing immediate availability to users (treating payment authorization as a promise) while managing asynchronous blockchain mints and eventual bank settlements.

Off-Ramp (Crypto to Fiat)

An off-ramp is the reverse process where users return stablecoins to the platform and receive fiat currency:
  1. Burn Instruction: User requests withdrawal, platform locks their stablecoins
  2. Burn Confirmation: Stablecoins are burned on the blockchain
  3. Fiat Transfer: Platform initiates bank transfer to user
  4. Transfer Confirmation: Bank confirms the fiat transfer completed
Similar to on-ramp, managing the asynchronous nature of blockchain operations and bank transfers is essential.

Key Concepts

Payment Authorization Promise Similar to card authorization in payment acceptance, real-time payment confirmation from PSPs is treated as an asset—a binding promise to settle later. Stablecoin Reserves & 1:1 Peg
  • Reserve Backing: Fiat held in bank accounts backing stablecoins in circulation
  • 1:1 Peg: Each stablecoin represents exactly one unit of fiat currency (1 USDC = $1.00 USD)
This creates dual reserve requirements: on-chain minted supply AND bank-held fiat. Mint & Burn Operations
  • Minting: Creating new tokens on the blockchain when users purchase stablecoins
  • Burning: Permanently destroying tokens when users redeem for fiat
These are asynchronous blockchain operations taking seconds to minutes for confirmation. Multi-Stage Settlement
  • On-Ramp: Payment authorization (instant) → Mint confirmation (minutes) → Bank settlement (days)
  • Off-Ramp: Burn instruction (instant) → Burn confirmation (minutes) → Fiat transfer (days)
Managing three asynchronous timelines simultaneously. In-Flight Operations
  • Mint In-Flight: Blockchain transactions submitted but not yet confirmed (awaiting block confirmations)
  • Bank Settlements Pending: PSP authorization received, bank settlement T+1 to T+3
  • Burn In-Flight: Burn transactions submitted but not yet on-chain
  • Fiat Withdrawals Pending: Bank transfers initiated but not yet completed
Operational Costs
  • Gas Fees: Blockchain transaction costs (typically absorbed by platform)
  • Payment Processing Fees: PSP/acquirer fees for fiat payments
  • Transfer Fees: Bank wire transfer costs for withdrawals
Platforms typically absorb these to provide better UX.

Example Stablecoin Operations with Formance

Let’s work out comprehensive examples for both on-ramp and off-ramp operations.

System Actors

  • Payment Providers (PSPs)
  • Banks
  • Blockchain Networks
  • End Users
  • Platform
Normal Debit Accounts - Hold real-time payment promises.Similar to payment acceptance, these financial institutions provide:
  • Real-time card processing and instant payments
  • Immediate technical confirmation
  • Asynchronous settlement (T+1 to T+3)
The authorization is treated as an asset (their binding promise to settle).Account structure:
@psp:{psp_id}:main
Examples: Stripe, Adyen, Banking Circle, Column

Asset Representation

Formance uses Universal Monetary Notation (UMN) to handle the precision differences between fiat and blockchain assets:
Asset TypeUMN FormatDecimalsExample ValueMeaning
FiatUSD/22100000$1,000.00
FiatEUR/2250000€500.00
StablecoinUSDC/6610000000001,000.00 USDC
StablecoinEURC/66500000000500.00 EURC
Native TokenETH/181810000000000000000001.0 ETH
Native TokenSOL/9910000000001.0 SOL
Precision Conversion Example: $1,000.00 USD = 1,000.00 USDC
  • Fiat: USD/2 100000 (2 decimals, 100,000 cents)
  • Stablecoin: USDC/6 1000000000 (6 decimals, 1,000,000,000 micro-USDC)
Formance handles these conversions automatically in your Numscripts. When converting between fiat and stablecoins, ensure your business logic correctly scales the amounts.

Balances & Periods

A balance of an asset in an account is the result of the sum of all incoming volumes subtracted by the sum of all outgoing volumes. For stablecoin operations, you need to track multiple temporal states across three systems:
  • Payment authorization: Real-time confirmation from PSP/acquirer (immediate)
  • Mint in-flight: Transactions submitted to blockchain but not yet confirmed (minutes)
  • Stablecoins in circulation: Confirmed on-chain supply matching user balances
  • Bank settlement: Fiat actually received in bank account (T+1 to T+3)
  • Burn in-flight: Burn transactions submitted but not yet confirmed
  • Fiat withdrawals pending: Bank transfers initiated but not yet completed
The key insight is that users receive stablecoins immediately upon payment authorization (like in payment acceptance), then the platform handles the asynchronous mint and bank settlement in the background. Using Formance’s bi-temporality feature allows you to maintain accurate historical views despite these asynchronous operations.

Transaction Patterns

Below are example Numscripts that capture the intent of movements and bookings needed to handle stablecoin on-ramp and off-ramp operations. This is merely an example and your business might need different implementations, additional details, different steps and events to handle, etc…
Download the T-Account Movements Excel Template to visualize all accounting entries with detailed T-account diagrams for each payment acceptance flow described in this guide.
Zoom 🔍

On-Ramp Step 1: Payment Authorization and Immediate Stablecoin Credit

User initiates a payment (card, instant payment, etc.) and receives immediate technical confirmation from the payment provider. The PSP handles fiat currency (USD/2, EUR/2), which we receive as their promise. We use a platform pivot account to immediately mint/issue the equivalent stablecoin to the user for the best UX.

Variables

  • $fiat_asset (asset): Fiat asset in UMN (e.g., USD/2, EUR/2)
  • $stable_asset (asset): Stablecoin asset in UMN (e.g., USDC/6)
  • $fiat_amount (number): Payment amount in fiat
  • $stable_amount (number): Equivalent stablecoin amount (accounting for decimal differences)
  • $psp_id (account): Payment service provider identifier
  • $client_id (account): Client account identifier
  • $authorization_id (string): Payment authorization reference

Accounts Used

  • @psp:$psp_id:main : PSP account holding the fiat payment promise (normal debit)
  • @platform:pivot:stablecoin_issuance : Platform pivot account for fiat→stablecoin conversion
  • @clients:$client_id:stablecoin : Client’s stablecoin balance (normal credit)

Numscript

vars {
    asset $fiat_asset
    asset $stable_asset
    number $fiat_amount
    number $stable_amount
    account $psp_id
    account $client_id
    string $authorization_id
}

send [$fiat_asset $fiat_amount] (
    source = @psp:$psp_id:main allowing unbounded overdraft
    destination = @platform:pivot:stablecoin_issuance
)

send [$stable_asset $stable_amount] (
    source = @platform:pivot:stablecoin_issuance allowing unbounded overdraft
    destination = @clients:$client_id:stablecoin
)

set_tx_meta("authorization_id", $authorization_id)
set_tx_meta("type", "payment_authorization_stablecoin_credit")

Example Usage

USD card payment → USDC credit - Try in Playground →
{
    "variables": {
        "fiat_asset": "USD/2",
        "stable_asset": "USDC/6",
        "fiat_amount": "100000",
        "stable_amount": "100000000000",
        "psp_id": "stripe",
        "client_id": "user789",
        "authorization_id": "pi_3NnUV82eZovKypr4123456"
    }
}
EUR instant payment → EURC credit - Try in Playground →
{
    "variables": {
        "fiat_asset": "EUR/2",
        "stable_asset": "EURC/6",
        "fiat_amount": "50000",
        "stable_amount": "50000000000",
        "psp_id": "adyen",
        "client_id": "user456",
        "authorization_id": "INST_PAY_20240115_00123"
    }
}
The PSP provides fiat (USD/2 or EUR/2), which flows through the platform pivot account that converts it into stablecoins for the user. The pivot account holds both the fiat receivable (from PSP) and stablecoin payable (to client), acting as the conversion point.

On-Ramp Step 2: Blockchain Mint Instruction

Platform initiates the mint transaction on the blockchain to create the stablecoins that back the user’s balance. The user already has their stablecoins from Step 1, so this is a background operation to maintain the reserve backing. We move the stablecoin obligation from the pivot account to a mint in-flight tracking account.

Variables

  • $stable_asset (asset): Stablecoin asset in UMN
  • $stable_amount (number): Stablecoin amount to mint
  • $network (account): Blockchain network identifier
  • $mint_tx_hash (string): Blockchain transaction hash
  • $authorization_id (string): Original payment authorization reference

Accounts Used

  • @platform:pivot:stablecoin_issuance : Platform pivot account (has stablecoin overdraft from Step 1)
  • @blockchain:$network:mint_in_flight : Tracking in-flight mint operations

Numscript

vars {
    asset $stable_asset
    number $stable_amount
    account $network
    string $mint_tx_hash
    string $authorization_id
}

send [$stable_asset $stable_amount] (
    source = @blockchain:$network:mint_in_flight allowing unbounded overdraft
    destination = @platform:pivot:stablecoin_issuance
)

set_tx_meta("mint_tx_hash", $mint_tx_hash)
set_tx_meta("authorization_id", $authorization_id)
set_tx_meta("type", "mint_instruction")

Example Usage

USDC mint instruction on Ethereum - Try in Playground →
{
    "variables": {
        "stable_asset": "USDC/6",
        "stable_amount": "100000000000",
        "network": "ethereum:0xA0b86991c6218b36c1d6H7F070A8cAe128Bf7A8C",
        "mint_tx_hash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef12345678",
        "authorization_id": "pi_3NnUV82eZovKypr4123456"
    }
}
The mint is submitted to the blockchain. The pivot account’s stablecoin overdraft is now offset by the mint_in_flight account, which represents the pending on-chain mint operation. The pivot account still holds the fiat receivable from the PSP.

On-Ramp Step 3: Blockchain Mint Confirmation

Blockchain confirms the mint transaction. The stablecoins are now officially in circulation on-chain, backing the user’s balance. The in-flight mint resolves.

Variables

  • $stable_asset (asset): Stablecoin asset in UMN
  • $stable_amount (number): Stablecoin amount minted
  • $network (account): Blockchain network identifier
  • $mint_tx_hash (string): Blockchain transaction hash
  • $block_number (string): Block number where mint was confirmed

Accounts Used

  • @blockchain:$network:mint_in_flight : In-flight mint operations
  • @blockchain:$network:circulating : Confirmed on-chain supply

Numscript

vars {
    asset $stable_asset
    number $stable_amount
    account $network
    string $mint_tx_hash
    string $block_number
}

send [$stable_asset $stable_amount] (
    source = @blockchain:$network:circulating allowing unbounded overdraft
    destination = @blockchain:$network:mint_in_flight
)

set_tx_meta("mint_tx_hash", $mint_tx_hash)
set_tx_meta("block_number", $block_number)
set_tx_meta("type", "mint_confirmation")

Example Usage

USDC mint confirmed on Ethereum - Try in Playground →
{
    "variables": {
        "stable_asset": "USDC/6",
        "stable_amount": "100000000000",
        "network": "ethereum:0xA0b86991c6218b36c1d6H7F070A8cAe128Bf7A8C",
        "mint_tx_hash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef12345678",
        "block_number": "18901234"
    }
}
The mint is confirmed on-chain. The blockchain:circulating account now properly tracks the minted stablecoins backing user balances.

On-Ramp Step 4: PSP/Bank Settlement

The PSP/acquirer settles the fiat payment to your bank account. Similar to acquirer settlement in payment acceptance, they typically settle net amounts after deducting fees. This resolves the fiat side of the pivot account - completing the full backing cycle.

Variables

  • $fiat_asset (asset): Fiat asset in UMN (USD/2, EUR/2)
  • $net_amount (number): Net settlement amount received
  • $fee_amount (number): Fee deducted by PSP
  • $psp_id (account): Payment service provider identifier
  • $bank_number (account): Bank account identifier
  • $settlement_ref (string): Settlement reference

Accounts Used

  • @banks:$bank_number:main : Bank account receiving fiat (normal debit)
  • @platform:pivot:stablecoin_issuance : Platform pivot account (clearing the fiat receivable)
  • @platform:expenses:payment_fees : Platform’s payment processing fee expenses

Numscript

vars {
    asset $fiat_asset
    number $net_amount
    number $fee_amount
    account $psp_id
    account $bank_number
    string $settlement_ref
}

send [$fiat_asset $net_amount] (
    source = @banks:$bank_number:main allowing unbounded overdraft
    destination = @psp:$psp_id:main
)

send [$fiat_asset $fee_amount] (
    source = @platform:expenses:payment_fees allowing unbounded overdraft
    destination = @psp:$psp_id:main
)

set_tx_meta("settlement_ref", $settlement_ref)
set_tx_meta("type", "psp_settlement")

Example Usage

USD settlement from Stripe to bank - Try in Playground →
{
    "variables": {
        "fiat_asset": "USD/2",
        "net_amount": "97000",
        "fee_amount": "3000",
        "psp_id": "stripe",
        "bank_number": "021000089:123456789",
        "settlement_ref": "po_1pkJxL2eZovKypr4123456789"
    }
}
Complete on-ramp cycle: User has stablecoins (Step 1) → Stablecoins minted on-chain (Steps 2-3) → Fiat settled to bank (Step 4). The pivot account is now balanced, with fiat in your bank backing the on-chain minted stablecoins that back the user’s balance.

Off-Ramp Step 1: Burn Instruction

User requests to withdraw fiat and return their stablecoins. Platform initiates a burn transaction on the blockchain and locks the user’s stablecoin balance.

Variables

  • $stable_asset (asset): Stablecoin asset in UMN
  • $stable_amount (number): Stablecoin amount to burn
  • $network (account): Blockchain network identifier
  • $client_id (account): Client account identifier
  • $burn_tx_hash (string): Blockchain transaction hash

Accounts Used

  • @clients:$client_id:stablecoin : Client’s stablecoin balance
  • @blockchain:$network:burn_in_flight : Tracking in-flight burn operations

Numscript

vars {
    asset $stable_asset
    number $stable_amount
    account $network
    account $client_id
    string $burn_tx_hash
}

send [$stable_asset $stable_amount] (
    source = @clients:$client_id:stablecoin
    destination = @blockchain:$network:burn_in_flight
)

set_tx_meta("burn_tx_hash", $burn_tx_hash)
set_tx_meta("type", "burn_instruction")

Example Usage

USDC burn instruction on Ethereum - Try in Playground →
{
    "variables": {
        "stable_asset": "USDC/6",
        "stable_amount": "50000000000",
        "network": "ethereum:0xA0b86991c6218b36c1d6H7F070A8cAe128Bf7A8C",
        "client_id": "user789",
        "burn_tx_hash": "0xfedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543221"
    }
}
Ensure the client has sufficient stablecoin balance before initiating the burn. The off-ramp process is typically irreversible once the burn is confirmed on-chain.

Off-Ramp Step 2: Burn Confirmation

Blockchain confirms the burn transaction. The stablecoins are permanently removed from circulation, and the corresponding fiat is released from backing reserves to pending withdrawal.

Variables

  • $fiat_asset (asset): Fiat asset in UMN
  • $stable_asset (asset): Stablecoin asset in UMN
  • $fiat_amount (number): Fiat amount to release
  • $stable_amount (number): Stablecoin amount burned
  • $network (account): Blockchain network identifier
  • $client_id (account): Client account identifier
  • $burn_tx_hash (string): Blockchain transaction hash
  • $block_number (string): Block number where burn was confirmed

Accounts Used

  • @blockchain:$network:burn_in_flight : In-flight burn operations
  • @blockchain:$network:circulating : Confirmed on-chain supply
  • @platform:reserves:backing_stablecoins : Fiat backing circulating stablecoins
  • @platform:reserves:pending_withdrawal : Fiat pending withdrawal to clients

Numscript

vars {
    asset $fiat_asset
    asset $stable_asset
    number $fiat_amount
    number $stable_amount
    account $network
    account $client_id
    string $burn_tx_hash
    string $block_number
}

send [$stable_asset $stable_amount] (
    source = @blockchain:$network:burn_in_flight
    destination = @blockchain:$network:circulating
)

send [$fiat_asset $fiat_amount] (
    source = @platform:pivot:stablecoin_issuance
    destination = @platform:reserves:pending_withdrawal
)

set_tx_meta("burn_tx_hash", $burn_tx_hash)
set_tx_meta("block_number", $block_number)
set_tx_meta("client_id", $client_id)
set_tx_meta("type", "burn_confirmation")

Example Usage

USDC burn confirmed on Ethereum - Try in Playground →
{
    "variables": {
        "fiat_asset": "USD/2",
        "stable_asset": "USDC/6",
        "fiat_amount": "50000",
        "stable_amount": "50000000000",
        "network": "ethereum:0xA0b86991c6218b36c1d6H7F070A8cAe128Bf7A8C",
        "client_id": "user789",
        "burn_tx_hash": "0xfedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543221",
        "block_number": "18902345"
    }
}

Off-Ramp Step 3: Fiat Withdrawal

Platform initiates bank transfer to send fiat to the client. This follows similar patterns to the payout flow in omnibus accounts.

Variables

  • $fiat_asset (asset): Fiat asset in UMN
  • $fiat_amount (number): Fiat amount to transfer
  • $bank_number (account): Bank account identifier
  • $client_id (account): Client account identifier
  • $transfer_ref (string): Bank transfer reference

Accounts Used

  • @platform:reserves:pending_withdrawal : Fiat pending withdrawal
  • @banks:banknumber:withdrawal:bank_number:withdrawal:transfer_ref : In-flight bank transfer

Numscript

vars {
    asset $fiat_asset
    number $fiat_amount
    account $bank_number
    account $client_id
    string $transfer_ref
}

send [$fiat_asset $fiat_amount] (
    source = @platform:reserves:pending_withdrawal
    destination = @banks:$bank_number:withdrawal:$transfer_ref
)

set_tx_meta("transfer_ref", $transfer_ref)
set_tx_meta("client_id", $client_id)
set_tx_meta("type", "fiat_withdrawal")

Example Usage

USD withdrawal to bank - Try in Playground →
{
    "variables": {
        "fiat_asset": "USD/2",
        "fiat_amount": "50000",
        "bank_number": "021000089:123456789",
        "client_id": "user789",
        "transfer_ref": "WITHDRAW_20240115_USER789"
    }
}
Once the bank confirms the transfer (similar to step 3 in the omnibus payout flow), you would create a final transaction moving the funds from the in-flight withdrawal account to the bank’s main account, completing the off-ramp process.

Key Differences from Traditional Operations

Stablecoin operations combine patterns from both payment acceptance and omnibus accounts, with unique blockchain characteristics:
  • Triple Asynchrony
  • Asset Creation vs. Movement
  • Precision & Decimals
  • Reserve Management
  • Operational Characteristics
  • Multi-Network Complexity
Unlike traditional operations with one or two timing phases, stablecoins require managing three asynchronous timelines:On-Ramp:
  1. Payment authorization (instant) - User charged via PSP
  2. Blockchain mint (minutes) - Tokens created on-chain
  3. Bank settlement (days) - Fiat arrives in bank
Off-Ramp:
  1. Burn instruction (instant) - User initiates withdrawal
  2. Blockchain burn (minutes) - Tokens destroyed on-chain
  3. Fiat transfer (days) - Bank sends money to user
Each phase requires tracking in the ledger.

Advanced Ledger Features

These examples use Numscript features available in Ledger v2.3+. Ensure your deployment runs a compatible version.