> ## Documentation Index
> Fetch the complete documentation index at: https://docs.passage.coinlist.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Ondo Swap Buy

> Buy tokenized stocks from Ondo. What the offer cannot tell the SDK, why an order takes two calls, and the lower-level namespace underneath.

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.

<Note>
  **Recommended:** render [`CheckoutContainer`](/sdk/checkout). 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.
</Note>

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:

```tsx theme={null}
const checkoutConfig = defaultCheckoutConfig({
  "ondo::swap": { symbol: (offer) => AssetSymbol(offer.asset.code) },
  "coinlist::token_sale": { render: (offer) => <MyTokenSalePage offer={offer} /> },
});
```

`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](/sdk/checkout#why-symbol-takes-the-offer) 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.

<Note>
  **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.
</Note>

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

<Warning>
  `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.
</Warning>

<Note>
  **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`.
</Note>

<AccordionGroup>
  <Accordion title="Build your own UI with hooks (L2)">
    `useOndoBuyCheckoutViewModel` wraps the whole flow - trading status, quotes, the wallet step, the approval, the review and the broadcast - and returns `{ state, onEvent }`.

    ```tsx theme={null}
    "use client";

    import { useOndoBuyCheckoutViewModel } from "@coinlist-co/react";
    import { AssetSymbol } from "@coinlist-co/react/shared";

    export function OndoCheckout({ offer, wallets }) {
      const { state, onEvent } = useOndoBuyCheckoutViewModel({
        offer,
        ondoSymbol: AssetSymbol(offer.asset.code),
        chain: "ethereum_mainnet",
        wallets,
      });

      switch (state.type) {
        // ... your markup, driven by state; send user actions to 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:

    | Hook                           | Returns                                                  | `enabled`  |
    | ------------------------------ | -------------------------------------------------------- | ---------- |
    | `useOndoTradingStatus`         | Whether the market is open, and the session's size caps. | yes        |
    | `useOndoPrice`                 | A pollable read quote for an amount.                     | yes        |
    | `useOndoSwapTransaction`       | The committed quote from `prepareSwap`, with its expiry. | yes        |
    | `useOndoAmountViewModel`       | The amount step.                                         | not needed |
    | `useOndoReviewViewModel`       | The review and confirm step.                             | not needed |
    | `useOndoWalletSelectViewModel` | The wallet step.                                         | not needed |
    | `useOndoSidebarViewModel`      | The order summary sidebar.                               | not needed |

    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](/sdk/structure#hooks) for the conventions they share.
  </Accordion>

  <Accordion title="Drive it yourself with the client (L1)">
    `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.

    | Method                         | Kind        | Notes                                                                     |
    | ------------------------------ | ----------- | ------------------------------------------------------------------------- |
    | `getTradingStatus(params)`     | read        | Free to poll. Returns `tradable` with size caps, or `not-tradable`.       |
    | `getQuote(params)`             | read        | Free to poll. Returns a price for an amount. No fee applied or disclosed. |
    | `buildSwapTransaction(params)` | write       | Spends an attestation. Returns calldata plus `expiresAt`.                 |
    | `prepareSwap(params)`          | client only | Approves, then builds. Step-tagged result.                                |
    | `executeSwap(params)`          | client only | Broadcasts and confirms. Step-tagged result.                              |

    On the server, `coinlist.ondo` exposes the three reads only: a backend has no wallet to sign with.

    ```ts theme={null}
    const prepared = await coinlist.ondo.prepareSwap({
      wallet,                                   // your EvmWallet
      symbol: AssetSymbol("AAPLon"),            // Ondo's API symbol
      chain: "ethereum_mainnet",
      tokenAddress: USDC_ADDRESS,               // the ERC-20 being spent
      amount: parsed.amount,                    // BlockchainAmount, base units
      onProgress: (phase) => setBusy(phase),
    });

    if (prepared.type === "error") {
      // prepared.error.step: "unsupported-chain" | "allowance-check"
      //   | "approval" | "approval-reverted" | "insufficient-allowance"
      //   | "build-transaction"
      return;
    }

    const filled = await coinlist.ondo.executeSwap({
      wallet,
      transaction: prepared.transaction,
      chain: "ethereum_mainnet",
      onProgress: (phase) => setBusy(phase),
    });

    if (filled.type === "success") {
      // filled.txHash, filled.transaction
    } else {
      // filled.error.step: "quote-expired" | "spender-mismatch"
      //   | "swap" | "swap-reverted"
    }
    ```

    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-allowance` → `confirming-allowance-reset`, only for stale non-zero allowances)* → **`approving`** *(wallet popup)* → `confirming-approval` → `building-transaction`

    **`executeSwap` phases**: **`broadcasting-swap`** *(wallet popup)* → `confirming-swap`

    Two 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.
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Building the Checkout flow" icon="cart-shopping" href="/sdk/checkout">
    The recommended path: one container for every provider.
  </Card>

  <Card title="Errors and edge cases" icon="triangle-exclamation" href="/sdk/errors">
    Every error shape the flows return, and how to handle it.
  </Card>
</CardGroup>
