Developers

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. It is also a two-leg flow: the provider delivers crypto to a Stridge deposit address (leg 1), then Stridge settles that crypto to your configured destination (leg 2). Paying the provider is a milestone, not the finish line — success is shown only once leg 2 lands. End to end:

Rail

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).

Amount + currency

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 entry is validated against the provider's per-currency limits, so a too-small or too-large amount is caught here rather than at the provider (see Per-currency limits). The Continue CTA stays disabled while the quote is loading or rejected.

Confirm

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.

Hosted checkout

The Kit creates the session and opens the provider's checkout in a new browser tab automatically, at the step transition. A new tab (rather than a popup) survives mobile popup blockers and keeps the Kit's own tab alive so it can keep polling the session to completion — a full-window redirect would tear that down. If a strict popup blocker still refuses the post-round-trip open, the pending screen's Reopen payment tab button — a direct user click that is never blocked — brings it back.

Track (the two legs)

An in-dialog Finish your payment screen shows a You pay → You buy → You 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. Each leg carries its own check / active / pending signal. When the provider's deposit lands (SESSION_COMPLETED), the strip flips to its settling phase: "You pay" and "You buy" check off, "You receive" goes active, the copy shifts to "we're crediting your account", and the reopen / back affordances retire (there is nothing left to reopen).

Terminal

Success is shown only when leg 2 — Stridge's settlement to your destination — succeeds, funding the destination with the asset your app configured. A session that expires or fails before payment routes to a retryable error back to amount entry. A settlement that fails after payment routes to a non-retryable contact-support screen — retrying would risk a double charge. Both receipts carry the order context (You pay, You receive, Destination, Order ID).

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.

Note

SESSION_COMPLETED is the provider-deposit milestone (the provider delivered crypto to the Stridge deposit address), not the final success. The Kit reaches the true terminal verdict from the session's delivery leg (flow.entry.settlement_state) when the backend exposes it — which works with no wallet connected — and otherwise from an owner-scoped settlement watcher. See SDK-level contract.

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). Quick-amount preset chips are currency-aware too ($1K, ¥20K) and clamped to the provider's accepted range so they never overshoot the limit. 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.

Note

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.

Per-currency limits

The provider quotes a different [min, max] per fiat — AED [20, 20000] reads very differently from JPY. The Kit reads those limits from the catalog scoped to the selected currency, refetches whenever the currency changes, and gates the amount entry against them so the user never bounces off a generic provider error:

  • Below the floor — the CTA flips to Use min (snaps the entry up to the floor) alongside a clear Minimum {x} notice, instead of a generic "Something went wrong".
  • Above the ceiling — the CTA flips to Use max and snaps the entry down to the ceiling, mirroring the wallet-deposit "Deposit max".
  • Unavailable — when the provider exposes a zero ceiling for the region, the entry shows a region notice.

These range notices win over the quote error, so a too-small / too-large amount always shows the actionable bound rather than the provider's raw message.

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.

Resuming an interrupted payment

The hosted checkout lives in its own tab, and people wander — they close the dialog, reload the page, or close the Kit's tab entirely. The onramp mirrors the crypto-deposit settlement-resume pattern so a paid (or paying) user is never stranded. There are two recovery paths:

  • Closed the dialog (or reloaded), Kit tab still open. The pending session is persisted to Kit storage, and a non-blocking "Return to payment" banner (OnrampStatusBanner) re-detects it. Tapping it re-enters the pending screen with the same session and resumes polling — and can reopen the checkout tab. The banner is dismissible and matches the crypto-deposit resume banner pixel-for-pixel.
  • Closed the Kit tab entirely. A banner can't help here (it only shows while the dialog is open), so the backend bounces the provider's checkout tab back to your app with ?stridge_onramp_session={id}. The Kit mounts a headless reader that captures that param at first paint, strips it from the URL immediately, and auto-reopens the dialog and resumes — straight to the right screen for the session's true state (still paying, settling, delivered, or failed). This resume works even with no wallet connected and survives a reload mid-resume.

The return-bounce is default-on. It only fires when your app's origin is on the gateway key's referrer whitelist (the Kit sends its own page URL as return_app_url; the backend validates it). Opt out with onrampReturnResume={false} on <StridgeProvider />:

App.tsx
<StridgeProvider gatewayKey={key} flows={{ deposit: { destination } }} onrampReturnResume={false}>
  {children}
  <DepositDialog />
