Chapter 5: Fiat Ramps
The fiat ramp layer is what makes Capxul a complete financial operations platform rather than a crypto-to-crypto payment tool. It connects the stablecoin world to the fiat world — bank accounts, mobile money, and local currency.
Four Fiat Flows
Section titled “Four Fiat Flows”Flow 1: Employee Off-Ramp from Smart Account
Section titled “Flow 1: Employee Off-Ramp from Smart Account”The core employee experience. An employee has USDC in their smart account and wants fiat in their bank account or mobile money wallet.
- Source: Employee’s smart account (on-chain)
- Destination: Bank account or mobile money wallet (fiat)
- Trigger: Manual withdrawal or auto-routing rules
- KYC: Individual KYC required (see Chapter 10)
- Provider: Backend calls off-ramp provider API, gets provider deposit address, session key sends USDC to that address, provider converts and pays out fiat
Flow 2: Org Off-Ramp from Safe to Pay Fiat Vendor
Section titled “Flow 2: Org Off-Ramp from Safe to Pay Fiat Vendor”An org approves an invoice from a fiat-only vendor. The Safe routes the payment through the off-ramp provider.
- Source: Organization’s Safe (on-chain)
- Destination: Vendor’s bank account (fiat)
- Trigger: Invoice approval with fiat payment destination selected
- KYB: Org must be KYB-verified
- Provider: Same as Flow 1, but the Safe module sends USDC to the provider deposit address
Flow 3: Org On-Ramp via Virtual Account
Section titled “Flow 3: Org On-Ramp via Virtual Account”An org funds their Safe treasury with fiat. HoneyCoin provides a persistent virtual bank account. Fiat deposits to this account are converted to USDC and sent to the org’s Safe on-chain.
- Source: Fiat bank transfer to virtual account
- Destination: Organization’s Safe (on-chain, USDC)
- Trigger: External fiat deposit (Capxul is reactive, not initiating)
- KYB: Required before virtual account provisioning
Flow 4: Vendor Off-Ramp from Smart Account
Section titled “Flow 4: Vendor Off-Ramp from Smart Account”A vendor gets paid in USDC, then off-ramps from their own smart account. Identical to Flow 1 — the facade does not distinguish between employee and vendor smart accounts.
A Note on Terminology: Provider Deposit Address
Section titled “A Note on Terminology: Provider Deposit Address”Off-ramp providers generate a unique address per transaction that they control. You send USDC to it. The provider now has your USDC unconditionally. They then pay out fiat to the specified bank account because that is what their API contract says they will do. There is no conditional release, no neutral party, no smart contract holding funds in between.
The unique address exists for accounting correlation — matching an API request to a specific deposit. This is a standard pattern across all crypto off-ramp providers.
Throughout this document, this is called a provider deposit address. If you encounter “escrow” in provider documentation, it refers to the same thing, but the term is misleading — no escrow mechanism exists.
Provider Abstraction Layer
Section titled “Provider Abstraction Layer”Why a Facade
Section titled “Why a Facade”HoneyCoin is the only provider at launch. But off-ramp and on-ramp providers vary by jurisdiction, fee structure, supported currencies, payout methods, settlement speed, and reliability. No single provider covers every market. As Capxul expands, additional providers will be needed.
The facade ensures that adding a provider is a backend configuration change, not a platform rewrite. The rest of the system (Convex records, UI, session key logic, indexer) never knows which provider fulfilled a transaction.
Off-Ramp Provider Interface
Section titled “Off-Ramp Provider Interface”Every off-ramp provider must expose:
- getSupportedCurrencies(country) — available fiat currencies and payout methods
- getSupportedBanks(country, currency) — banks and mobile money operators
- getQuote(amount, token, chain, currency, country, payoutMethod) — expected fiat amount, exchange rate, fees
- createTransaction(amount, token, chain, currency, country, payoutDetails, webhookUrl, externalReference) — initiates the off-ramp, returns a provider deposit address with expiry
- getTransactionStatus(providerTransactionId) — polls status (fallback if webhooks are delayed)
- parseWebhook(payload, signature) — validates and parses incoming webhooks
On-Ramp Provider Interface
Section titled “On-Ramp Provider Interface”- provisionVirtualAccount(orgId, orgName, country, currency) — creates a persistent virtual bank account
- getVirtualAccountStatus(accountId) — current state of the virtual account
- parseDepositWebhook(payload, signature) — validates incoming deposit notifications
Provider Registry
Section titled “Provider Registry”A Convex table rampProviders stores provider configuration: ID, type (off_ramp, on_ramp, both), API base URL, supported countries, currencies, payout methods, active flag, and priority. At launch: one row (HoneyCoin, type both, covering the initial launch markets). See Appendix C for provider-to-country mappings. The registry supports any number of providers covering any number of countries.
Provider Routing
Section titled “Provider Routing”When a fiat transaction is initiated:
- Look up destination country and currency
- Find active providers supporting that country, currency, and payout method
- One provider: use it. Multiple: select by priority (future: lowest fee or best quote)
- No provider: reject with a clear error
This runs in the Convex backend. The frontend never selects a provider.
The fiatTransactions Table
Section titled “The fiatTransactions Table”Off-ramp and on-ramp transactions do not belong in financialDocuments. Financial documents are accounting artifacts. Fiat transactions are operational processes with different lifecycles, fields, and failure modes. The two tables reference each other — a completed off-ramp generates a receipt in financialDocuments.
Schema
Section titled “Schema”Shared fields: transaction ID, direction (off_ramp/on_ramp), status, org ID, actor ID/type, source type (smart_account/safe), source address, provider ID, provider transaction ID, external reference, timestamps.
Off-ramp fields: crypto amount, token, chain, fiat amount, currency, exchange rate, provider fee, net fiat amount, payout method (bank_transfer/mobile_money), payout details, provider deposit address, deposit address expiry, on-chain tx hash, fiat payout reference.
On-ramp fields: fiat amount deposited, currency, exchange rate, provider fee, crypto amount minted, destination chain/address, mint tx hash, virtual account ID.
Off-Ramp Lifecycle
Section titled “Off-Ramp Lifecycle”requested -> quoted -> deposit_address_generated -> crypto_sent -> crypto_confirmed -> fiat_processing -> fiat_completeFailure states: failed (before crypto sent), expired (quote or address expired), tx_failed (on-chain revert), refunding -> refunded (provider refunding), fiat_failed (bank rejection).
| Status | What Happened |
|---|---|
| requested | User initiated. Convex record created. |
| quoted | Provider returned exchange rate and fees. |
| deposit_address_generated | User confirmed. Provider returned deposit address (1-hour expiry for HoneyCoin). |
| crypto_sent | On-chain tx submitted via session key or module. Tx hash recorded. |
| crypto_confirmed | On-chain tx confirmed (indexer or receipt polling). Provider has the funds. |
| fiat_processing | Provider webhook: fiat payout initiated. |
| fiat_complete | Provider webhook: fiat landed. Terminal success. Receipt generated. |
On-Ramp Lifecycle
Section titled “On-Ramp Lifecycle”deposit_received -> converting -> minting -> mint_confirmed -> completeOn-ramp is simpler because Capxul is reactive. The org (or someone paying the org) makes a bank transfer. Capxul receives a webhook, tracks the conversion, and confirms the USDC lands in the Safe (detected by the indexer).
Off-Ramp Orchestration
Section titled “Off-Ramp Orchestration”Employee Off-Ramp (Flow 1 / Flow 4)
Section titled “Employee Off-Ramp (Flow 1 / Flow 4)”- User initiates. Selects amount, fiat currency, payout destination. Frontend calls Convex mutation.
- Convex creates record. Status:
requested. Validates: KYC verified? Sufficient balance? Valid destination? - Backend gets quote. Calls provider via facade. Record advances to
quoted. Quote returned to user. - User confirms. Backend calls
createTransaction. Provider returns deposit address and expiry. Record:deposit_address_generated. - Backend submits on-chain tx. Session key constructs UserOp sending exact crypto amount to provider deposit address. Tx hash recorded. Status:
crypto_sent. - Indexer confirms. Transfer event detected. Status:
crypto_confirmed. - Provider webhook: processing. Status:
fiat_processing. - Provider webhook: complete. Status:
fiat_complete. Receipt generated infinancialDocuments.
Org Off-Ramp to Pay Fiat Vendor (Flow 2)
Section titled “Org Off-Ramp to Pay Fiat Vendor (Flow 2)”Same flow but triggered by invoice approval with fiat destination. Source is the Safe (not smart account), executed via Safe module (not session key). The invoice transitions to paid when fiat_complete is reached.
Timing and Expiry
Section titled “Timing and Expiry”The provider deposit address expires after 1 hour (HoneyCoin). The backend submits the on-chain transaction immediately after receiving the address. If submission fails, the address expires unused — no funds at risk. If the transaction takes unusually long to confirm, the backend monitors and resubmits with higher gas before the window closes.
Idempotency
Section titled “Idempotency”The fiat transaction’s Convex ID is the externalReference passed to the provider. Duplicate webhooks for an already-completed transaction are no-ops. Status transitions are validated (cannot jump from requested to fiat_complete).
On-Ramp Orchestration
Section titled “On-Ramp Orchestration”Virtual Account Provisioning
Section titled “Virtual Account Provisioning”When an org completes KYB and requests on-ramp:
- Backend calls
provisionVirtualAccount - Provider returns persistent virtual account details (account number, bank name)
- Stored in Convex on the org record
- Org admin sees details in their dashboard
Deposit Processing
Section titled “Deposit Processing”- External party sends fiat to the virtual account
- Provider webhook:
deposit_received. Fiat transaction record created. - Provider converts fiat to USDC and sends to Safe address. Status advances through
converting->minting. - Indexer detects USDC Transfer into the Safe. Treasury balance updates. Status:
mint_confirmed. - Status:
complete. Deposit appears in treasury activity feed.
Auto-Routing
Section titled “Auto-Routing”Auto-routing lets employees configure rules like “send 60% to GTBank and keep 40% in crypto” and have this execute automatically on each claim.
Routing Rules
Section titled “Routing Rules”Stored in Convex per employee:
- Enabled flag
- Trigger type:
on_claim(at launch).scheduledandthresholddesigned in schema but deferred. - Rules: ordered list of
{ destinationType, destinationDetails, percentage }. Must sum to 100%.
Execution Flow (On-Claim Trigger)
Section titled “Execution Flow (On-Claim Trigger)”- Indexer detects a claim event
processClaimEventmutation creates the claim receipt and checks: auto-routing enabled?- If yes, schedules an auto-routing action
- The action reads routing rules and claimed amount
- For each fiat destination: initiates off-ramp (without user confirmation — pre-approved via routing rules and session key grant)
- For each crypto wallet destination: direct transfer via session key
- “Retain” percentage stays in the smart account
Failure Handling
Section titled “Failure Handling”Each routing destination is an independent transaction. If one destination fails (provider down, tx reverts), funds for that destination remain in the smart account. The employee is notified. Other destinations proceed independently. Auto-routing never fails atomically.
Why On-Claim First
Section titled “Why On-Claim First”Scheduled triggers require periodic balance checking across all employees. Threshold triggers require polling or hooking every Transfer event. On-claim is a natural hook — the indexer already detects claims. Adding “and then route” is minimal. It covers the most common case: employee claims pay and wants it split immediately.
Session Key Design for Off-Ramp
Section titled “Session Key Design for Off-Ramp”The session key is what allows the backend to move funds from a smart account without requiring the employee to sign every transaction. See Chapter 3: Session Key Model for the authoritative technical reference on V1 session key capabilities.
The session key is granted during initial routing setup. The on-chain whitelist includes the USDC contract (Path A) or the CapxulRouter contract (Path B). Destination enforcement happens in Convex (Path A) or on-chain via the CapxulRouter allowlist (Path B).
What Does NOT Require Re-Granting
Section titled “What Does NOT Require Re-Granting”Changing routing percentages, adding/removing bank accounts (fiat details are in Convex), changing trigger type, pausing auto-routing, swapping the off-ramp provider.
What DOES Require Re-Granting
Section titled “What DOES Require Re-Granting”Adding a new token, changing per-transaction limits, session key expiry renewal (every 6 months), migrating from Path A to Path B.
Security Model
Section titled “Security Model”If the backend is compromised: an attacker can trigger off-ramp transactions to bank accounts configured in the user’s profile. Every fraudulent transaction leaves an audit trail in both Convex and the off-ramp provider. The fiat destination is a real, KYC-verified bank account. Transaction amounts are capped by the user’s configured limits.
Webhook Processing
Section titled “Webhook Processing”Single Endpoint
Section titled “Single Endpoint”A single Convex HTTP action receives webhooks from all providers. It validates the signature, parses with the provider-specific parser, looks up the fiat transaction by provider transaction ID, and calls the status transition mutation.
Reliability
Section titled “Reliability”Delayed: Not a problem. The record sits in its current status.
Duplicated: Not a problem. Status transitions are idempotent.
Lost: The real concern. A Convex scheduled function runs every 5 minutes scanning for transactions stuck in intermediate states. For each, it calls getTransactionStatus on the provider. This is polling-as-fallback.
Security
Section titled “Security”The endpoint is public. It validates webhook signatures, verifies the referenced transaction exists, and checks that the status transition is valid. Invalid webhooks are logged and dropped.
KYC/KYB Gating
Section titled “KYC/KYB Gating”Verification checks run before any provider interaction. See Chapter 10 for the full verification architecture.
- Employee off-ramp:
getActorVerificationLevelchecks individual KYC tier against jurisdictional thresholds - Org off-ramp/on-ramp:
getActorVerificationLevelchecks org KYB status - Auto-routing: Verification checked on every execution, not just at session key grant time (verifications can expire)
Cost Projections
Section titled “Cost Projections”Per-Transaction (Provider Costs)
Section titled “Per-Transaction (Provider Costs)”- HoneyCoin fee: 0.3-1% of transaction value
- FX spread: 0.2-0.8%
- Gas: negligible on Base
- Bundler cost: typically < $0.01 on Base
For a 500 USDC off-ramp to NGN, the employee receives approximately 485-495 USDC equivalent in naira after fees and spread.
Infrastructure Costs
Section titled “Infrastructure Costs”Minimal: webhook endpoint (Convex HTTP action), status polling (Convex scheduled function), session key transactions (Base gas). The dominant cost is the provider’s per-transaction fee, not Capxul’s infrastructure.
Virtual Cards (Planned)
Section titled “Virtual Cards (Planned)”Virtual cards are a frequently requested capability that extends the off-ramp model. Instead of converting stablecoins to fiat via bank transfer or mobile money, the platform would issue virtual debit cards funded from the employee’s smart account balance.
Why Virtual Cards Matter
Section titled “Why Virtual Cards Matter”- Instant spending. No waiting for off-ramp settlement. Funds are available immediately via card.
- Online purchases. Employees can pay for goods and services directly without off-ramping to a bank first.
- Expense management. Organizations can issue cards with spending limits to employees for business expenses.
- Reduced friction. For employees who receive stablecoins but need to spend in fiat day-to-day, a virtual card eliminates the manual off-ramp step.
Architecture Fit
Section titled “Architecture Fit”Virtual cards fit naturally into the existing facade pattern. A card provider (e.g., Reap, Immersve, or a regional issuer) is added behind a card facade with the same patterns as the fiat ramp facade: provider abstraction, Convex as source of truth, session key orchestration for funding.
The smart account is the funding source. When the card is used, the card provider debits from a pre-funded balance or triggers a just-in-time transfer from the smart account via session key.
Status
Section titled “Status”Design and provider evaluation deferred. This section will be expanded when virtual card integration enters active planning.