Cash — buy crypto with fiat
Not every user arrives holding crypto. The onramp rail lets them fund your app with a card or bank: they pick a fiat currency, enter an amount, the Kit prices it against the destination asset your app needs, and hands them off to a provider's hosted checkout to pay. When the purchase settles, the funds land in the same destination your crypto deposits use — no manual bridging, no wallet required to start.
The onramp rail sits beside the on-chain crypto rail inside the same <DepositDialog />. It is on by default — every integration that ships a <DepositDialog /> gets the "Cash" rail unless it explicitly opts out (see Enabling the rail).
How it works
The onramp is a hosted-checkout flow — the provider collects the payment details on its own secure page, so the Kit never touches card data. End to end:
The deposit method picker shows a Cash rail next to Crypto. The Cash rail carries a single payment-method row, Debit Card, which goes straight to amount entry — there is no in-app card form (the provider's page collects payment details).
The user enters a fiat amount. A currency chip (country flag + code) opens a searchable currency picker; picking a currency re-prices the quote in it. The live quote resolves as you type, showing what they'll receive (destination asset + amount), the rate, and the fees — and a provider card showing the auto-picked provider and estimated delivery time. The Continue CTA stays disabled while the quote is loading or rejected.
A priced summary. When the provider requires a third-party-consent disclaimer, the confirm screen renders an inline consent block (provider, "You will receive ≈ …", delivery estimate, Terms/Privacy/Support links) and gates Continue to payment behind a required acknowledgement checkbox.
The Kit creates a session and opens the provider's checkout in a centered popup window.
An in-dialog Finish your payment screen shows a pay → buy → receive flow strip — the fiat you pay, the asset the provider buys at the deposit address, and the asset you ultimately receive — and polls the session, updating automatically.
The session resolves to completed, expired, or failed, and the Kit shows the matching success / failure receipt — both carry the order context (You pay, You receive, Destination, Order ID). On success the destination is funded with the asset your app configured.
The destination asset, network, and address are exactly the ones you already configured on the provider for the crypto rail — the onramp reuses them, so a user who buys with a card ends up with the same asset as a user who deposits on-chain.
Currencies & money formatting
The amount-entry screen is multi-currency. The currency chip opens a searchable picker (popular currencies first, then the full list, with instant client-side filtering over code / name / symbol). Picking a currency re-quotes and re-creates the session in it; the choice persists across opens and preselects next time, falling back to the country-resolved default.
Every fiat value formats correctly per currency — symbol placement and fraction digits follow each currency's rules (JPY / KRW render with 0 decimals, EUR / GBP with 2). Currency rows and the chip render the country flag through the Kit-owned CurrencyGlyph, resolved from a public-domain flag CDN (no bundled assets), with a themed symbol/code fallback.
The UAE dirham's modern sign (Unicode 18.0 U+20C3) has no font coverage on today's devices, so the bare codepoint would render as a tofu box. The Kit ships the official mark as an inline SVG and routes every AED amount through it automatically — you don't need to do anything. The same FiatSymbol / FiatAmount / formatFiat primitives are exported from @stridge/kit/ui and @stridge/kit/format if you render fiat in your own chrome.
Providers & the consent gate
The onramp is single-provider today — the gateway resolves one provider server-side and the Kit surfaces it as the active provider carrying the one real quote. The provider surface is built for more: a provider card on amount entry and a provider sub-screen are already wired, and the picker shows a single "More providers coming soon" row rather than fabricated brands. Adding a real second provider later is data-only — no FSM or UI changes.
When the active provider carries a disclaimer, the confirm screen gates the hand-off behind a consent checkbox (per-attempt, never persisted). The disclaimer names the provider and links its Terms / Privacy / Support pages — the single sanctioned surface where the integrated provider is named. Absent a disclaimer, the CTA works immediately.
Enabling the rail
The rail is on by default. Crypto-only integrations (or any host without an onramp-capable driver) opt out with flows.deposit.methods.onrampRail.enabled: false.
"use client"
import { StridgeProvider, DepositDialog } from "@stridge/kit"
export function App({ children }) {
return (
<StridgeProvider
gatewayKey={process.env.NEXT_PUBLIC_STRIDGE_GATEWAY_KEY}
flows={{
deposit: {
destination: { address: treasury, asset: "USDC", network: "60" },
methods: {
// The "Cash" rail is on by default. Opt out for a crypto-only experience:
onrampRail: { enabled: false },
},
},
}}
>
{children}
<DepositDialog />
</StridgeProvider>
)
}When only one rail is enabled, the picker collapses to a flat method list — the rail tabs only appear when both the crypto and onramp rails are available. Setting onrampRail.disabled: true hides the rail too (equivalent to enabled: false).
The onramp catalog is fetched lazily, only once the user picks the Cash rail — a deposit that never opens Cash makes no onramp request — so the rail's visibility never depends on a catalog round-trip.
Opening straight into Cash
Just like the wallet and transfer rails, you can open the dialog directly into the Cash flow from the deposit hook — handy for a dedicated "Buy with card" button. Pass an optional amount / currency to preselect the band and price the quote on entry; the driver arms and the catalog / currency / provider fetches fire just as picker entry does.
"use client"
import { useDeposit } from "@stridge/kit"
export function BuyWithCardButton() {
const { open } = useDeposit()
return (
<button onClick={() => open({ method: "onramp", amount: 100, currency: "USD" })}>
Buy with card
</button>
)
}open({ method: "onramp" }) returns false only when the host has opted the rail out.
Compound & headless
The onramp screens follow the same four-tier model as the rest of the Kit:
- Drop-in —
<DepositDialog />renders every onramp screen (amount entry, currency picker, provider picker, confirm, payment-pending, success, error) with no extra wiring. - Compound —
OnrampAmountEntry,OnrampConfirm,OnrampPaymentPending,OnrampCurrencyPicker, andOnrampProviderPickerare exported from@stridge/kit/deposit/widgets; compose their parts (OnrampAmountEntry.CurrencyChip/.ProviderCard,OnrampConfirm.Disclaimer, …) into your own layout via the@stridge/kit/deposit/compoundscaffolding. - Headless — the deposit hooks (
useDeposit,useDepositState,useDepositSnapshot) expose the onramp states and snapshot entities. Render your own UI; the Kit owns the state machine and the gateway calls.
The headless surface for the onramp:
| Surface | Members |
|---|---|
FSM states (useDepositState().name) | onrampAmountEntry, onrampCurrencyPicker, onrampProviderPicker, onrampConfirm, onrampPaymentPending |
Snapshot entities (useDepositSnapshot()) | onrampMethods, onrampCurrencies, onrampProviders, onrampQuote, onrampSession |
Actions (useDeposit().actions) | selectOnramp, setOnrampAmount, confirmOnrampAmount, openOnrampCurrencyPicker, selectOnrampCurrency, openOnrampProviderPicker, selectOnrampProvider, confirmOnramp |
See Compound components and Headless integration for the general patterns — the onramp parts slot into both.
SDK-level contract
If you are building the onramp surface from scratch (no Kit UI), the @stridge/sdk client speaks the gateway onramp API directly via stridge.onramp — browse the catalog, price a quote, open a provider-hosted checkout session, and poll it to a terminal state:
import { createApiClient, isOnrampTerminalState } from "@stridge/sdk"
const stridge = createApiClient({ projectKey, env: "prod" })
// 1. Catalog — fiats, crypto, payment methods for the resolved country.
const catalog = await stridge.onramp.catalog({ country: "US" })
// 2. Quote — indicative price; only the fiat side is needed.
const quote = await stridge.onramp.quote({ fiatCurrency: "USD", fiatAmount: "100" })
// 3. Session — binds the quote, allocates the deposit address, opens the checkout.
const session = await stridge.onramp.createSession({
owner,
fiatCurrency: "USD",
fiatAmount: "100",
destinationAsset: "USDC",
destinationNetwork: "60",
destinationAddress,
})
// → redirect the user to session.checkoutUrl
// 4. Poll — until the state is terminal.
const latest = await stridge.onramp.getSession(session.sessionId, { ifNoneMatch: session.etag })
if (isOnrampTerminalState(latest.state)) { /* completed / expired / failed */ }The session response is flow-nested — flow.fiat (what the user pays), flow.udaEntry (the crypto the provider deposits), and flow.destination (what the user receives, and where). Reads honor If-None-Match for cheap ETag polling. For a list/dashboard surface, poll stridge.onramp.listOwnerSessions(owner) instead. See Onramp endpoints for the full method reference.
Testing
The hosted-checkout provider runs a sandbox. Point the SDK at the dev environment (env: "dev") and use the provider's sandbox credentials (test card, verification PIN, and the documented triggers for complete / expired / cancelled / declined outcomes) — see your provider's own sandbox docs for the current values. No crypto moves in sandbox.
Because the provider's hosted page is a third party, driving it from an automated test is brittle. For integration tests, use the non-production state override to push a session straight to a terminal state without completing the real checkout:
// Non-production only — the route is not registered in production.
await stridge.onramp.setSessionState(session.sessionId, { state: "SESSION_COMPLETED" })
// or drive a failure:
await stridge.onramp.setSessionState(session.sessionId, {
state: "SESSION_FAILED",
failureCode: "payment_declined",
})This lets a test exercise the full create → poll → terminal path deterministically. It is rejected in production, so it can never short-circuit a real purchase. See Onramp endpoints → setSessionState for the full contract.