Skip to main content
Superstate supplies tokenized equities. A Superstate offer arrives with type: "superstate::swap" and settles directly onchain: the user pays with a stablecoin and receives the tokenized equity, an ERC-20, in the same flow.
Recommended: render CheckoutContainer. It detects superstate::swap and renders this entire flow - wallet step, amount, review, confirmation - for you. This page covers what is specific to Superstate, and the rungs below the component.
passage.superstate was the swap namespace before v0.11.0, and executeSwap is now execute. It is named after the provider now, because Ondo ships its own swap and swap could only ever mean one of them. See the changelog for the migration table.

What Superstate needs from you

Nothing. The swap contract is looked up from the chain, and the funding assets and issuer are the provider’s own constants. Every CheckoutConfig key is optional, and so is config itself. The "superstate::swap" entry takes one optional listener, onOrderConfirmed, which fires once the swap has mined. The confirmation dialog shows either way:
The SDK ships the Superstate swap contract for Ethereum Sepolia only today. On a chain with no contract, the checkout renders its flow-level error instead of quoting. Three things distinguish Superstate from the other providers:
  • The wallet must be allowlisted. Superstate only settles to addresses the issuer has approved. The user signs an ownership challenge and the SDK allowlists the address on the swap contract, once per wallet per offer. The same allowlists are what make permissioned DeFi pools and RWA lending markets reachable. That is also why the Superstate checkout never skips its wallet step: a wallets.preselected wallet opens the step with its row already selected, rather than skipping it (see Wallets).
  • The fee is charged on top. The wallet is debited inputTokenAmount + fee, so always show that sum as the total cost. (Ondo takes its fee off the deposit instead.)
  • Quotes are read quotes, polled every 15 seconds. Nothing is committed until the swap itself, and slippage protection guards the on-chain minimum output. There is no expiring committed quote to race, as there is with Ondo.

Prerequisites

  • Completed OAuth with a working PassageProvider
  • A swap-enabled offer. CoinList provides the offer ID during onboarding - keep it in a constant (SWAP_OFFER_ID).
    Offers expose a type field, so you can detect Superstate swap offers from fetch offers data via offer.type === "superstate::swap" instead of hard-coding IDs. Ondo swap offers are 'ondo::swap' and take a different flow.
  • A wallet integration. The partner demo uses wagmi + Reown AppKit; any stack that can sign messages and send transactions works. See Wallets.
useSuperstateSwapCheckoutViewModel wraps the whole flow and returns { state, onEvent }. Call it behind your own markup when you want the SDK’s logic, state and error handling but not its UI.
state is either { type: "error", reason }, when the swap could not be loaded at all (no contract on the chain, or the output token read failed), or { type: "content" } with one slice per region: wallet, amount, review, sidebar, and orderConfirmed. Each slice is a discriminated union of its own. switch on each exhaustively and assign the default to const exhaustive: never, so a new arm is a compile error rather than a blank screen.The data hooks it is built from are exported too, so you can assemble your own viewmodel instead:Each returns its result as a state union with DISABLED, LOADING, ERROR (with a reason of "not-authenticated" or "generic-error"), and CONTENT arms, and each takes a required enabled flag. Switched off with no answer in hand, a hook reports DISABLED. useSwapQuote keeps a quote it already has; useErc20TokenBalances drops its balances and reports DISABLED whenever it is switched off or has no address. Like all React hooks they must be called unconditionally. See SDK structure for the conventions they share.The step viewmodels (useSuperstateAmountViewModel, useSuperstateWalletViewModel, useSuperstateReviewViewModel, useSuperstateSidebarViewModel) and the SlippagePicker component are no longer exported since v0.13.0. Use the god viewmodel above, or build the steps yourself from the data hooks and the L1 namespace below.
passage.superstate is a plain namespace with no React. The walkthrough below builds the flow from scratch and mirrors partner-demo’s useSwapViewModel.tsx, trimmed to the minimum that still compiles and runs.

Flow at a glance

  1. connect_wallet - your wallet UI; the SDK only needs the connected account.
  2. authorizing - passage.superstate.authorizeWallet proves the user owns the wallet (a message signature) and allow-lists it for the offer, broadcasting an allow-list transaction if the contract requires one. One-time per wallet per offer.
  3. quoting - useSwapOutputToken + useSwapQuote show a live quote (input, fee, output), refreshed every 15s.
  4. swapping - passage.superstate.execute runs the ERC-20 approval and then the swap, reporting progress phases.
  5. success / error - show the confirmed output amount and an explorer link, or a readable error.
