coinlist.swap namespace is headless - it drives quoting, wallet authorization, and swap execution, but you bring the wallet by implementing a small EvmWallet interface. This page mirrors partner-demo’s useSwapViewModel.tsx, trimmed to the minimum that still compiles and runs.
Prerequisites
- Completed OAuth with a working
CoinListProvider - A swap-enabled offer. CoinList provides the offer ID during onboarding - keep it in a constant (
SWAP_OFFER_ID).Offers expose atypefield ("sale"or"swap"), so you can detect swap offers from fetch offers data viaoffer.type === "swap"instead of hard-coding IDs. - A wallet integration. The partner demo uses wagmi + Reown AppKit; any stack that can sign messages and send transactions works (see Step 2).
Flow at a glance
- connect_wallet - your wallet UI; the SDK only needs the connected account.
- authorizing -
coinlist.swap.authorizeWalletproves 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. - quoting -
useSwapOutputToken+useSwapQuoteshow a live quote (input, fee, output), refreshed every 15s. - swapping -
coinlist.swap.executeSwapruns the ERC-20 approval and then the swap, reporting progress phases. - 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.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 ships
SUPERSTATE_SWAP_CONTRACT_ADDRESS_SEPOLIA; CoinList provides the production mainnet address when swaps go live. To move to mainnet, set SWAP_CHAIN to "ethereum_mainnet" and swap the contract constant - TOKEN_REGISTRY already knows the mainnet USDC/USDT addresses.Step 2: Adapt your wallet to EvmWallet
The swap flows never import wagmi. They drive any wallet through a five-method EvmWallet interface: address, signMessage, writeContract, broadcastRawTx, and awaitTx. Here’s the adapter for wagmi + AppKit:
wagmiEvmWallet.ts
Let your wallet library’s errors propagate as-is. The SDK catches and classifies them into a typed
WalletError (user_rejected, insufficient_funds, contract_reverted, timeout, …), so you never parse wallet errors yourself. See wagmiEvmWallet.ts in the partner demo.Step 3: Authorize the wallet
Build the wallet from your connected account, then callauthorizeWallet. It proves ownership with a signed message and allow-lists the address on the swap contract.
authorizeWallet reports these phases in order - two of them open a wallet popup:
checking-authorization → requesting-challenge → signing-message (wallet popup) → submitting-signature → broadcasting-transaction (wallet popup, only if the contract needs an allow-list tx) → awaiting-confirmation → verifying-authorization
It is idempotent: if the wallet is already authorized it returns success immediately without prompting, so you can safely call it every session. Flow-level failures come back as { type: "error" } rather than throwing.
Step 4: Show a live quote
Read the output token to learn its decimals, parse the user’s input, then poll a quote.useSwapQuote skips quoting until it has both the output-token decimals and a valid amount.
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.SWAP_POLL_INTERVAL_MS). isRefreshing flags a background refresh; a failed poll keeps the last quote and retries on the next tick.
Step 5: Execute the swap
executeSwap runs the whole on-chain sequence and returns a discriminated result.
checking-status → checking-allowance → (resetting-allowance → confirming-allowance-reset, only for stale non-zero allowances) → approving (wallet popup) → confirming-approval → swapping (wallet popup) → confirming-swap
The user signs up to two transactions - an ERC-20
approve for input + fee, 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@coinlist-co/react@0.9.0 and reuses constants.ts (Step 1) and buildEvmWallet (Step 2).
MinimalSwap.tsx
useSwapViewModel.tsx in the partner demo.
Going further
- Balances and a Max button -
useSwapTokenBalances({ address, chain, assets: [USDC_SYMBOL], enabled })returns aMapof per-asset balances so you can show the user’s USDC and prefill the max amount. - A slippage picker -
SLIPPAGE_OPTIONS_BPS(0.25%–5%) withformatBpsAsPercent(bps, LOCALE)for labels; pass the selectedBpstoexecuteSwap. - More formatting helpers in
@coinlist-co/react/shared:shortenAddress,formattedPricePerShare.
Common questions
Why does the user sign a message before swapping?
Why does the user sign a message before swapping?
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.Why does my wallet ask for two transactions?
Why does my wallet ask for two transactions?
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.How fresh is the quote, and what if the price moves?
How fresh is the quote, and what if the price moves?
useSwapQuote polls every 15s. On execution, executeSwap 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 confirmed outputAmount in the result is decoded from the on-chain Swapped event, not the quote estimate.Do I need wagmi / Reown AppKit specifically?
Do I need wagmi / Reown AppKit specifically?
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.How do I go to mainnet?
How do I go to mainnet?
This page uses Sepolia everywhere. 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.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.