Skip to main content
Ondo supplies tokenized stocks - AAPLon, TSLAon and the rest. An Ondo offer arrives with type: "ondo::swap", and the buyer spends a stablecoin to receive the tokenized asset.
Recommended: render CheckoutContainer. It detects ondo::swap and renders this entire flow for you. This page covers what is specific to Ondo, and the rungs below the component.
Ondo has two products on the same ondo::swap sale type: buy (invest in tokenized stocks) and sell (liquidate holdings). Only buy ships today; sell will get its own page.

What Ondo needs from you

One config entry, and only one field of it is required:
symbol is Ondo’s API symbol, not the on-chain symbol() and not necessarily offer.asset.code. Ondo’s symbol tracks the underlying ticker and changes on a rebrand, and the two already disagree on Sepolia, where a mock asset stands in. Return AssetSymbol(offer.asset.code) if your catalogue agrees; map the exceptions if it does not. It must also be total - see the warning on the checkout page for why it runs for offers Ondo has nothing to do with. The config also accepts an optional onOrderConfirmed(order), fired once a swap has mined. The confirmation dialog shows either way; add it only if you want to navigate, refresh a portfolio, or log.
Quotes are priced against Ondo production whatever chain you pass, because Ondo runs no sandbox. On a testnet the price is real and the money is not.

Placing an order takes two calls

This is the one structural thing to know about Ondo. An order is prepareSwap then executeSwap, not one call, and the split is where the buyer’s decisions are:
  1. prepareSwap approves the swap contract to spend the amount, then builds the transaction that spends it. The result is a committed quote: firm calldata with an expiresAt, valid for about a minute.
  2. The buyer reviews firm numbers - what they pay, the fee, what they receive.
  3. executeSwap broadcasts that calldata verbatim and waits for it to mine.
Bundling them behind one button would burn most of the transaction’s ~60-second life on the approval and hand the buyer an expired quote. The approval deliberately comes first: building first would spend an attestation on calldata that had already expired by the time the buyer could act on it.
buildSwapTransaction spends an attestation on every call. The two reads - getTradingStatus and getQuote - are free to poll while the user edits an order. Budget one build per order placed, plus one per refresh the user asks for.
No CoinList fee is applied to a read quote, and no read discloses one. Ondo prices exactly the amount passed. A built transaction carries the fee explicitly, taken off the deposit rather than added on top, so the approval never has to cover more than payInputAmount.
useOndoBuyCheckoutViewModel wraps the whole flow - trading status, quotes, the wallet step, the approval, the review and the broadcast - and returns { state, onEvent }.
state is a discriminated union - switch on it exhaustively and assign the default to const exhaustive: never = state so a new step is a compile error rather than a blank screen.The smaller hooks it is composed from are exported too, so you can assemble your own viewmodel instead:The three that fetch or poll take enabled; the four viewmodels gate on their step input instead. All of them must be called unconditionally. See SDK structure for the conventions they share.
coinlist.ondo is a plain namespace with no React. Three methods are backed by the API; two more need a wallet and so exist only on the browser client.On the server, coinlist.ondo exposes the three reads only: a backend has no wallet to sign with.
Both flows are total: every failure comes back step-tagged rather than thrown, so you map each one to your own copy.prepareSwap phases, in order. An allowance that already covers the order skips the approval phases:checking-allowance(resetting-allowanceconfirming-allowance-reset, only for stale non-zero allowances)approving (wallet popup)confirming-approvalbuilding-transactionexecuteSwap phases: broadcasting-swap (wallet popup)confirming-swapTwo error steps are worth calling out:
  • insufficient-allowance is separated from the generic build failure because it has a remedy. Reaching it means the approval that just mined is not the one the backend sees, usually an RPC node a block behind. That is a retry, not a re-approval.
  • quote-expired and spender-mismatch are refusals to broadcast, not failed broadcasts. Both would revert on-chain and cost the buyer gas, and both are fixed by building a fresh transaction.
executeSwap broadcasts the calldata verbatim. Never re-encode it: the contract verifies a signature over the exact arguments inside.

Next steps

Building the Checkout flow

The recommended path: one container for every provider.

Errors and edge cases

Every error shape the flows return, and how to handle it.