The flow involves two identities: the CoinList session (from OAuth) identifies who is buying, and the external wallet is where the shares are delivered. authorizeWallet is what links them - which is why the user signs a message before ever swapping.

Step 1: Pin the swap parameters

Keep the offer, chain, contract, and input token in one place. superstateSwapContractAddress(chain) returns the swap contract the SDK knows for a chain, or null where none is deployed. TOKEN_REGISTRY knows the ERC-20 metadata and per-chain address for USDC and USDT.
constants.ts
This page runs on Sepolia throughout. The SDK knows the Sepolia swap contract only, so superstateSwapContractAddress("ethereum_mainnet") returns null today. CoinList provides the production mainnet address when swaps go live. To move to mainnet, set SWAP_CHAIN to "ethereum_mainnet" and point SWAP_CONTRACT_ADDRESS at that address - TOKEN_REGISTRY already knows the mainnet USDC/USDT addresses.

Step 2: Bring your wallet

The swap flows never import wagmi. They drive any wallet through the EvmWallet interface, which you implement once for every flow in the SDK.

Wallets: the EvmWallet seam

The five methods, the wagmi + AppKit adapter, and how wallet errors are classified.

Step 3: Allowlist the wallet

Superstate only settles to wallets the issuer has allowlisted. passage.superstate.authorizeWallet does both halves in one call: it proves the user controls the wallet by having them sign a challenge, then registers the address against the offer, broadcasting an allowlist transaction if the contract requires one.
It is idempotent: if the wallet is already authorized it returns success immediately without prompting, so it is safe to call at the start of every session. Failures come back as { type: "error" } rather than throwing.

Parameters

Progress phases

Emitted in order. Two of them open a wallet popup:checking-authorization → requesting-challenge → signing-message (wallet popup) → submitting-signature → broadcasting-transaction (wallet popup, only when the contract needs an allowlist transaction) → awaiting-confirmation → verifying-authorization

Error steps

WalletAuthorizationError is tagged by the step that failed, so you can tell a user rejection apart from an infrastructure problem.An allowlist transaction that reverts is not an error of its own: the flow goes on to the on-chain re-check, and reports not-authorized only if the contract still says no.authorizeWallet reads authorized(address) on the swap contract over JSON-RPC (through CoinList’s authenticated RPC proxy unless you set Config.rpc), and calls two Frontline endpoints. The first is also passage.wallets.createOwnershipChallenge, which the requirements checklist uses to bind a wallet to an offer option. For non-SDK integrations, Frontline also serves the authorization read as GET /v1/wallet/authorized.

Step 4: Show a live quote

Read the output token to learn its decimals, parse the user’s input, then poll a quote. useSwapQuote waits while outputTokenDecimals is null; gate it on a valid amount yourself, through enabled.
quoteState is DISABLED while switched off with no quote in hand, LOADING until the first quote lands (or while the output token’s decimals are unknown), ERROR with a reason when no quote could be read at all, and CONTENT with quote and isRefreshing otherwise.A SwapQuote has inputTokenAmount, fee, and outputTokenAmount, all BlockchainAmounts. Render them with formatAmount(amount, LOCALE), and derive the total and the slippage-protected minimum:
The fee is charged on top of the amount that buys shares. The wallet is debited inputTokenAmount + fee, so always show that sum as the total cost.
The quote refreshes every 15s; pass pollIntervalMs to change that. isRefreshing flags a newer quote on the way. A failed poll behind a quote in hand keeps the last quote, stays CONTENT, and retries on the next tick, so ERROR means the hook never got a quote. A quote also stays in CONTENT after you switch the hook off, and after the amount changes until the new quote lands, which is why the sample checks it against the amount before using it, the way the SDK’s own review step does.

Step 5: Execute the swap

passage.superstate.execute runs the whole on-chain sequence and returns a discriminated result. It never throws.
Two of those steps need care before you offer a retry:
  • swap-unconfirmed: the wallet sent the swap, but waiting for its receipt failed. It may still mine, so swapping again can fill the order twice. It carries the txHash to look up first, and a cause of type WalletError. The SDK’s own review step disables its button until the user leaves the step, with “Your swap was sent but has not confirmed yet. Check your wallet before placing another.”
  • swap means the wallet never sent the swap (cause says why), so swapping again is safe.