</StridgeProvider>

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.

App.tsx
"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.

BuyWithCardButton.tsx
"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.

Observing the flow

Hosts can observe the Cash flow over the typed event bus — four flow-tier events alongside the rest of the deposit events:

EventFires when
deposit.onramp.session.createdA session was created, the checkout opened in a new tab, and the FSM advanced to the pending screen.
deposit.onramp.session.resumedA background probe re-detected a pending session the user had left (the "Return to payment" path).
deposit.onramp.session.completedThe session reached SESSION_COMPLETED — the provider deposited (leg 1).
deposit.onramp.session.failedThe session reached SESSION_EXPIRED / SESSION_FAILED — read session.failureCode for the reason.

Each payload carries the full onrampSession entity (the checkoutUrl + the fiat / receive legs). Subscribe through the same events surface as every other Kit event.

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) plus the resume banner, with no extra wiring.
  • CompoundOnrampAmountEntry, OnrampConfirm, OnrampPaymentPending, OnrampCurrencyPicker, OnrampProviderPicker, and OnrampStatusBanner are exported from @stridge/kit/deposit/widgets; compose their parts (OnrampAmountEntry.CurrencyChip / .ProviderCard, OnrampConfirm.Disclaimer, OnrampPaymentPending.Flow / .ReopenButton, …) into your own layout via the @stridge/kit/deposit/compound scaffolding.
  • 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:

SurfaceMembers
FSM states (useDepositState().name)onrampAmountEntry, onrampCurrencyPicker, onrampProviderPicker, onrampConfirm, onrampPaymentPending (+ the shared terminal success / error)
Snapshot entities (useDepositSnapshot())onrampMethods, onrampCurrencies, onrampProviders, onrampQuote, onrampSession
Actions (useDeposit().actions)selectOnramp, setOnrampAmount, confirmOnrampAmount, openOnrampCurrencyPicker, selectOnrampCurrency, openOnrampProviderPicker, selectOnrampProvider, confirmOnramp

onrampPaymentPending carries a ctx.phase of awaitingProvider (waiting on the provider deposit) or settling (provider deposited, Stridge delivering) — branch your pending UI on it to render the two-leg progress. The resume paths are driven by the OnrampStatusBanner widget and the onrampReturnResume reader, backed by the resumeToOnrampPending / resumeToOnrampSuccess / resumeToOnrampError actions; in the headless tier they let you rebuild the close/return recovery yourself.

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. The request/response fields are snake_case, matching the wire contract:

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({ fiat_currency: "USD", fiat_amount: "100" })

// 3. Session — binds the quote, allocates the deposit address, opens the checkout.
const session = await stridge.onramp.createSession({
  owner,
  fiat_currency: "USD",
  fiat_amount: "100",
  destination_asset_symbol: "USDC",
  destination_network_id: "60",
  destination_address: destinationAddress,
  // Optional: bounce the checkout tab back to this page for resume (referrer-whitelisted).
  return_app_url: window.location.href,
})
// → open session.checkout_url in a new tab

// 4. Poll — until the session state is terminal.
const latest = await stridge.onramp.getSession(session.session_id, { ifNoneMatch: session.etag })
if (isOnrampTerminalState(latest.state)) { /* completed / expired / failed */ }

The session response is flow-nested — flow.fiat (what the user pays), flow.entry (the crypto the provider deposits, and the deposit address), and flow.destination (what the user receives, and where). The two-leg model lives here: flow.entry.settlement_state (pending | succeeded | failed) is the delivery-leg outcome that makes a session a self-sufficient tracking resource — SESSION_COMPLETED only means the provider deposited, while settlement_state: "succeeded" (with flow.destination.tx_hash as the receipt) is the real success. 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.session_id, { state: "SESSION_COMPLETED" })
// or drive a failure:
await stridge.onramp.setSessionState(session.session_id, {
  state: "SESSION_FAILED",
  failure_code: "payment_declined",
})

This lets an SDK test exercise the full create → poll → terminal path deterministically. Note the two legs: forcing SESSION_COMPLETED makes the session itself terminal, but the Kit shows its success screen only once the destination settlement lands — the Kit's mock driver auto-advances session → settling → settlement so the happy path still reaches success in a Kit test. The override is rejected in production, so it can never short-circuit a real purchase. See Onramp endpoints → setSessionState for the full contract.

Was this page helpful?