Fiat-to-Crypto Payment Gateways: How On/Off-Ramp Infrastructure Works
Post Your First Suspense-Account Transaction
Clone Formance Ledger on GitHub, run it locally, and post your first suspense-account transaction.
Post Your First Suspense-Account Transaction
Clone Formance Ledger on GitHub, run it locally, and post your first suspense-account transaction.
Conventional payment gateways settle inside a single regulated rail (card networks, ACH, SEPA) where the rail operator defines finality. A fiat-to-crypto gateway spans two settlement systems with incompatible rules: the fiat legs stay reversible for days while the on-chain legs become final in minutes and cannot be reversed.
The rules mismatch shows up mid-transaction. A custody instruction times out, no transaction ID is recorded, and $2,500,000 sits between the bank and the chain with no ledger state describing the funds. We call that stranded value the Ramp Gap: the period between fiat settlement and confirmed on-chain delivery when value belongs to neither the bank nor the chain, and only the internal ledger can attribute it.
A fiat-to-crypto payment gateway uses on- and off-ramp infrastructure to bridge banking networks and blockchains, converting government-issued currency into crypto and back. Building the gateway reliably means keeping the Ramp Gap attributable to a named user, in real time, on every transaction.
This guide covers on-ramp and off-ramp flows, the gateway's five components, duplicate-safe webhook ingestion, reconciliation, and pre-launch tests.
An on-ramp converts fiat (USD, EUR) into digital assets or stablecoins across three sequential stages: KYC and AML verification, payment initiation, and fiat-to-asset conversion with on-chain delivery.
The core operational risk is the Ramp Gap: fiat has settled, but the on-chain leg has not, and the internal ledger must represent that window explicitly, treating fiat reversibility and on-chain finality as separate policy inputs. Stablecoin finality informs that policy split.
Know Your Customer (KYC) and Anti-Money Laundering (AML) verification collects identity evidence, such as government-issued ID and personal details, and produces a state (pending, approved, rejected, or manual_review) that gates every downstream transition. Treat verification status as mutable after onboarding because a one-shot check at signup is insufficient.
Payment initiation funds the transaction through traditional payment rails like credit cards, debit cards, Automated Clearing House (ACH), or Single Euro Payments Area (SEPA) transfers. Assign each rail a return-window policy, and use that policy as a transition guard so no downstream posting treats the funds as final while a pullback is still possible.
Fiat-to-asset conversion swaps received fiat for digital assets through liquidity providers or partner exchanges, then delivers the digital assets to the user's on-chain wallet. During the Ramp Gap, the funds sit in a pending-delivery suspense account on the core ledger; a role Formance Ledger fills with atomic double-entry postings and per-user in-flight balances readable in real time.
A fiat off-ramp turns crypto back into spendable local currency across four sequential stages: crypto deposit, compliance review, liquidity execution, and fiat payout. The Ramp Gap runs in reverse here — crypto has been received on-chain but fiat has not yet cleared to the user's bank account — so the same suspense-account discipline applies.
The off-ramp state machine runs the sequence:
Quote issued → identity/KYC cleared → asset received → conversion executed → payout instructed → settled → credited.
Each transition has an event, a guard, and a resulting ledger posting:
| State | Event | Guard | Resulting posting |
| quote_issued | User accepts quote | Quote within validity window | None; no value has moved |
| awaiting_deposit | Transaction first seen on-chain | None yet | None; record the observation only |
| asset_received | Confirmation depth reached | N confirmations for that chain | Credit user's off-ramp pending account from the custody deposit account |
| compliance_cleared | Screening result returned | Screening returns pass | None on pass; on reject, a compensating posting to frozen-funds |
| conversion_executed | Fill confirmed by liquidity venue | Executed inside the rate-lock window | Digital assets out, fiat into the settlement account |
| payout_instructed | Bank rail accepts instruction | Fiat present in settlement account | Move fiat into a payout-pending suspense |
| settled/credited | Rail settlement confirmation | Return-window policy elapsed | Clear the payout suspense |
Crypto deposit begins when the user initiates a withdrawal request and sends the chosen amount of digital assets to the platform's designated wallet. The handler records first sight of the deposit as an observation only. The configured confirmation depth is the guard that permits a posting.
Compliance review runs the ongoing AML, transaction monitoring, and Travel Rule checks that gate the transition from received asset to conversion, and the compliance-review outcomes flow through the same state model as onboarding KYC.
Counterparty data exchange is a live transition on many corridors, and when the travel rule meets stablecoin flows, a failed exchange must mark the transaction in a compliance-hold state.
Liquidity execution sells the deposited digital assets through an internal pool or an OTC desk. The quoted rate is only valid for a set window, so if the fiat leg settles after the quote expires, the platform re-quotes rather than filling at the old rate.
Fiat payout sends local currency to the user's bank account or debit card through a regional rail. The rail can still reverse the payment during its return window (for example, ACH returns), so the payout suspense stays open until that window closes.
Both on-ramp and off-ramp flows depend on the same five components staying in lockstep across every Ramp Gap transition: payment rails, the core ledger, custody, compliance engines, and liquidity providers. Value goes missing when any two disagree about a transaction's state.
Payment rail adapters wrap provider APIs (ACH, SEPA, Visa/Mastercard, and blockchain nodes) and normalize vendor payloads onto one internal event contract before the ledger sees any event. The contract carries the provider event ID, provider transaction ID, event type, event version and timestamp, a normalized error class, and the retained raw payload.
Bank-rail events (ACH returns and SEPA R-transactions) and chain-confirmation events feed separate state-machine guards on separate settlement clocks. A confirmation callback satisfies a guard but does not override the settlement policy.
The core ledger records every in-flight and settled balance across fiat and crypto in double-entry form. Its most consequential job is holding the Ramp Gap in a pending-delivery suspense account: debited on fiat settlement and cleared only on confirmed on-chain credit.
Never credit the user's final balance from a bank event. Crediting twice double-counts, and holding fiat with no posting creates unattributed liability. The debit-then-clear order has to run in a concurrency-safe layer close to the ledger write path.
The Prime Trust receivership shows what happens without a defensible ledger representation of the Ramp Gap. By mid-2023, the custodian owed clients over $85M against roughly $3M in cash and was placed in receivership for using customer funds to buy replacement crypto. A pending-delivery suspense account with confirmation guards keeps funds-in-flight value inside an omnibus structure attributable to each user.
Custody is where the gateway's crypto assets live, and the ledger must mirror the custody structure one-to-one or reconciliation breaks. Custody instructions must be idempotent, and every custody move (deposit, withdrawal, internal transfer, and key rotation) must post to the sub-ledger before the on-chain transaction acknowledges it.
Most gateways run an omnibus wallet: a single on-chain account holding crypto for many users, with an internal sub-ledger attributing each unit to an owner. Reconcile the on-chain omnibus balance against the sum of sub-ledger balances continuously. This fiat-to-digital-asset playbook keeps off-chain cash and on-chain assets in sync under this model.
Compliance engines run sanctions screening, identity validation, and Travel Rule checks, and the results decide whether the ledger can move to the next state. If a screen rejects a transaction after the fiat has already been debited into a suspense account, the ledger must post a compensating entry that moves the funds to a refund or frozen-funds account. Flagging the user in a CRM does not move any money.
Liquidity providers are the market makers and exchanges that quote prices and hold inventory for the conversion. Each quote is valid for a fixed window. If the fiat settles after that window closes, the platform re-quotes before filling. The gap between the quoted rate and the actual fill (the provider fee variance) is a reconciliation breakpoint that must be addressed.
Numscript, Formance's transactional language, describes financial transactions in the ledger. On fiat receipt of $2,500,000, the wire moves from the bank boundary account into a user-specific pending account, opening the Ramp Gap for that user:
// ONRAMP_FIAT_RECEIVED
// Event: receive $2,500,000 for the user's pending on-ramp
send [USD/2 250000000] (
source = @platform:banks:primary:main allowing unbounded overdraft
destination = @users:8452:onramp:pending
)
set_tx_meta("event_type", "onramp_fiat_received")
set_tx_meta("onramp_id", "onr001")
The user's available balance is untouched. The balance of @users:8452:onramp:pending is that user's Ramp Gap exposure and must be readable during an incident. On confirmed on-chain delivery, the next transaction closes the Ramp Gap by clearing the suspense, routing fiat to the liquidity provider, and crediting the digital asset:
// ONRAMP_CONVERSION_DELIVERED
// Event: convert the pending $2,500,000 and deliver 2,500,000 USDC
send [USD/2 250000000] (
source = @users:8452:onramp:pending
destination = @counterparties:liquidityProviders:001
)
send [USDC/6 2500000000000] (
source = @counterparties:liquidityProviders:001 allowing unbounded overdraft
destination = @users:8452:available
)
set_tx_meta("event_type", "onramp_conversion_delivered")
set_tx_meta("onramp_id", "onr001")
Check for a repeat event before writing to the ledger, and process events in version order rather than arrival order, to keep webhook ingestion safe from duplicates.
The deduplication key is the provider ID plus the provider's event ID, checked against an idempotency record before writing the posting. If the deduplication key is already present, return the stored result rather than writing a second posting. Checking after the write means correcting an extra posting in a table with millions of rows.
Process versioned events so out-of-sequence retries don't overwrite state. Discard a lower-version pending payload that arrives after a higher-version confirmed one. Keep both the reorg reversal and the original posting queryable for your audit retention window.
The same duplicate-safe pattern resolves a Ramp Gap that stalls between bank and chain. Send the same idempotency key with the custody instruction so a retry after a dropped response returns the original result instead of signing a second transfer.
Then carry a broadcast_pending state that holds the funds in the pending-delivery suspense account until either a txid or a definitive not-found returns. The balance of @users:*:onramp:pending reports how much value sits in broadcast_pending right now.
Ramp reconciliation compares the bank statement, the internal ledger, and the custody or on-chain balance and must handle three Ramp Gap-specific exception classes: timing skew, fee variance, and suspense aging. Standard reconciliation patterns for high-volume fintech apply as a baseline.
The bank and the blockchain often record the same transaction on different days. Match transactions using your state machine's transition times, not either clock alone.
The amount that fills is rarely the exact amount that was quoted. Record the difference as its own posting. Hiding small differences under a threshold lets fee drift build up unnoticed in your P&L.
Any pending balance older than your normal confirmation-plus-retry window means a Ramp Gap is stuck between systems. Flag it as an exception.
When suspense balances live in named ledger accounts, stuck value shows up the day it happens and not at month-end.
A reliable fiat-to-digital-asset payment gateway comes down to keeping the Ramp Gap attributable across five components: payment rails, the core ledger, custody, compliance engines, and liquidity providers.
The core ledger holds a pending-delivery suspense account for every Ramp Gap balance, debited on fiat settlement and cleared only on confirmed on-chain credit, exactly like the Numscript postings above. Provider boundaries use duplicate-safe ingestion, while reconciliation runs against state-machine transitions rather than a single clock.
Formance Ledger provides the double-entry primitives, atomic postings, and immutable audit trail those patterns depend on, and Formance Reconciliation runs the timing-skew, fee-variance, and suspense-aging checks against the same accounts your ramp writes to in production.