swap-paused means the contract has suspended swaps and may resume, so “try again shortly” fits. swap-stopped is permanent.
Read outputAmountConfirmed before you show outputAmount to a user. It is true when the amount is decoded from the on-chain Swapped event, and false when that event would not decode and the quote’s estimate stands in. The swap mined either way, so the order is settled - but an estimate labeled as a confirmed fill is the one way this flow can misreport what happened. pricePerShare derives from the same number and carries the same caveat.
Phases (wallet popups marked):checking-status → checking-allowance → (resetting-allowance → confirming-allowance-reset, only for stale non-zero allowances) → approving (wallet popup) → confirming-approval → swapping (wallet popup) → confirming-swapWhen the standing allowance already covers input + fee, the approval phases are skipped.
The user signs up to two transactions - an ERC-20 approve for input + fee when the allowance does not already cover it, then the swap itself. USDT-style tokens with a stale non-zero allowance need one extra reset transaction first. All read-only checks run before the first popup, so the user never signs a transaction that would revert.
slippageBps sets the on-chain minimum-output guard. DEFAULT_SLIPPAGE_BPS is 0.5% and is fine for a minimal integration; see Going further for a picker.

Putting it together

A complete, minimal swap component. It compiles against the current SDK release and reuses constants.ts (Step 1) and buildEvmWallet (Step 2).
MinimalSwap.tsx
The production-grade version - with balances, a slippage picker, per-step error copy, and a review screen - lives in useSwapViewModel.tsx in the partner demo.

Going further

  • Balances and a Max button - useErc20TokenBalances({ address, chain, assets: [USDC_SYMBOL], enabled }) returns { state }. Its CONTENT arm carries balances, a Map of raw per-asset balances (null for a read that failed), so you can show the user’s USDC and prefill the max amount. Balances refresh every 15s. It was useSwapTokenBalances before v0.11.0.
  • A slippage picker - SLIPPAGE_OPTIONS_BPS (0.25%-5%) with formatBpsAsPercent(bps, LOCALE) for labels; pass the selected Bps to passage.superstate.execute.
  • A quote without React - passage.superstate.getQuote({ contractAddress, chain, inputToken, amount, outputTokenDecimals }) returns the same SwapQuote the hook polls, and passage.superstate.getOutputToken({ contractAddress, chain }) the output token.
  • More formatting helpers in @passage-cl/react/universal: shortenAddress, formattedPricePerShare.

Common questions

The flow has two identities: the OAuth CoinList session is who is buying, and the external wallet is where shares are delivered. authorizeWallet links them - the user signs an ownership challenge, and the SDK allow-lists that address on the swap contract. It’s one-time per wallet per offer, and returns success immediately on later calls if the wallet is already authorized.
A swap needs an ERC-20 approve (for input + fee) before the swap itself, so the contract can pull the stablecoin. USDT-style tokens that already have a stale non-zero allowance need one extra reset-to-zero transaction first. Every read-only check (status, allowance) runs before the first popup, so the user never signs a transaction that would revert.
useSwapQuote polls every 15s. On execution, passage.superstate.execute passes a slippage-derived minimum output to the contract, so if the price moves beyond your slippageBps the swap reverts rather than over-charging. The result’s outputAmount is decoded from the on-chain Swapped event when the receipt yields one, and falls back to the quote’s estimate when it does not - outputAmountConfirmed tells you which one you got.
No. Anything implementing the five-method EvmWallet interface works - viem directly, Privy, ethers with a thin adapter. The SDK only calls signMessage, writeContract, broadcastRawTx, and awaitTx; it never sees your wallet library.
This page uses Sepolia everywhere, and the SDK knows the Sepolia swap contract only. CoinList provides the production swap contract address and swap offer ID during onboarding. Set SWAP_CHAIN to "ethereum_mainnet", point SWAP_CONTRACT_ADDRESS at the mainnet contract, and the wallet adapter’s CHAINS map handles the network switch. CheckoutContainer runs on mainnet once the SDK ships that contract.

Next step

See the full swap implementation

The partner demo’s swap feature adds balances, slippage selection, a review screen, and typed error handling on top of this flow.