type: "coinlist::token_sale", and the user commits a stablecoin to buy into it. The participation settles later on CoinList’s own schedule, so nothing leaves the wallet during the flow: the user signs an ERC-20 approve and that is all.
Recommended: render
CheckoutContainer. It routes on offer.type, so your integration handles every provider without knowing which one an offer belongs to.What the token sale needs from you
Unlike Superstate and Ondo, the SDK ships no token-sale UI yet, soCheckoutContainer hands this branch back to you through a required render slot. An L3 token-sale component is planned; until it lands, render is where your own checkout goes:
ReactNode because the container knows which offer it is being asked about, and a host with a working token-sale page of its own needs that offer to render it. A host that never lists token-sale offers returns null.
The rest of this page is what goes inside that slot.
Prerequisites
- Completed OAuth with a working
CoinListProvider - An
OfferDetailand theOfferOptionthe user picked (from Display offer details) - The funding contract address for the offer. CoinList provides it during onboarding - keep it in a constant.
- A wallet integration exposed as an
EvmWallet. The partner demo uses wagmi + Reown AppKit; any stack that can send transactions works. The adapter is the same one every flow uses - see Wallets.
Build your own UI with hooks (L2)
Build your own UI with hooks (L2)
The token sale is one of the two features that stop at L2: it has a namespace and data hooks, but no components.
useParticipations is the hook it exposes - see Track participations below, which is the same surface used for reading rather than buying.For the buy itself, drive coinlist.tokenSale.execute from your own component state. It is a single call that returns a step-tagged result, so there is little for a viewmodel to hold beyond the amount and the current phase. The L1 walkthrough below shows exactly that.Drive it yourself with the client (L1)
Drive it yourself with the client (L1)
coinlist.tokenSale runs the whole flow end-to-end: it reads the allowance, submits the ERC-20 approve, and records the participation, so you do not hand-roll the on-chain plumbing.Flow at a glance
- connect_wallet - your wallet UI; the SDK only needs the connected account.
- enter_amount - the user picks a funding asset (
offerDetail.fundingAssets) and enters an amount. - investing -
coinlist.tokenSale.executereads the allowance, runs the ERC-20 approval, and records the participation, reporting progress phases. - success / error - show the recorded participation, or a readable error.
Unlike a Superstate swap, a token sale needs no wallet allowlisting step - there’s no ownership challenge to sign. The user only signs the ERC-20
approve. The funding move settles later on Passage’s contracts, so nothing leaves the wallet during this flow.Step 1: Pin the sale parameters
Keep the chain, funding contract, and payment 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. To move to mainnet, set
SALE_CHAIN to "ethereum_mainnet" and point FUNDING_CONTRACT_ADDRESS at the mainnet contract CoinList gives you - TOKEN_REGISTRY already knows the mainnet USDC/USDT addresses.Step 2: Bring your wallet
The token-sale flow drives any wallet through the sameEvmWallet interface as every other flow. It only ever calls address, writeContract, and awaitTx - there is no message to sign - so an adapter you already wrote works unchanged.Wallets: the EvmWallet seam
The five methods, the wagmi + AppKit adapter, and how wallet errors are classified.
Step 3: Execute the sale
coinlist.tokenSale.execute runs the whole on-chain sequence and returns a discriminated result. The amount is a BlockchainAmount in the payment token’s decimals - parse the user’s input with parseBlockchainAmount.checking-allowance → (resetting-allowance → confirming-allowance-reset, only for stale non-zero allowances) → approving (wallet popup) → confirming-approval → recording-participationAn
approve is submitted on every participation, even when the existing allowance already covers the amount: the backend requires a fresh approval transaction hash and verifies it on-chain before recording. USDT-style tokens with a stale non-zero allowance need one extra reset-to-zero transaction first, reported as the distinct allowance-reset phase.error.step === "participation"), the error carries the approvalTxHash so you can retry the record without asking the user to approve again.Putting it together
A complete, minimal invest component. It reusesconstants.ts (Step 1) and the buildEvmWallet adapter from Wallets.MinimalInvest.tsx
Going further
- Show the user’s balance -
coinlist.erc20.getBalance({ tokenAddress, owner, chain })reads the connected wallet’s balance for a payment token, so you can validate the amount or render a Max button before investing.useErc20TokenBalances({ address, chain, assets, enabled })is the polling hook form. - Lower-level control - if you drive the ERC-20
approveyourself (a custom wallet, batching, a different confirmation UX), skipcoinlist.tokenSale.executeand record the participation directly withcoinlist.tokenSale.createParticipation, passing theapprovalTransactionHashyou obtained.executeis the batteries-included version of exactly that sequence.
Track participations
A participation is the record CoinList keeps of a user taking part in a token sale. It moves through its statuses asynchronously - the SDK does not push, so you re-fetch on session refresh or after a user action.Recommended: useParticipations
offerId is optional: omit it to list every participation for the signed-in user. Pass data to seed it with participations pre-fetched via CoinListServer#tokenSale.list() and skip the client-side fetch. See SDK structure.
Reading and writing imperatively
coinlist.tokenSale is available on both the browser client and CoinListServer, except execute, which needs a wallet.
amount is a decimal token amount ("100" for 100 USDC), not raw base units. The SDK maps the camelCase keys to the API’s snake_case fields; see the API reference for the raw request and response schema.NotAuthenticatedError otherwise.
Common questions
Why an approve instead of a transfer?
Why an approve instead of a transfer?
Passage settles the funding move on its own contracts, on its own schedule, after on-chain verification. The user only signs an allowance up to the amount they want to invest; nothing leaves their wallet until the Passage backend later pulls it. This pattern is cancellable (the user can revoke the allowance) and lets Passage batch transfers.
Why is an approval always submitted?
Why is an approval always submitted?
The backend hard-requires an approval transaction hash on every participation and verifies that exact transaction on-chain (sender, token, spender, and approved amount) before confirming. A participation recorded without a fresh approval always fails, so
coinlist.tokenSale.execute submits one even when the standing allowance already covers the amount - the allowance read only decides whether a USDT-style reset-to-zero is needed first.Do I need wagmi / Reown AppKit specifically?
Do I need wagmi / Reown AppKit specifically?
No. Anything implementing the
EvmWallet interface works - viem directly, Privy, ethers with a thin adapter. The token-sale flow only calls address, writeContract, and awaitTx; it never sees your wallet library. It’s the same EvmWallet every other flow uses.What if recording fails after the approval mined?
What if recording fails after the approval mined?
The error comes back as
{ step: "participation", approvalTxHash }. The approval is already on-chain, so retry coinlist.tokenSale.createParticipation with that hash rather than re-running the whole flow - the user doesn’t need to approve again.What about chains other than Ethereum?
What about chains other than Ethereum?
The flow generalises: the same
EvmWallet adapter maps each EthereumChain to a network, and TOKEN_REGISTRY knows the payment-token addresses per chain. Your account manager will confirm which chains and assets are enabled for your offers.Next step
Building the Checkout flow
The recommended path: one container that renders whichever provider an offer belongs to.