This changelog covers the
@passage-cl/react SDK. Up to v0.12.1 it was published as @coinlist-co/react.November 06, 2026 - Wallet sign-in for users with no CoinList account, AuthState becomes an object, and access tokens carry signInMethod
A user with no CoinList account can now sign in with their wallet, then buy and sell Ondo and buy, sell, and claim Centrifuge. Every host has to migrate
Upgrading signs no one out: at runtime, a token without the field reads as a CoinList login and, if you set a
An app-level token has no user behind it, so it has no
getState() checks and any code that builds an access token, even if it never turns wallet sign-in on.This is a 0.x minor with breaking changes.
useCheckoutSessionViewModel and CheckoutSessionUiState are @alpha, outside the compatibility contract. There are no new peer dependencies.Breaking Changes
AuthState is an object
passage.auth.getState() and passage.init() return an object, and a logged-in state says how the user signed in.Migration: compare
type.OAuthAccessToken requires signInMethod
signInMethod is { method: "coinlist" } for a CoinList login, or { method: "wallet", wallet: { address, chain } } for a wallet sign-in. A getAccessToken or getSession that builds its own token stops compiling, even on a host with only CoinList logins.Migration: update all four places. Reads fail to compile, but writes do not, because they copy fields by hand. A host that fixes only the reads with a fallback compiles cleanly and turns every wallet session into a CoinList one.logger, logs a warning. See OAuth authentication.ClientCredentialsOAuth is no longer an OAuthAccessToken
An app-level token has no user behind it, so it has no signInMethod, and it no longer assigns to OAuthAccessToken (TS2741). The ClientCredentialsOAuth constructor takes value and expiresAt only.Migration: type the variable as ClientCredentialsOAuth, and keep app tokens out of anything typed OAuthAccessToken.Added
Wallet sign-in
A wallet session is for one address on one network, for a user with no CoinList account. Wallet sign-in needs React for now, because onlyPassageProvider can turn it on. Sign in with wallet has the full setup.On your server:In the browser:
walletSignInonPassageProvidertakes{ challenge, complete, signOut, connection }: three calls to your backend, and your app’sWalletConnection(connecting,disconnected, orconnectedwith an address and chain).useWalletSignIn()returns{ state, signIn }.signIn(wallet)resolves to aWalletSignInResult:signed-in,canceled, orfailedwith thestepthat failed (challenge,signature, orcomplete). A call while one is pending starts nothing and resolves with the pending one’s result.passage.auth.signInWithWallet({ wallet })is the same sign-in without the hook. Both throwWalletSignInConfigRequiredErrorwithout thewalletSignInprop.usePassage().authStateispassage.auth.getState()as React state, so a component re-renders on a sign-in or sign-out.
walletSignIn prop, wallet sign-in is off and the SDK does not follow your wallet connection.What a wallet session can call
The right-hand column throws
NotAuthenticatedError on a wallet session, except wallets.connect, which resolves with the error code not_authenticated. A wallet session builds only for its own wallet and chain, and CoinList answers 403 to any other. On a server, offers.* called with an app-level token still works during a wallet session, and sends the app token.Checkout on a wallet session
CheckoutContainer handles this for Ondo and Centrifuge, with no new props. A user signed in with their wallet sees:A Centrifuge sell names no option, so it runs no requirements check. These cards have no button, so render your own sign-in or connect action near the checkout. Superstate and the token sale do not run this check, so don’t render them on a wallet session: it can’t allowlist a Superstate wallet or record a token sale participation.On a wallet session,
ClaimContainer’s Centrifuge rows read and claim only the session’s wallet. Its rows on other networks still show, but fail to claim. The footer still shows whichever wallet is connected.Other additions
signInMethodFor(requirements)from/universalreturns"coinlist"for any requirement and"wallet"for none, so a server can choose the page’s sign-in button before any user is known.useOfferRequirements({ offerId, data?, enabled? })reads an offer’s requirements without statuses, so a wallet session can read them too.useCheckoutSessionViewModel(@alpha) gives a checkout with its own UI the same session handling asCheckoutContainer.OndoSwapRefusaland abuild-refusedarm onOndoSwapPreparationError, for an order CoinList refuses because its requirements are not met. An exhaustiveswitchon the error gets a compile error until it handles the new arm.SignInMethod,SessionWallet,SiweMessage,WalletSignInChallenge,WalletSignInChallengeParams, andWalletSignInGrantare new types from/universal.
Changed
These apply to every host, with or without wallet sign-in.- Ondo and Centrifuge checkouts show a skeleton until
PassageProvideris ready. - An Ondo
requirements_not_metnow says “Complete the requirements for this offer before placing an order.” instead of asking the user to try again. - A wallet that rejects with a plain 4001 now reads as canceled rather than failed.
November 02, 2026 - Checkouts require optionId, ClaimContainer shows several offers in one card, and Centrifuge offers carry their underlying fund
Every checkout now needs the option the user is buying, so a second option on an offer can never break orders.
See Checkout.
The
The
Centrifuge vault offers carry
ClaimContainer takes a list of offers and shows them in one “Your orders” card, and Centrifuge vault offers name the fund they invest in.This is a 0.x minor with breaking changes. Required
optionId and the new useCoinListTokenSaleParticipations argument touch the @public surface. The claim card, its viewmodels and Centrifuge are @alpha, outside the compatibility contract.Breaking Changes
optionId is required on every checkout
CheckoutContainer and the checkout viewmodels require optionId. The SDK no longer picks an option for you.It is required for every offer type, even where it is not sent: the Superstate swap and a Centrifuge redemption (
side: "sell") still ignore it. unknown-option now means only that the offer does not list the option you named. Nothing changes on the wire.The viewmodels that now require it: useOndoBuyCheckoutViewModel, useOndoSellCheckoutViewModel, useCentrifugeBuyCheckoutViewModel and useCoinListTokenSaleCheckoutViewModel.Migration: pass the option the user picked, and put it in the key. If you want a default, choose it yourself:ClaimContainer takes offers
The offer prop is now offers: AsyncOfferDetail | AsyncOfferDetail[]. One offer still works as before.Migration: rename the prop.One claim viewmodel
useClaimViewModel (@alpha) replaces the two per-provider claim viewmodels and their types.useClaimViewModel takes the same inputs as ClaimContainer: { offers, wallets, walletLabel?, config?, enabled? }. Pass onClaimed through config["centrifuge::vault"]. There is no pollInterval option, and no data seed for token-sale participations.ClaimUiState’s CONTENT arm holds rows: ClaimRowUi[], each tagged with its offer type, plus empty: ClaimEmptyUi for the empty-state copy.Migration:useCentrifugeClaimPositions reads several offers
The @alpha option vaults: CentrifugeVault[] is now vaultsByOffer: CentrifugeVault[][]: one list per offer, each with one vault per chain. Wrap a single offer’s vaults in a list: vaultsByOffer: [vaults]. The state is ERROR when any offer has no read that answered.Added
Several offers in one claim card
ClaimContainer renders any mix of Centrifuge vault and CoinList token-sale offers in one “Your orders” card, in the order you pass them.- The card has one header, one empty state, and one wallet footer. The footer shows only when a vault offer is on the card.
- It loads until every offer has answered. If any offer fails, it shows one error, and Retry refetches every offer. An error shows even while another offer is still loading.
- Rows come in this order: claimable vault positions, pending vault positions, then token-sale participations, newest first.
- With one offer and no orders, the card shows that provider’s copy. With two or more, it shows “No orders yet.” A card of vault offers only renders nothing when it has no rows and
externalAdapterisnull. - An empty list renders nothing.
- Each provider reads all of its offers on one poll. A claim collects one row, on that row’s offer.
config and enabled apply to every offer. See Centrifuge vaults.CentrifugeVaultOfferDetail.underlyingFund
Centrifuge vault offers carry underlyingFund: string | null, the fund the share token invests in. It is null until it is recorded for the offer. The Centrifuge checkout sidebar now shows it, on buy and sell.Changed
useCoinListTokenSaleParticipationstakesoffers?: OfferId | readonly OfferId[]as its first argument (wasofferId?: OfferId). Existing calls still compile. A list is read offer by offer on one poll, and the hook settles once every offer has answered. Any failure fails the whole read. A new array with the same ids does not refetch. See Invest flow.CentrifugeVaultClaimedgainsofferId, soonClaimedtells you which offer was claimed.CentrifugeVaultLegIdnow includes the vault address, so two vaults on one chain no longer share an id. Treat it as opaque.
October 29, 2026 - Centrifuge claims wait for the host's wallets, Ondo hides its quote line while placing, and CoinList terms links
The Centrifuge claim card no longer flashes “No orders found” while the host is still restoring its wallets. The Ondo review hides its quote countdown while an order is being placed, and the Centrifuge and Ondo checkouts link to CoinList’s terms.
The Centrifuge claim card reads it (
Centrifuge is
@alpha, outside the compatibility contract. The @public surface only grows: WalletSelection gains an optional status, and OndoQuoteStatusUi gains a placing arm.Added
WalletSelection.status
WalletSelection takes an optional status: 'loading' | 'ready', which defaults to 'ready'. Pass 'loading' until every embedded wallet the user has is listed, not as soon as your wallet library says it is ready: a library can be ready before it has restored the user’s wallets.ClaimContainer, and useCentrifugeClaimPositions as the @alpha walletsStatus option):- While
'loading', before a first answer, the card shows its loading state. It sends no request and starts no poll. - It shows “No orders found for this asset.” only once every wallet it asked about has answered.
- When a wallet is added, the card shows loading in that same render instead of the earlier empty answer. Rows it already shows stay visible while the new wallet is read.
- Going from
'ready'back to'loading'keeps the orders and the connect footer the card last showed until'ready'returns.
status keeps the old behaviour: with no wallets, the card says “No orders found for this asset.” See Centrifuge vaults.Changed
- The Ondo quote line (“Quote expires in N seconds” and “Refresh quote”) is hidden while the place-order button is loading, on both buy and sell, and comes back once it stops.
OndoQuoteStatusUigains{ type: 'placing' }, whichuseOndoReviewExecutionreturns exactly while an order is being placed. An exhaustiveswitchoverOndoQuoteStatusUineeds a case for it. See Ondo swap. - Terms links in the review step now point at CoinList:
https://coinlist.co/terms/centrifugefor Centrifuge (washttps://centrifuge.io/terms) andhttps://coinlist.co/terms/ondofor Ondo (washttps://docs.ondo.finance/legal/terms-of-service).
Fixed
coinlistBaseUrlwith a trailing slash or a path.auth.startOAuth()andrequirements.handlejoincoinlistBaseUrlto the page path with exactly one slash. Before, a trailing slash gave a//in the sign-in redirect, andrequirements.handledropped any path after the origin.
October 27, 2026 - Checkouts follow a disconnected wallet, the Centrifuge deposit fee, and Offer drops category
Every checkout now notices when the host stops supplying the wallet the user confirmed, and sends them back to the wallet step. A wallet that has disconnected gets its own
Throw
WalletError arm and reconnect copy. A Centrifuge deposit builds its order on the amount step, so the review shows the fee the API charges.Centrifuge is
@alpha, outside the compatibility contract. In the @public surface, category is removed from offers and WalletError gains an arm. WalletDisconnectedError and WalletSelection.find are new.Breaking Changes
category is removed from offers
OfferDetailBase.category is gone from every OfferDetail. The API no longer sends it.Migration: delete every read of offer.category. To group offers, switch on offer.type.WalletError gains disconnected
WalletError gains { type: 'disconnected' }: the wallet is no longer connected, or no longer on the account the SDK was handed, so nothing was sent. Retrying through the same wallet fails the same way, so the user has to reconnect or choose another wallet. The SDK classifies these as disconnected:- an EIP-1193 error with code
4100anywhere in thecausechain; - wagmi’s
ConnectorNotConnectedError; - the new
WalletDisconnectedError.
switch over WalletError needs the case.Added
WalletDisconnectedError
Throw WalletDisconnectedError from your EvmWallet when the account the SDK was handed is gone, for example after the user switched accounts. It is exported from @passage-cl/react/universal.WalletSelection.find(selection, address) returns the EvmWallet a selection currently holds for an address, or null. See Wallets and Errors.Centrifuge deposit fee
CentrifugeVaultBuyTransaction gains fee, a BlockchainAmount in the funding token. It is taken out of the deposit, not added on top.useCentrifugeBuyTransaction (@alpha) builds a deposit request on its own: pass request (a BuildCentrifugeVaultBuyParams, or null), enabled and an optional data seed; it returns { state, refetch }. See Centrifuge vaults.Changed
- Checkouts follow the wallet the host supplies now. The token sale, Ondo buy and sell, Superstate and Centrifuge checkouts hold the confirmed wallet by its address and look it up in the current
walletson every render, so each step sends through the wallet you supply now. When you stop supplying that address, the checkout reopens the wallet step with “Your wallet disconnected - reconnect or choose another.” A send already in flight settles first. A checkout that skipped its wallet step for apreselectedwallet (every provider but Superstate, which keeps it) has no step to return to, so there the user sees the disconnected copy only if a send fails. The Centrifuge claim card looks up its wallet the same way. - A Centrifuge deposit is built on the amount step. Confirming the amount approves the funding token and requests the deposit (
POST /v1/centrifuge/vault/invest), so the review opens with the built request and its fee, and Place only sends it. An approval or request failure shows on the amount button. The review shows the API’s fee,0.00 USDCincluded, and a total equal to the amount. A request that expires before Place is rebuilt once, and so is one the vault refuses asrequest-expiredat Place. A failed rebuild shows an error with Retry. A redemption is unchanged. - Centrifuge buy review types (
@alpha):CentrifugeBuyCheckoutUiState.reviewis aCentrifugeBuyReviewUiState, which adds{ type: 'loading' }and{ type: 'error'; reason: CentrifugeBuyReviewErrorReason }('build-failed' | 'expired') toCentrifugeReviewUiState.CentrifugeBuyCheckoutUiEventtakesCentrifugeBuyReviewUiEvent, which adds{ type: 'ON_RETRY' }.
Fixed
- The Centrifuge deposit’s confirmation dialog shows the token’s icon. Before, it showed none.
October 26, 2026 - Orders name the offer option, Centrifuge claims go through the API, and dark-theme token logos
Ondo and Centrifuge orders now name the offer option the user chose, so the API runs that option’s requirements and the checkout shows that option’s minimum. Centrifuge claims are built by the API again, now that it serves them. The requirements checklist takes a label for its Continue button, and the checkout shows a token’s dark logo in the dark theme.Ondo orders require
Requirements checklist
Centrifuge is
@alpha, outside the compatibility contract. The required offerOptionId on Ondo orders is the one break in the @public surface.Breaking Changes
Ondo orders require offerOptionId
passage.ondo.prepareBuy, prepareSell, buildBuyTransaction and buildSellTransaction require offerOptionId. The SDK sends it to the API as offer_option_id, and the API runs that option’s requirements. An offer can have several options, each with its own requirements and minimum. Before, an order on such an offer that named no option was refused only after the user had paid gas for the approval.The request you pass to useOndoBuyTransaction and useOndoSellTransaction (BuildOndoBuyParams, BuildOndoSellParams) requires it too.The Centrifuge equivalents, passage.centrifuge.prepareBuy, buy and buildBuyTransaction, require it as well. A Centrifuge redemption sends no option id.Migration: pass the id of the option whose requirements you showed the user.Centrifuge claims are built by the API
passage.centrifuge.claim asks the API for the claim transaction, has the wallet send it, and waits for it to confirm. Before, the SDK encoded the call to the vault proxy itself. The API claims everything claimable, read on-chain when it signs, so no amount is sent.CentrifugeClaimError also gains { step: 'build-refused', reason }, where reason is a CentrifugeClaimRefusal ('vault-refused' or 'no-proxy-on-chain'), { step: 'build-transaction' } and { step: 'claim-expired' }.Migration:Added
optionId for Ondo and Centrifuge checkouts
CheckoutContainer’s optionId prop now applies to Ondo buy and sell and to a Centrifuge deposit. The checkout sends the option’s id with the order. An Ondo buy and a Centrifuge deposit check the amount against that option’s minimum; an Ondo sell has no minimum.- With no
optionId, the checkout uses the offer’s only option. - An offer with several options and no
optionId, an id the offer does not show, or an offer with no options replaces the checkout with “Option not available” (unknown-option). No request is sent. - Superstate and a Centrifuge redemption ignore it. The token sale still buys its first option when none is named.
- The checkout reads
optionIdonce, at mount. To switch options, remount it with akey.
useOndoBuyCheckoutViewModel, useOndoSellCheckoutViewModel and useCentrifugeBuyCheckoutViewModel take the same optionId option. See Checkout.Centrifuge claim transactions
It returns a
CentrifugeClaimTransaction: { tx, expiresAt, direction, claimAmount }. claimAmount is in the funding token for a deposit and in shares for a redemption.Requirements checklist continueLabel
RequirementsChecklistContainer and useRequirementsChecklistViewModel take an optional continueLabel, which defaults to "Continue". Pass continueLabel="Buy" to say where the button goes. RequirementsChecklistUiState.continueAction gains label. See Requirements.USDZ
StablecoinSymbol gains 'USDZ', a test dollar anyone can mint on Ethereum Sepolia. USDZ, USDZ_SYMBOL and TOKEN_REGISTRY.usdzAddress(chain) are new. A Centrifuge vault on ethereum_sepolia takes USDZ, and a token sale that lists USDZ can be paid in it there. Centrifuge on every other chain, and Ondo and Superstate everywhere, still take USDC. Code with an exhaustive switch over StablecoinSymbol needs the case.Dark-theme token logos
The checkout shows a token’s dark logo in the dark theme when the token registry has one.CheckoutAssetUi gains iconUrlDark.Changed
CheckoutBlockedReasonis now'unsupported-chain' | 'unknown-option' | TBlocked, soOndoCheckoutErrorReasonandCentrifugeCheckoutErrorReasongain'unknown-option'. The token sale already had it. An exhaustiveswitchover either reason needs a case for it.- The Centrifuge claim dialog shows three steps: “Preparing your claim”, “Confirm in your wallet” and “Confirming on-chain”. Wallet failures use the same messages as the checkout. Retry is offered once when the vault refuses the claim, and never for a claim that may still confirm, for nothing to claim, or for a chain with no proxy.
- The Centrifuge claim card reads “No orders found for this asset.” and “Invested with an external wallet? Connect it to view your orders.” Claimable rows explain the
~estimate in a tooltip. - A blocked checkout logs
checkout blockedonce per reason: aterrorfor a configuration problem (unsupported-chain,unknown-option,no-price,no-payable-asset), atwarnfor market state (not-tradable,not-orderable). Superstate’sload-failedscreen logs no such line. - At
debug, every transaction logs when it is sent and how it ends, and a reverted transaction is replayed to log the contract’s revert reason. Belowdebugthe replay makes no request. Thedebugline for anHttpErrorincludes the response body and request id.
Fixed
- A transaction built by the API (Ondo swaps, Centrifuge requests, the Superstate allow-list) that reverts during gas estimation is now a
contract_revertedWalletErrorwith its reason. Before, it wasunknown. - A stale Centrifuge request now reports
request-expired, as v0.14.0 described. Before, the check never matched and the checkout showed a generic error. - Retrying a failed Centrifuge claim uses the wallet the host supplies now for that address. Before, it reused the adapter from the first attempt and failed again after the wallet changed account or chain. If the address is no longer connected, Retry closes the dialog.
October 21, 2026 - The Centrifuge sell checkout, on-chain Centrifuge claims, and the coinlist_account requirement
This release completes the Centrifuge vault journey.
CheckoutContainer now mounts a redemption checkout for side="sell", and the SDK encodes claims on-chain itself instead of asking the API for calldata. The vault status reports each direction separately, and the API’s refusals arrive as typed reasons. The requirements checklist knows the new coinlist_account requirement.Centrifuge is
@alpha, outside the compatibility contract, so its breaking changes ship in a minor release. Nothing outside the Centrifuge types breaks.Breaking Changes
Centrifuge claims are encoded by the SDK
passage.centrifuge.createClaim and passage.centrifuge.createClaimProceeds are removed, with CentrifugeVaultClaimTransaction and CentrifugeVaultClaimProceedsTransaction. Their endpoints, POST /v1/centrifuge/vault/claim and /claim-proceeds, do not exist on the API. A claim is the vault proxy’s own unauthenticated call, so passage.centrifuge.claim now calls the proxy’s deposit or redeem from the wallet with the maximum amount, which claims everything claimable. No API request and no approval come first.CentrifugeClaimSettlement is { assets, shares }, read from the Deposited or Redeemed event in the receipt. It is null only when the receipt cannot be decoded.Migration:Centrifuge vault status reports each direction
CentrifugeVaultStatus is now { chain, symbol, deposit, redeem, asOf }. deposit and redeem are each a CentrifugeDirectionState: 'active', 'paused' (it can come back) or 'stopped' (permanent for that proxy on that chain).Migration:
Centrifuge checkout and claim types
Migration: a
config["centrifuge::vault"].onOrderPlaced handler now runs for both directions. Read order.side before order.transaction.Added
Centrifuge sell checkout
<CheckoutContainer side="sell"> for a centrifuge::vault offer now mounts a redemption checkout. Before, it rendered an empty container.- The steps are wallet, amount and review. A preselected wallet skips the wallet step, as on the buy side.
- The amount is a number of the vault’s share tokens, bounded by the wallet’s balance. The offer’s dollar minimum applies to deposits only.
- Placing the order approves the vault proxy for the shares when the allowance is short, builds the request and broadcasts it. A redemption pays nothing yet: the fund fulfils it later, and the user claims the USDC in
ClaimContainer. - The checkout blocks with “Redeeming is paused right now” when the vault’s
redeemdirection is paused or stopped. config["centrifuge::vault"].onOrderPlacedfires after the redemption request mines, withorder.side === "sell".- To build your own UI, use
useCentrifugeSellCheckoutViewModel(@alpha) withCentrifugeSellCheckoutUiStateandCentrifugeSellCheckoutUiEvent.
passage.centrifuge.placeRequest now broadcasts either a buy or a redeem transaction.Typed Centrifuge refusals
When the API refuses to build a Centrifuge request,prepareBuy, prepareSell, buy and sell return { step: 'build-refused', reason } instead of build-transaction. reason is a CentrifugeRequestRefusal: 'requirements-not-met', 'insufficient-balance', 'direction-paused', 'direction-stopped', 'vault-refused' or 'no-proxy-on-chain'. The checkout shows its own message for each. See Errors.ApproveThenBuildError and ApproveThenBuildResult take this reason as an optional type parameter that defaults to never, so the Ondo error types are unchanged.Claim estimates
Each claimable row and the claim dialog show a~ estimate of what the claim pays, at the vault’s spot price. After the claim mines, the dialog shows the exact amount from the receipt. CentrifugeDepositPosition gains estimatedShares, CentrifugeRedeemPosition gains estimatedAssets, and CentrifugeClaimRowUi gains estimate.coinlist_account requirement
RequirementType gains 'coinlist_account'. The checklist shows it as “CoinList Account”, “Sign in with a CoinList account.” The user meets it by signing in, so its action opens no page. Code with an exhaustive switch over RequirementType needs the case. See Requirements.Changed
- The Centrifuge buy checkout blocks only when the vault’s
depositdirection is paused or stopped. Before, it also blocked on an unlinked, halted or unpermissioned vault, or a stale price. - The Centrifuge claim card no longer polls the vault status. Each row leads with what the claim pays, in that token’s icon, and the claim dialog is titled with it (“Claiming ~964.12 deJTRSY”).
- The Centrifuge order-placed dialog shows the amount invested or redeemed, the receiving wallet, and a notice to claim once the order is processed.
Fixed
- Centrifuge claims work. Before, both claim directions failed with a
404. - A Centrifuge request whose signature went stale, because it expired or a claim moved the wallet’s nonce, now reports
request-expired, so placing it again builds a fresh one.
October 19, 2026 - The SDK moves to @passage-cl/react, a smaller public API, the token-sale and Centrifuge checkouts, ClaimContainer, and session expiry handling
This release renames the SDK to Passage, in its package name and in its exported names. There are no deprecated aliases, so every app has to migrate. It also shrinks the public API to root Containers, their viewmodels, data hooks and namespaces, and the package now ships as ESM only. The package is now
The SDK is published as Then rename the destructured The Universal names are exported from
Each name now has exactly one entry point. These names were exported from both
See Checkout.
A rejected Data hooks return a
These hooks now return a discriminated union instead of loose fields. Each state is
See OAuth authentication.CoinList token-sale checkout in
A
CheckoutContainer takes side as a prop and needs no config. It mounts the SDK’s own CoinList token-sale checkout and a Centrifuge vault checkout, and honors a preselected wallet in every flow. The new ClaimContainer shows the second leg of an asynchronous offer. Session renewal is single-flight, and the new onSessionExpired tells you when a session ends.Breaking Changes
The package is now @passage-cl/react
The SDK is published as @passage-cl/react. The /universal and /server entry points keep their subpaths. See the install commands and the table above.Migration: replace @coinlist-co/react with @passage-cl/react in package.json and in every import, then apply the renames below.Exported names are rebranded to Passage
Every export that named the SDK after CoinList is renamed. The old names are gone.The field on the hook result is renamed too:
const { coinlist } = useCoinList() becomes const { passage } = usePassage(). The members of the client and server are unchanged, so coinlist.offers.list() becomes passage.offers.list(). The provider-missing error now reads usePassage() must be used within PassageProvider. Wrap your app with <PassageProvider config={...}>.Names that mean CoinList the company keep the CoinList name. These include CoinListSignInButton, CoinListSignInCardContainer, the CoinListTokenSale* checkout and participation names, passage.tokenSale, the coinlist::* offer types, ClientConfig.coinlistBaseUrl, and the coinlist.* browser storage keys.Migration: a find-and-replace over your source, in this order:coinlist field of usePassage() to passage.CSS class names and selectors are rebranded
The
--color-* design tokens keep their names. Only the selector they are defined on changed.Migration: a stylesheet that overrides tokens on .clco-sdk-root must target .pcl-sdk-root instead.name on every log record from pinoClientLogger and pinoServerLogger is now @passage-cl/react. Update any log filter that matches on the old name.ESM only, Node engines and bounded React peers
- The package ships ES modules only. The CommonJS builds,
main,moduleand therequireexport conditions are removed. Bundlers and Next.js consume ESM natively. A plain-Noderequire()works only through Node’s built-inrequire(esm). engines.nodeis now^20.19.0 || >=22.12.0.- The
reactandreact-dompeer ranges are now^18.0.0 || ^19.0.0, instead of>=18.0.0.viem ^2is unchanged.
Universal names are exported from /universal only
Each name now has exactly one entry point. These names were exported from both @coinlist-co/react and /universal, and are now exported only from @passage-cl/react/universal:HttpError, HttpResponse, EvmWallet, EvmSigner, ConnectWallet, BroadcastTxParams, WriteContractParams, WalletError, RedactedWalletError, RpcFailureReason, KycToken, KycLevelName, Logger, LogLevel, ProductionLogLevel, LogScope, LogValue, LogCause, LogBinding, LogBindings, SafeEvent, SafeFields, DebugEvent, UnredactedFields, RequestId, FrontlineEventId, PinoLoggerOptions.Migration:The public API is Containers, viewmodels, data hooks and namespaces
The public surface is now: each feature’s root Container, the viewmodel behind it, the data hooks it is built from, and the namespaces. Views, sub-screens, step viewmodels and the standalone flow functions are no longer exported. To build your own UI, use the product’s top-level viewmodel, whose state still carries each step’s UI types.These are removed with no replacement:
classifyWalletError (flows return an already classified WalletError), offerAssetIconUrl, offerIconUrl, stablecoinIconUrl, tokenLogoUrl, tokenLogoSrcSet, USDC_ICON_URL, USDT_ICON_URL, formatMonthYear, formatCountdown, formatSaleCardCountdown, useCountdown, the Ondo and Superstate constants and ABIs (ONDO_*, DEFAULT_ONDO_AMOUNT_TO_COMPUTE_PRICE, DEFAULT_ONDO_SELL_TOKENS_TO_COMPUTE_PRICE, ondoQuoteOfferDeadline, DEFAULT_AMOUNT_TO_COMPUTE_PRICE, SUPERSTATE_ISSUER_NAME, SUPERSTATE_SUPPORTED_INPUT_ASSETS, SUPERSTATE_SWAP_CONTRACT_ADDRESS_SEPOLIA (call superstateSwapContractAddress(chain) instead), SWAP_POLL_INTERVAL_MS, SUPERSTATE_SWAP_ABI, ERC20_ABI), the step enums OndoBuyStep, OndoSellStep and SuperstateSwapStep, ondoLoadErrorReason, ERC20_BALANCE_POLL_INTERVAL_MS, quoteIsStale, quoteOverAsks, decodeSwappedOutputAmount, BPS_DENOM, applyBps, QueryParamValue, QueryParamValues, User, UserId, UserEmail, PaginatedResponseDto, ServerNamespaceContext, ServerAuthConfig, and the client auth internals (OAUTH_STATE_KEY, OAUTH_CODE_VERIFIER_KEY, ClientAuthConfig, ClientAuthSession).The *NamespaceImpl classes and the fromDto companions are no longer part of the published types. Reach every namespace through PassageClient or PassageServer.The *Params types of the client flow methods (PrepareOndoBuyParams, ExecuteSwapParams, AuthorizeWalletParams and the rest) no longer carry the namespaces the SDK injects. A call on passage.* passes the same object as before. Only code that builds these types with ondo, erc20, swap, wallets or tokenSale fields breaks. ExecuteOndoSwapParams loses now.OauthClientResult and OauthClientErrorReason are renamed OAuthClientResult and OAuthClientErrorReason.Everything tagged
@alpha in the published types is public but outside the compatibility contract, and may still change in a minor release. That covers the new Centrifuge, claim and token-sale checkout surfaces.CheckoutContainer takes side as a prop, and config is optional
Only
offer, chain and wallets are required. side is read once at mount: Ondo mounts its buy or sell checkout, a Centrifuge vault mounts its buy checkout for "buy" and an empty container for "sell", and Superstate and the token sale ignore it. To change the side, remount with a key that covers the offer, the chain and the side. A sell page that forgets side now renders the buy checkout instead of failing to compile.Migration:CheckoutWalletSelection takes an externalAdapter
CheckoutWalletSelection is now the shared WalletSelection plus preselected. The two callbacks move into one adapter, and null hides the external-wallet row for a host with embedded wallets only.Migration:
disconnect is now logged and the selection is kept. Before, it was an unhandled promise rejection.In the wallet step’s UI state, externalWallet is now the three-arm CheckoutExternalWalletUi (hidden, disconnected, or connected with canDisconnect), and canDisconnect moves inside it. See Wallets.Checkout viewmodel state
OndoWalletSelectUiStateandOndoWalletSelectUiEventare replaced byCheckoutWalletSelectUiStateandCheckoutWalletSelectUiEvent. The event has the same shape. The state’sexternalWalletchanges as described above.OndoBuyCheckoutUiState.walletis now nullable. It isnullwhen a preselected wallet skipped the wallet step.OndoBuyQuoteStatusUiandOndoSellQuoteStatusUimerge intoOndoQuoteStatusUi, with the same shape.- Superstate’s amount step uses the shared
CheckoutAmountUiStateandCheckoutAmountUiEvent.SuperstateAmountUiStateandSuperstateAmountUiEventare removed,confirmEnabledbecomesconfirmCta: CheckoutCtaUi, and the confirm eventON_PREVIEW_ORDERis renamedON_CONFIRM_AMOUNT. - The Superstate order dialog state drops
issuer.
Data hooks return a state union
These hooks now return a discriminated union instead of loose fields. Each state is DISABLED | LOADING | ERROR { reason: LoadErrorReason } | CONTENT.useSwapQuote now reaches ERROR on a first failure instead of staying in its loading state. A failed refresh behind a quote it already has stays CONTENT. useErc20TokenBalances and useSwapOutputToken report LOADING as soon as their request changes, instead of showing the previous request’s answer.The Ondo load states (LoadOndoPriceState, LoadOndoTradingStatusState, LoadOndoBuyTransactionState, LoadOndoSellTransactionState) gain a DISABLED arm. A hook switched off with nothing in hand reports DISABLED instead of LOADING. An exhaustive switch over these states needs the new case.useParticipations is renamed useCoinListTokenSaleParticipations
Its behavior changes too:
- It polls
passage.tokenSale.list(offerId)every 15 seconds while the page is visible, and reads again when the page becomes visible.pollInterval?: Secondssets the interval. - It returns
refetch(). - A
dataseed skips only the first read. Before, a seed stopped all fetching. - A switched-off hook with nothing in hand reports
DISABLEDinstead ofLOADING. - A failed poll keeps the list it already has. A
not-authenticatedfailure drops the list and reportsERROR.
DISABLED case to any exhaustive switch over the state.Flow errors separate “not sent” from “sent but not confirmed”
A transaction that was broadcast but whose receipt wait failed used to come back as an ordinary failure, without its hash, so the UI let the user try again and could fill the order twice. It now has its own error arm, carrying thetxHash:The
swap and broadcast steps now mean only that the wallet never sent the transaction. The built-in review steps disable their button until the user leaves the step, and show “Your order was sent but has not confirmed yet. Check your wallet before placing another.” (or the swap and authorization equivalents).Migration: handle the new arms wherever you switch on these errors, and do not retry them.passage.tokenSale.execute takes the offer
The flow reads the funding contract, the payment token and the asset id from the
CoinListTokenSaleOfferDetail. It refuses an unsupported-chain, an unsupported-asset or an unknown-option before it prompts the wallet. Pass approval: { type: "mined", txHash } to retry a participation whose approval already mined, without a second wallet prompt.CoinListTokenSaleOfferDetail gains fundingContracts: OfferFundingContract[], read from the required funding_contracts field of the offer response.Token-sale and Superstate types
- The
BlockchainandWalletAddressnewtypes are removed.CreateParticipationParamstakeschain: EthereumChainandwalletAddress: EvmWalletAddress.Participation.chainis nowChain | null, andnullstands for a chain the SDK does not model.Participation.walletAddressisEvmWalletAddress | null. AllowWalletResponse’s broadcast arm nests the transaction:{ action: "broadcast_transaction"; tx: Tx }instead of{ action; to; data }.CoinListSignInButtonPropsis its own interface, and its props no longer acceptnull. Omit a prop instead.
Session handling (server)
ServerConfig.accessTokenExpiryBufferSecondsis now aSeconds, not anumber. Build it withSeconds.of(120)from@passage-cl/react/universal. The default is still 60 seconds.passage.auth.getAccessToken()on the server now throws the underlying error when the access token has expired and the refresh could not get an answer: a 5xx, a network failure orinvalid_client. It used to returnnulland clear the session. It still returnsnullwhen there is no session or the session was rejected. Other methods that need a session throw the same error in that case.CompleteOAuthParamsis removed.passage.auth.completeOAuth(grant)takes anAuthorizationGrant({ code, codeVerifier }) from@passage-cl/react/universal, with the same fields.ServerAuthNamespaceImplis no longer exported.
getAccessToken should map a null to “no session” (for example a 204) and a throw to a failure (for example a 503). An endpoint that maps every throw to “no session” signs your users out during an outage.Session handling (browser)
A throw from yourClientConfig.getAccessToken now leaves the auth state as it was, and passage.init() rejects with that error. Before, a throw became "logged-out". PassageProvider catches it, so usePassage().isReady can be true while passage.auth.getState() is still "unknown". Resolve null for “no session” and throw for “could not find out”.An empty base URL throws
createPassageClient and createPassageServer throw a ValidationError when baseUrl or tokensBaseUrl is an empty or whitespace-only string, for example an environment variable set to nothing. Before, the browser fell back to production for an empty baseUrl, and the server built relative URLs. Omit the field to use the default. In React, the error throws from PassageProvider’s render.Added
CoinList token-sale checkout in CheckoutContainer
A coinlist::token_sale offer now mounts the SDK’s own checkout: a wallet step, an amount in USD paid in USDC or USDT, and a review with the estimated tokens, the price per token and the receiving wallet. The amount is checked against the option’s minimum, and the checkout is blocked outside the sale window. Placing the order runs passage.tokenSale.execute.optionId?: OfferOptionIdonCheckoutContainerpicks the sale option. It defaults to the first option and is read once at mount.config["coinlist::token_sale"].onOrderPlacedis called after an order is placed.- An option with no fixed price, such as an auction, renders “Not available yet” (
no-price). useCoinListTokenSaleCheckoutViewModeland its UI types are public (@alpha).TokenSaleApprovalis public and inside the compatibility contract.
Centrifuge vaults
centrifuge::vault is a new offer type (@alpha). Vault offers now appear in offer listings. Before, the SDK dropped them from a list and threw on passage.offers.get for one. CentrifugeVaultOfferDetail carries vaultContracts, and TokenRole gains "vault".CheckoutContainermounts a Centrifuge buy checkout, which places a deposit request, forside: "buy". It is USDC only and blocks withunsupported-chainornot-orderablewhile the vault is paused.side: "sell"renders an empty container until the redeem flow ships.config["centrifuge::vault"].onOrderPlacedfires after the request mines.passage.centrifugeexists on bothPassageClientandPassageServer:
- Hooks:
useCentrifugeVaultStatus,useCentrifugeVaultStatuses,useCentrifugeClaimPositions,useCentrifugeClaim,useCentrifugeBuyCheckoutViewModelanduseCentrifugeVaultClaimViewModel. 'CENTRIFUGE'is a newLogScope. A logger that switches overLogScopeneeds the case.
switch over OfferType, Offer["type"], OfferDetail["type"] or TokenRole needs a centrifuge::vault (or vault) case.ClaimContainer
ClaimContainer (@alpha) is a drop-in card, titled “Your orders”, for the second leg of an asynchronous offer. Narrow the offer with AsyncOfferDetail.is(offer) first.- For a Centrifuge vault, it reads every wallet’s pending and claimable positions on-chain, and offers a Claim button on each claimable row.
config["centrifuge::vault"].onClaimedfires after a claim. - For a CoinList token sale, it lists your participations in that sale across your account and polls every 15 seconds, so an order placed on the same page appears by itself. There is nothing to claim: the sale delivers tokens and refunds by itself.
- It takes
wallets: WalletSelection, an optionalwalletLabelresolver,config,enabledandclassName. - To build your own claim UI, use
useCentrifugeVaultClaimViewModeloruseCoinListTokenSaleClaimViewModel(@alpha), with their UI state and event types.
Wallet preselection in every checkout
A preselected wallet (wallets.preselected) now applies to every provider. Ondo buy and sell, Centrifuge and the token sale skip the wallet step and open on the amount, and the remaining cards renumber. Superstate keeps its wallet step, because that step allow-lists the wallet on-chain, but opens with that wallet’s row selected. preselected is read once at mount: put it in the key if it can change. Before, only Ondo sell honored it.onSessionExpired
Config.onSessionExpired?: () => Promise<void> tells you when the SDK has given up on a session, on both the client and the server.- In the browser, it fires after a
401when yourgetAccessTokentwice answersnullor the very token the API just refused, one second apart, and the user had been logged in. - On the server, it fires when the same refresh token is rejected twice, 5 to 60 seconds apart, or when an expired session has no refresh token. The
SessionStoreis cleared first. - It never fires on logout, a 5xx or a network failure. It fires at most once per session within a 60-second window, counted per JavaScript module instance on the server (Next.js middleware and route handlers count separately), so make the handler idempotent. The SDK does not await it, and a rejection is logged and dropped.
Ondo market closed
When an Ondo market is paused or closed, the API answers a quote with409 not_tradable. The Ondo checkout now shows “Trading is closed right now” until the trading status reports tradable again. isOndoNotTradableError(error) (/universal) recognizes the error, isNotTradable(state) reads the trading-status state, and OndoLoadErrorReason and OndoCheckoutErrorReason gain "not-tradable".Namespace methods
passage.wallets.connect({ wallet, offerId, offerOptionId })binds a host wallet to an offer option. It replacesconnectExternalWalletFlow.passage.superstate.getQuote(params)returns aSwapQuote, on both the client and the server.
Other additions
LoadErrorReason("not-authenticated" | "generic-error") is the shared reason on theERRORstate of the ERC-20, Superstate quote, Ondo, participation and Centrifuge hooks. On these hooks an HTTP401, including one from the JSON-RPC proxy, now reportsnot-authenticatedinstead ofgeneric-error.useOffers,useOfferDetailsanduseRequirementskeep their own reason types.EpochMillis,MillisandSecondstime types in/universal.WalletSelection,ExternalWalletAdapterandWalletLabelResolverin/universal.formatShareAmount(amount, locale), and aminFractionDigitsoption onformatAmount.CheckoutExternalWalletUi,CheckoutWalletSelectUiState,CheckoutAmountUiState,CheckoutBlockedReason,OndoQuoteStatusUiand theLoad*Statetypes named above../package.jsonis exported.- The published API is recorded in API reports, and the typed surface outside
@alphais the compatibility contract.
Changed
- Single-flight session renewal. On the server, one refresh runs per refresh token per process, shared across
PassageServerinstances. A firstinvalid_grantno longer clears the session, and a 5xx or network failure never clears it. In the browser, concurrent requests share onegetAccessTokencall. A401now forces a refresh even when the token looks valid locally./oauth/tokenis never retried automatically. - A session with an invalid
expiresAtnow throwsValidationErrorinstead of being served as fresh forever. - Superstate checkout copy now matches the Ondo and Centrifuge wording. The amount button reads “Confirm amount”, approval and swap errors use the shared copy, and the review disclaimer names CoinList’s Terms of Service and Privacy Policy as well as Superstate’s Terms.
- Amount validation order is the same in every checkout: parse, zero, minimum, balance, cap. Ondo buy now reports the minimum before the balance.
- Amount fields show the token symbol after the number and accept only digits and one decimal point. The “Powered by CoinList” tag is removed from the sign-in card.
formatAmounttrims trailing zeros past the minimum after rounding, and Ondo share amounts show at most six decimals.- The Superstate review keeps its in-flight state when the user leaves mid-swap, so a second swap cannot start while the first is still running.
Fixed
passage.auth.logout()on the server clears the session whatever the revoke failure, including a network error, unlessstrictis set. Withstrict, it rethrows and keeps the session.- In the browser,
passage.auth.logout()wins over agetAccessTokenanswer already in flight, so a late answer can no longer sign the user back in. passage.ondo.executeSwapkeeps thetxHashwhen the receipt wait fails (see the newswap-unconfirmedarm).- The Ondo transaction hooks no longer get stuck loading when the request changes during a build.
- A nullable field missing from a response now reads as
nullinstead ofundefined. - A
401renewed into a persistent 5xx makes 4 attempts instead of 6. instanceof HttpErrorholds whichever entry point the error came from.- Long fund names wrap in the order-confirmation dialogs.
September 28, 2026 - Offers as a union on type, on-chain reads over JSON-RPC, token-registry artwork in the offers grid, and enabled on every data hook
This is a patch release by number, but it breaks compile-time contracts.
Both types are now discriminated unions with one variant per Code that builds an
The grid now loads the token registry alongside the offers, so its server-seed prop takes both.The logged-out error changes. These reads no longer throw Superstate
A host that drives
A chain you leave out falls back to CoinList’s proxy (Security reference fields on
These now take
Offer and OfferDetail become unions on type, so a field that only one kind of offer has can only be read after you narrow. The SDK now reads contract state through JSON-RPC instead of dedicated CoinList endpoints, which changes the return types of the ERC-20 and Superstate reads. OffersGridContainer’s data prop and the requirements checklist’s logged-out handling also change shape.The release also adds Config.rpc for routing on-chain reads to your own node, token-registry names and artwork on the offer cards, the security reference fields on OfferDetail, and enabled on every data hook that was missing it.Breaking Changes
Offer and OfferDetail are unions on type
Both types are now discriminated unions with one variant per OfferType. Each variant carries only the fields that mean something for it:An Ondo swap trades continuously and runs no sale window, so it has no
tagline, bannerUrl or startsAt. Previously it carried a blank string and a 1970 date. Swap offers publish without artwork more often than not, so their logoUrl is nullable. The fields every offer shares are OfferBase and OfferDetailBase, and the sale-window trio is OfferEditorial.Migration: narrow on type before reading a variant field. A field every offer shares (id, slug, tokens, endsAt, and on the detail name, asset and the rest) reads as before.Offer or OfferDetail by hand, such as fixtures, mocks or preloaded server data, has to build a specific variant.The checkout surface narrows with it. Each provider’s viewmodel takes its own variant, and so do the CheckoutConfig callbacks:An inline callback such as
symbol: (offer) => AssetSymbol(offer.asset.code) compiles unchanged. A callback you annotated with OfferDetail needs the narrower type.The offer-card UI models follow the same change. On OfferSaleCardUi, tagline and startsAt are now nullable (null for an Ondo swap). On both OfferSaleCardUi and OfferAssetCardUi, logoUrl: string is replaced by logo: OfferLogoUi | null. See “Token-registry artwork on the offer cards” under Added.A page from
coinlist.offers.list() or listPage() now drops any offer whose type this SDK version does not know, instead of failing the whole page. coinlist.offers.get(id) still throws ValidationError for an unknown type, because a single offer has nothing beside it to keep. isOfferType(value) is exported if you receive offer types from somewhere else.OffersGridContainer data is an object
The grid now loads the token registry alongside the offers, so its server-seed prop takes both.The change applies to both
OffersGridContainer and useOffersGridViewModel. Each half is independent: seed only offers and the client still fetches the registry.Migration:On-chain reads return plain values
coinlist.erc20 and the read methods of coinlist.superstate now call the contract through a viem PublicClient instead of a CoinList endpoint that wrapped each value in a DTO. The single-field wrappers are gone:TokenAllowance, TokenBalance and SwapAuthorization are removed.Migration: drop the property access.NotAuthenticatedError. By default they go through CoinList’s authenticated JSON-RPC proxy, so a logged-out read fails with the proxy’s 401. viem wraps whatever a transport throws, so that 401 reaches you as a viem error wrapping the SDK’s HttpError. A chain you point at your own node through Config.rpc (see Added) needs no CoinList session at all. coinlist.superstate.allowWallet is a backend write and still throws NotAuthenticatedError.Superstate SwapStatus is a union, and paused is not stopped
SwapStatus was { stopped: Uint256; swapLevel: Uint256 }. It is now:A
paused contract may still be swapping. pausedLevels is a bitmask over operations, and only the SWAP_LEVEL bit halts swaps. Ask isSwapAvailable(status) rather than comparing state to "active".SwapExecutionError gains a swap-paused arm, and swap-stopped now means the swap is permanently closed:Migration: add a
swap-paused case wherever you switch on result.error.step from coinlist.superstate.execute.The requirements checklist no longer renders a sign-in card
unauthenticatedState is removed from RequirementsChecklistContainer and RequirementsChecklistView, and RequirementsChecklistUiState loses its not-authenticated arm. A logged-out session now renders the checklist’s error state and fetches nothing. The loading and error arms now carry the title (and loading also the description), so the header stays in place while the rows load or fail.Migration: gate on auth before mounting the checklist, and render the sign-in card yourself.useRequirementsChecklistViewModel behind its own markup drops its not-authenticated case. useRequirements itself still reports reason: "not-authenticated".ONDO_QUOTE_EXPIRY_THRESHOLD_MS is replaced
The review step now offers a committed quote until the earlier of Ondo’s deadline and 15 seconds after it was built, instead of until 5 seconds before Ondo’s deadline.Migration: only for hosts building their own Ondo review step. Count down to
ondoQuoteOfferDeadline(transaction.expiresAt) instead of expiresAt minus the old threshold.Added
Route on-chain reads to your own node
Config takes an optional rpc: RpcConfig, which maps each EthereumChain to an RPC URL or a viem Transport:POST /v1/chains/{chain}/rpc) on its own, so overriding mainnet does not opt Sepolia out. The SDK always builds the PublicClient itself, so the chain definition, Multicall batching and retry policy stay the same whatever transport you pass. createCoinListServer takes the same option.Alongside it:viemChain(chain)returns the viemChaindefinition the SDK uses for anEthereumChain.isSwapAvailable,SWAP_LEVELandPauseFlags, for the Superstate status above.- Logging: a new
RPCLogScope, and anrpcarm onLogCausewithreason: RpcFailureReason(reverted,node-rejected,transport,malformed-response,unknown). Each reason names a remedy, not the viem error class. Nothing is read off the thrown error abovedebug.
Token-registry artwork on the offer cards
OffersGridContainer now loads the public token registry alongside the offers. Each card takes its name, ticker and logo from the registry when it lists the offer’s token, and falls back to the offer’s own fields otherwise. The grid waits for both loads, so a card paints once with its final artwork. If the registry fails, the cards fall back to the offer’s own fields instead of failing the grid.OfferSaleCardUi.fromOffer / fromDetail and OfferAssetCardUi.fromOffer / fromDetail take an optional second argument, the offer’s TokenMetadata | null. Registry values win when both exist.Security reference fields on OfferDetail
OfferDetailBase gains five nullable fields describing the underlying security:The Ondo checkout sidebar now fills its asset-detail rows from these fields. A
null field hides its row.enabled on the remaining data hooks
These now take enabled (default true). When it is false they send no request, and a seeded or already-fetched CONTENT state survives:useOffers,useOfferDetails,useRequirements,useParticipationsuseOffersGridViewModelandOffersGridContaineruseRequirementsChecklistViewModelandRequirementsChecklistContainer
Requirements checklist states
RequirementsChecklistItemUi gains optional statusMessages (pending, completed and rejected sentences per requirement type), and the content state gains an optional blockedMessage for an unsupported jurisdiction. When blockedMessage is set, a neutral banner replaces the description, rows and Continue. The View also renders an explicit empty state when an option has no requirements.Changed
CheckoutContainermounts only the matching provider’s container. It used to call every provider’s viewmodel on every render and disable the ones that did not match.config["ondo::swap"].symbolandsidenow run only when anondo::swapoffer is showing, so they no longer have to handle Superstate and token-sale offers.enabledis forwarded to the mounted container as-is.- Ondo quotes are offered for at most 15 seconds. The review step’s countdown runs to the earlier of Ondo’s deadline and 15 seconds after the quote was built, then asks for a fresh one.
- Requirements checklist design and copy. The footer shows an “N/M completed” counter, and Continue stays disabled until every requirement is met. The default labels are now Identity Verification (was KYC Verification) and Tax Documentation (was Document), with new descriptions for KYC, accreditation and tax documents. A tax document whose status is
in_progressstays a required step until the user signs it. The loading, error and unsupported-jurisdiction states were redesigned.
Fixed
- Superstate’s confirmed output amount. The
Swappedevent ABI declared two non-indexed words where the contract emits three. As a result,decodeSwappedOutputAmountread the protocol fee as the amount received, andexecuteSwapreported that fee as the confirmedoutputAmount. It now decodes the amount received. - Amounts with a leading or trailing decimal point.
parseBlockchainAmountnow accepts.25(read as0.25) and25.(read as25). A bare.is still rejected. - Checkout loading skeletons. The price skeletons in
AssetSummaryRowand the Ondo and Superstate sidebars no longer stack flush against each other while loading.
September 17, 2026 - The /universal entry point, the Ondo sell flow, structured logging, and Base support
Three things change every call site: the
The subpath is renamed. Nothing it exports is renamed with it, and the entry point gained exports rather than losing any.The other two entry points, The React surface forked with it. Every buy screen’s component and viewmodel is now
Migration: add A chain with no Ondo contract on it is known before any step runs, and the old shape had to render a wallet picker leading nowhere and a confident
Checkout asset icons now resolve from the public token registry first and fall back to the offer’s own artwork, so
A host that already knows the wallet can skip the wallet step: set
/shared subpath is now /universal, Ondo’s one-directional swap API forks into buy and sell, and the Ondo swap contract is read off the offer instead of a constant the SDK held.The release also adds a complete Ondo sell checkout, an optional structured logger on Config, and Base mainnet and Base Sepolia in the chain model.Breaking Changes
@coinlist-co/react/shared is now @coinlist-co/react/universal
The subpath is renamed. Nothing it exports is renamed with it, and the entry point gained exports rather than losing any.shared described where the code sat in the repository; universal describes what it is - the half of the SDK that runs unchanged in a browser and on a backend. The old name also collided with the SDK’s internal shared/ directories, which mean something else entirely.Migration: find and replace the import path. There is no compatibility alias - the old subpath is gone from exports, so a missed one is a resolution error at build time rather than a silent fallback.@coinlist-co/react and @coinlist-co/react/server, are untouched./universal now also exports EvmWallet and the wallet-address constructors, WalletError, Logger and the rest of the logging types, and swapSpender. A host that was reaching for those through @coinlist-co/react can keep doing so; both barrels name them.Ondo forks into buy and sell
Ondo ships two products on oneondo::swap offer type. Preparing an order now depends on which one, so the single method became two; broadcasting stays one, because it reads calldata and a deadline and knows about neither direction.A purchase approves the funding coin and a sale approves the asset, and the two builders are two endpoints -
POST /v1/ondo/swap/buy and POST /v1/ondo/swap/sell - whose amount is denominated in a different token on each. A single method taking a side would have re-merged exactly that distinction.Migration: pick the method that matches the direction. The parameters are otherwise unchanged apart from the required swapContracts below.OndoBuy*, the new sell screens are OndoSell*, and the two steps both products share kept their unqualified names:The Ondo swap contract is read off the offer
ondoSwapContractAddress(chain) is removed, and OfferDetail gains a required swapContracts: OfferSwapContract[].One constant can only name one deployment, and beta and production run different contracts on the same chain - so whichever one it named, the other approved a spender that could not fill the order. The address now arrives per environment on the offer.Migration: pass offer.swapContracts to prepareBuy, prepareSell and useOndoBuyAmountViewModel; read a single address with swapSpender(offer.swapContracts, chain). Code that constructs an OfferDetail by hand - fixtures, mocks, preloaded server data - adds the field, with [] meaning “cannot be swapped anywhere”.OndoSwapExecutionError loses its spender-mismatch arm as a result: the backend compares the built transaction’s destination to the contract it serves on the offer, so a third comparison would check one source against itself.CheckoutConfig for Ondo requires side
config["ondo::swap"] takes a new required side: () => OrderBookSide.A thunk rather than a resolver taking the offer, because no offer can answer it: offer.type is ondo::swap both ways, and the direction is a property of the page. This is the one config entry that is per-render rather than per-integration, so a host with a buy page and a sell page closes the thunk over whatever selects the direction.side. It is required with no default because the wire parameter behind it defaults to buy silently, and an unstated direction should be a compile error rather than a purchase.The Ondo checkout state is a union
OndoBuyCheckoutUiState - and the new OndoSellCheckoutUiState - are now discriminated unions rather than product types:0 USDC balance before refusing on Confirm. OndoCheckoutErrorReason is 'unsupported-chain' today.Migration: only for hosts driving useOndoBuyCheckoutViewModel behind their own markup. switch on state.type first, then read the steps off the content arm.OfferCard is now two components
One card cannot be both a live-sale tile and a browse row.
OfferSaleCard is the dark sale card with the countdown; OfferAssetCard is the light catalog row. Each takes its own UI model - OfferSaleCardUi or OfferAssetCardUi, both with fromOffer and fromDetail companions - rather than an Offer.Migration: pick the card that matches the surface and map the offer through its UI model.CheckoutAssetUi.fromOffer takes token metadata
Checkout asset icons now resolve from the public token registry first and fall back to the offer’s own artwork, so CheckoutAssetUi.fromOffer takes the offer’s TokenMetadata | null alongside it. This affects hosts composing their own checkout viewmodel from the step-level hooks; pass null to keep the previous behavior.Added
The Ondo sell flow
A complete sell checkout, mirroring buy.CheckoutContainer renders it when config["ondo::swap"].side() returns "sell" - no other host change is required.A sale sells the asset and settles into USDC, so the approval is granted on the asset’s contract. The SDK ships no registry entry for a provider’s asset, so its address and decimals come off the sell quote and nowhere else.See Ondo Swap for the whole flow.
CheckoutWalletSelection.preselected
A host that already knows the wallet can skip the wallet step: set preselected to a ready EvmWallet and the checkout opens on the amount, renumbering the cards that remain. A sell page reached from a position knows which wallet holds it, and asking again would be a question with one answer.It lives on the wallet seam rather than in a provider’s config because nothing about it is one provider’s - but only the Ondo sell checkout honors it today, so do not assume a skip from the field being set.Structured logging
The SDK can now report what it is doing.Config takes an optional logger, and with none the SDK logs nothing at all - no console fallback, at any level, on any codepath.Logger(from@coinlist-co/react/universal) is the port:debug,info,warn,error, andlevel(). Implement it over your own sink in four methods.- Two shipped implementations, one per environment:
pinoClientLoggerfrom@coinlist-co/reactandpinoServerLoggerfrom@coinlist-co/react/server. - Every line is structured: a constant
msgnaming what happened, ascope(HTTP,OFFERS,ONDO,HOOKS, …), and a bag of fields carrying every value that varies. Field names follow OpenTelemetry where a stable key exists. - Every event is a lambda, checked against
level()before it runs, so an expensivedebugline costs nothing atinfoand nothing at all with no logger supplied.
pino ^10 is a new runtime dependency and does not tree-shake: every integration pays roughly 3 kB gzipped whether or not it logs.Base mainnet and Base Sepolia
EthereumChain gains base_mainnet (8453) and base_sepolia (84532), with chain ids, network names, explorers, and Base USDC and USDT in the token registry. Adding a chain is a deliberate compile error until every lookup table has an entry, so a host switching on EthereumChain exhaustively will need two more arms.No provider has a swap contract deployed on Base, so an Ondo or Superstate checkout there renders the unsupported-chain state.Smaller additions
coinlist.tokens.list(chain?)fetches the registry’s complete snapshot in a single request - every chain with no argument, one chain with it. Prefer it overget()in a loop when rendering a catalog. Unlikeget, a missing snapshot throws rather than answering[].Offer.tokensandOfferDetail.tokens, the(chain, address)entries an offer’s assets live at, keyed by role.OffersAssetGrid, a responsive browse grid ofOfferAssetCardleaves. Pure - no fetch, no provider - and distinct fromOffersGridView, which lays out sale cards.useErc20Balance, the address-keyed counterpart touseErc20TokenBalances, for a token whose contract address arrives at runtime and has no registry symbol to look up.useCheckoutOfferTokenMetadata, which keys the registry lookup by the offer’s token entry for the checkout chain.OndoCheckoutErrorandOndoCheckoutErrorReason, the flow-level error surface both Ondo products share.- Release candidates on npm. Every merge to
mainnow publishes<next-patch>-rc.<short-sha>under thercdist-tag, so a fix can be tried before it is tagged:npm install @coinlist-co/react@rc.
Changed
- Checkout asset logos resolve from the token registry first, taking the vector logo or the smallest raster variant covering the render width, and falling back to
Offer.logoUrland then a neutral placeholder. Every absence - loading, error, disabled, unlisted, no token entry - falls back rather than rendering a broken image. - An Ondo checkout on a chain with no contract now fails at the flow level, before any step runs. It previously displayed a
0balance for a token the flow would never spend and refused on Confirm, as though the amount were the problem. useTokenMetadatamoved from the tokens feature to the shared hooks, now that checkout is its second consumer. The import path is unchanged: it is still exported from@coinlist-co/react.lucide-reactis now^1(from^0.577). It is a bundled dependency, not a peer, so no host action is needed.
Fixed
- An offer token on a chain the SDK does not model no longer blanks the offers list. One Base token took down the whole catalog:
Offer.fromDtoruns inside the paginated response’smap, so one unparseable item threw away the page. Unknown chains are now skipped, costing that token its logo. - The Ondo amount step no longer strands the user when a quote expires while an error is showing. The refresh action is reachable from that state.
- The Ondo amount input reads
USDCwith amaxaffordance, matching the Superstate checkout.
Removed
identity_verified,proof_of_addressandsource_of_fundsare gone fromRequirementType. They were left over from an abandoned modular-KYC effort and the backend no longer returns them, so aswitchover requirement types needs three fewer arms.
August 25, 2026 - Namespaced client and server surfaces, the Ondo buy flow, a drop-in CheckoutContainer, and token metadata
This is the largest release since the SDK went out, and almost every call site changes. It finishes the namespace migration v0.10.0 started, splits every component with behavior into a
A second swap provider shipped in this release, so a namespace called
Components split into
Every component that has behavior is now a
Reading ERC-20 balances is not specific to a swap - the token sale and the Ondo buy flow need the same thing - so the hook moved out of the swap feature.Migration: rename the import and the call. The options (The reads are free to poll while the user sizes an order;
A global
A BCP-47 tag applied to number, date and currency formatting in every SDK component in the tree. Defaults to
Container / View pair, adds a second swap provider (Ondo) with a complete checkout UI, and introduces CheckoutContainer so a host can render any offer type without knowing which provider is behind it.Nothing below changes behavior. Parameters and return types are unchanged unless a row says otherwise.Breaking Changes
Every remaining flat method moved into a namespace
CoinListClient and CoinListServer now expose nothing but namespaces. init() is the single exception on the client, because it is lifecycle rather than domain; CoinListServer has no top-level member at all.ClientServer
Migration: insert the namespace before the method name, and pass a single object to any method that used to take more than one argument.
coinlist.swap is now coinlist.superstate
A second swap provider shipped in this release, so a namespace called swap could only ever mean one of them. Provider namespaces are named after the provider; CoinList’s own namespaces stay named after what they do.Migration: replace
coinlist.swap. with coinlist.superstate., rename executeSwap to execute, and take the ownership challenge from coinlist.wallets. authorizeWallet keeps its name, its parameters, its phases, and its error steps.Token-sale and ERC-20 methods dropped their stuttering prefixes
coinlist.tokenSale.createParticipation(params) is unchanged.Offer.type values changed
OfferType was 'sale' | 'swap'. It is now 'coinlist::token_sale' | 'superstate::swap' | 'ondo::swap', so an offer names the provider that settles it rather than the shape of the transaction. Two providers now issue swap offers, and 'swap' could not tell them apart.Migration: switch on the new values exhaustively rather than testing for one. A
default branch that assumes “everything else is a token sale” now catches Ondo offers it cannot render.If you render
CheckoutContainer (below), you do not switch at all: it dispatches on offer.type for you, and a future offer type becomes a compile error in your CheckoutConfig rather than a blank screen.Components split into Container and View
Every component that has behavior is now a Container (owns the props, calls the viewmodel) and a View (pure, driven by a state object). Both halves are exported, along with the viewmodel between them, so you can keep the SDK’s logic and supply your own markup.Migration: append
Container to the component name. Props are unchanged.OfferCard, RequirementItem and ConnectedWalletList are pure already and keep their names.useSwapTokenBalances is now useErc20TokenBalances
Reading ERC-20 balances is not specific to a swap - the token sale and the Ondo buy flow need the same thing - so the hook moved out of the swap feature.Migration: rename the import and the call. The options (address, chain, assets, pollIntervalMs, enabled) and the returned { balances, isLoading } are unchanged. The poll-interval constant is now ERC20_BALANCE_POLL_INTERVAL_MS.Added
Ondo buy flow
Ondo is the SDK’s second swap provider.coinlist.ondo covers the reads and the two halves of placing an order.The wallet-driven half is client-only, and is deliberately two calls rather than one:
coinlist.ondo.prepareSwap({ wallet, symbol, chain, tokenAddress, amount, onProgress? })submits the ERC-20 approval, then builds the transaction.coinlist.ondo.executeSwap({ wallet, transaction, chain, now?, onProgress? })broadcasts it.
Built calldata is only good for about a minute and an approval takes most of one to mine. Doing both behind a single button would hand the user an expired quote, so the SDK splits the flow where the user’s decision is. Both calls are total: every failure comes back as
{ type: "error", error: { step } } rather than throwing.prepareSwap is not, because building a transaction spends an attestation. No CoinList fee is applied to a read quote - buildSwapTransaction takes the gross amount and Ondo prices the remainder after the fee comes off.Ondo runs no sandbox, so the read params take no chain: every environment prices against Ondo production on Ethereum mainnet. buildSwapTransaction is the exception, because it names a real contract on a real chain.Also new: ondoSwapContractAddress(chain), ONDO_POLL_INTERVAL_MS, ONDO_QUOTE_EXPIRY_THRESHOLD_MS, ONDO_SUPPORTED_INPUT_ASSETS, and DEFAULT_ONDO_AMOUNT_TO_COMPUTE_PRICE.CheckoutContainer, a drop-in checkout for any offer type
CheckoutContainer switches on offer.type and renders the matching provider’s checkout, so a host renders one component for every offer in its catalog.CheckoutConfig is keyed by OfferType with every key required, so a new offer type in the catalog breaks the build rather than rendering nothing. defaultCheckoutConfig fills in the entries that have a sensible default.wallets is a CheckoutWalletSelection: the embedded wallets, the connected external one, and the two lambdas that connect and disconnect it. The SDK ships no wallet stack, so this is where you plug in Privy, AppKit, or your own.coinlist::token_sale is a host render slot until the SDK ships that flow. The Ondo symbol resolver must be total: React hooks cannot be called conditionally, so every provider’s viewmodel runs for every offer and the resolver is called for Superstate and token-sale offers too, where its answer is discarded. Return anything for an offer you do not recognize rather than throwing.Superstate swap checkout UI
The Superstate swap UI moved into the SDK from the reference app:SuperstateSwapCheckoutContainer and SuperstateSwapCheckoutView, the step views (SuperstateWalletView, SuperstateAmountView, SuperstateReviewView, SuperstateOrderConfirmed, SuperstateSidebarView, SlippagePicker), and the viewmodels behind them.Each product exposes a single god viewmodel wrapping the whole flow - useSuperstateSwapCheckoutViewModel(), useOndoBuyCheckoutViewModel() - plus the smaller hooks it is composed from, so you can write your own viewmodel against the same pieces.Token metadata from the CoinList registry
coinlist.tokens resolves display metadata - name, symbol, decimals, and light and dark logos - keyed by (chain, address) rather than by symbol, which can collide.It is public and unauthenticated: unlike every other namespace, its methods never require a logged-in user, and it works on the server with a read-only session store.
useTokenMetadata({ chain, address, enabled }) is the hook form.OfferDetail gained a tokens field carrying the (chain, address) pairs to look up. TokenLogo is VECTOR (one SVG) or RASTER (webp variants at 32-512px); pick the smallest variant that covers your render size.OfferToken.chain is a Chain, which spans Solana as well as EVM, while coinlist.tokens.get takes an EthereumChain. Narrow before you look a token up - an offer may list a token on a chain the registry routes do not serve.A global locale on CoinListProvider
'en-US' - a fixed constant, never navigator.language, so server and client render the same markup. A plain string, so a Next.js server layout can pass params.locale straight through.It does not reach embedded third-party flows: the Sumsub UI language stays on RequirementsChecklistContainer#identityVerificationOptions.locale.HttpError and apiErrorCode
HttpError is now exported from both @coinlist-co/react and @coinlist-co/react/shared; apiErrorCode from @coinlist-co/react/shared. HttpError is what every API call rejects with when the backend refuses a request, so without it there is no way to catch a 4xx by type. apiErrorCode(error) reads the code out of the response body, which is what tells two 422s apart.Smaller additions
SolanaChain('solana_mainnet' | 'solana_devnet') andChain, the union of it withEthereumChain, both with validating constructors that throwValidationErroron an unknown value. Solana offers are not executable yet - this is the type surface an offer’s tokens are parsed against, ahead of theSolanaWalletseam.EvmSigner, the signing-only half ofEvmWallet(address+signMessage). The external-wallet ownership proof needs nothing more, so a host can satisfy that flow without implementing on-chain capabilities.tokensBaseUrlonClientConfigandServerConfig, to pointcoinlist.tokensat a non-production registry.CoinListSignInButton, the button on its own for hosts that do not want the card.VerificationOverlay, and every requirements viewmodel and.typesmodule.useOndoPrice,useOndoTradingStatus,useOndoSwapTransactionfor composing your own Ondo viewmodel.- Formatting and presentation helpers:
useCountdown, and theasset-icon,dateanddurationformatters.
Changed
- Requirements, offers and wallets are the same namespace on both sides.
coinlist.offers,coinlist.requirementsandcoinlist.walletsexpose identical method names onCoinListClientandCoinListServer; the server adds an optional trailingclientCredson the reads that can run without a user session, and the client addsrequirements.handle. - Ondo reads live under
/v1/ondo/swap/*and take a requiredside. Ondo reports its limits per side, so a trading status fetched without one answers 422 rather than guessingbuy. getQuoterefuses a request sized by bothtokenAmountandnotionalValue, throwingValidationError. TypeScript accepts an object carrying both, and the previous behavior silently droppednotionalValue.Participation,SwapQuoteand the other provider types moved modules inside the package. They are still exported under the same names from@coinlist-co/react/shared, so only deep imports break - and deep imports were removed in v0.5.0.
Removed
coinlist.swap.requestWalletOwnershipChallenge- usecoinlist.wallets.createOwnershipChallenge, which is the same call and is shared by the checklist and the allow-list flow.
July 27, 2026 — Fix token-sale participation amount sent in the wrong units
Fixed
Token-sale participations recorded the amount in the wrong units
coinlist.tokenSale.executeTokenSale recorded the participation with the raw base-unit amount (e.g. "25000000" for 25 USDC) instead of a decimal token amount ("25"). The participations API rescales the amount by the asset’s decimals to check it against the on-chain approval allowance, so the base-unit value overflowed the approved allowance — the backend rejected every participation and funds failed to commit. executeTokenSale now records the decimal amount, so participations go through as expected.If you drive the lower-level coinlist.tokenSale.createParticipation yourself, pass amount as a decimal token amount (e.g. "100" for 100 USDC), not raw base units. This has always been what the backend expects; only the CreateParticipationParams.amount doc comment was wrong, and it is now corrected.July 24, 2026 — Token-sale flow, erc20/tokenSale namespaces, and stricter offer models
Breaking Changes
Participation methods moved to the tokenSale namespace
The flat participation methods on CoinListClient and CoinListServer now live under coinlist.tokenSale, mirroring how swaps sit under coinlist.swap.Migration: insert the
tokenSale namespace before the method name. The parameters and return types are unchanged.ERC-20 reads moved from swap to a new erc20 namespace
getTokenAllowance and getTokenBalance are generic ERC-20 reads shared across the swap and token-sale flows, so they moved off coinlist.swap onto coinlist.erc20.Migration: replace
swap.getTokenAllowance with erc20.getTokenAllowance and swap.getTokenBalance with erc20.getTokenBalance. The endpoints and parameters are unchanged.Offer.endsAt and OfferDetail.endsAt are now nullable
An offer can legitimately have no end date, so endsAt is now Date | null (was Date). fromDto maps a missing end date to null.Migration: handle the null case when reading endsAt.Added
Token-sale flow
A newtokenSale namespace encapsulates the on-chain invest flow, mirroring executeSwap. On the client, coinlist.tokenSale.executeTokenSale() runs a sale end-to-end against a connected EvmWallet:approve() for the sale amount (resetting a stale non-zero allowance to 0 first for USDT-style tokens), waits for it to mine, and records the participation with CoinList. Wallet connection and chain switching remain the host’s responsibility. See CoinList Token Sale for the end-to-end integration.An
approve() is submitted even when the existing allowance already covers the amount: the backend requires a fresh approval transaction hash on every participation and verifies it on-chain before confirming.allowance-reset / allowance-reset-reverted versus approval / approval-reverted) so you can tell which of the two wallet prompts the user rejected, and the approval hash is surfaced on a backend failure so recording can be retried without re-approving on-chain. New types: ExecuteTokenSaleParams, TokenSaleExecutionPhase, TokenSaleExecutionError, TokenSaleExecutionResult, and the shared Erc20ApprovalError (reused across the swap and token-sale flows).erc20 namespace
coinlist.erc20 exposes the generic ERC-20 reads shared across on-chain flows:Offer type
Offer and OfferDetail now expose a type field (OfferType, one of 'sale' or 'swap') so you can distinguish token-sale offers from swap offers. OfferType is exported from @coinlist-co/react/shared.Changed
Stricter offer models
Fields the backend now guarantees as non-null are typed as required (string instead of string | null):Offer:tagline,bannerUrl,logoUrl.OfferDetail:tagline,bannerUrl,logoUrl,category.
OfferDetail.about stays nullable. OfferCard now hides its “Ends” row when an offer has no end date.July 20, 2026 — Swap namespace, external-wallet connect, and KYC/tax/PII flows
This release folds in the 0.7.0 and 0.8.0 development versions.Shared (
The provider is now an alias of
Added
Swap namespace
client.swap and server.swap wrap the swap, token, and wallet endpoints:On the client, the namespace also drives a caller-supplied wallet:
authorizeWallet({ wallet, offerId, contractAddress, chain, onProgress })— proves wallet ownership and allow-lists it for a swap offer, with typed progress phases and asuccess/errorresult. Idempotent: returns success immediately if the wallet is already authorized.executeSwap({ wallet, contractAddress, chain, inputTokenAddress, quote, slippageBps, onProgress })— runs the full on-chain swap: status check, ERC-20 approval (with USDT-style allowance reset), swap submission, and confirmation. The confirmed output is decoded from theSwappedevent.
viem ^2 is a new peer dependency.Swap hooks
useSwapOutputToken— reads the swap contract’s output token (the tokenized equity).useSwapQuote— polls a live quote (15s default), skipping until the output-token decimals and a valid amount are known.useSwapTokenBalances— polls the user’s balance for one or more input assets.
Wallet abstraction
EvmSigner / EvmWallet interfaces plus typed WalletError classification — bring your own wallet stack (wagmi, viem, Privy, …). New phase and error types: WalletAuthorizationPhase, WalletAuthorizationError, SwapExecutionPhase, SwapExecutionError, SwapExecutionResult.See Superstate Swap for the end-to-end integration.External wallet connect
Prove and bind an external wallet to an offer option:createWalletOwnershipChallenge(params)→POST /v1/wallet-ownershipconnectExternalWallet(offerId, params)→POST /v1/offers/{offer_id}/addresseslistOptionAddresses(offerId, offerOptionId)→GET /v1/offers/{offer_id}/addressesremoveOptionAddress(offerId, addressId)→DELETE /v1/offers/{offer_id}/addresses/{id}
ConnectWalletModal and ConnectedWalletList (list, change, and remove bound wallets), with the useConnectWallet and useOptionAddresses hooks.Identity, KYC, and tax documents
createKycToken(levelName?, reset?)→POST /v1/kyc-token(Sumsub)fetchPii()→GET /v1/pii, pre-fill data for tax formssubmitDocument(documentType, fields)→POST /v1/documents/{document_type}/submission- Components
IdentityVerificationandTaxDocumentModal; hooksuseKycTokenanduseTaxDocument. handleRequirementnow resolves thekyc_approved,identity_verified,proof_of_address,source_of_funds,accreditation,external_wallet,whitelisted_wallet,document, andjurisdictionrequirement types.
identity_verified, proof_of_address, and source_of_funds have since been removed from the API. The SDK still handles them, but offers no longer return them, so you do not need to write branches for those three.App-level authentication
CoinListServer.clientCredentialsOAuth() performs the OAuth 2.0 client_credentials grant for app-level access without a user session. The server offer reads (fetchOffers, fetchOffersPage, fetchOfferDetails, fetchOfferRequirements) accept an optional clientCreds argument to use it.Shared (@coinlist-co/react/shared)
BlockchainAmount(withadd/sub),parseBlockchainAmount,formatAmount,computeSlip, and theBpsnewtype.- Constants and helpers:
DEFAULT_SLIPPAGE_BPS,SLIPPAGE_OPTIONS_BPS,SWAP_POLL_INTERVAL_MS,SUPERSTATE_SWAP_CONTRACT_ADDRESS_SEPOLIA,TOKEN_REGISTRY,USDC_SYMBOL,txExplorerUrl. - Swap, KYC, PII, document-submission, and wallet-ownership types.
Changed
CoinListProvider is safe to mount app-wide
The provider is now an alias of CoinListContextProvider and renders no DOM or styles. Components self-scope their styling through CoinListStyleScope, so mounting the provider high in your tree no longer affects the rest of the app.July 2, 2026 — Required approvalTransactionHash, stricter wallet type, CSS isolation
Breaking Changes
approvalTransactionHash is required on createParticipation
CreateParticipationParams now requires approvalTransactionHash — the hash of the ERC-20 allowance transaction that precedes the participation. Submit the approval on-chain first, then pass its hash.WalletAddress narrowed to `0x${string}`
WalletAddress is now the template-literal type `0x${string}` instead of string. Values that aren’t 0x-prefixed literals need validation or a cast at the boundary.Added
CSS isolation
SDK styles are isolated behind a prefixed Tailwind build with scoped provider injection, and context is separated from styling so the context functions can be used without pulling in SDK styles. This work completes in v0.9.0, whereCoinListProvider becomes safe to mount app-wide.coinlist.co support
Added first-class support for the coinlist.co environment.May 7, 2026 — Read-only SessionStore support
Breaking Changes
SessionStore.setSession is now optional
setSession has been changed from a required method to an optional one (setSession?). This formalizes read-only store mode for execution contexts — such as Next.js Server Components — that can read cookies but cannot write them.Previously the only workaround was a no-op setSession: async () => {}, which silently discarded refreshed sessions after consuming the refresh token over the network, effectively causing a silent logout. Omitting setSession is now the explicit, safe contract: the SDK skips token refresh entirely, making no network calls and consuming no refresh tokens.Migration: if you currently pass a no-op setSession, remove it. If your store is writable, no change is needed.Added
WritableSessionStoreRequiredError
New error class exported from @coinlist-co/react/server. completeOAuth() and logout() throw this immediately when called on a read-only store (no setSession), before any network call is made.Fixed
No unnecessary network retries on expired tokens in read-only mode
Previously, when a request returned a 401 and the store was read-only, the SDK would retry the request with the same expired token — wasting a round-trip that always failed. The SDK now detects the read-only store and surfaces the 401 immediately without retrying.May 5, 2026 — API consistency, new /shared entry point, and Base* component removal
Breaking Changes
Client methods renamed: fetchAll* → fetch*
fetchAllOffers and fetchAllParticipations have been renamed on both CoinListClient and CoinListServer to drop the redundant All prefix.Migration: do a global find-and-replace in your codebase:
Hooks renamed: useCoinList* → use*
The CoinList infix has been dropped from all hook names and their associated option/result/reason types.Migration: rename the hook calls and any imported types. Example:
SSR prop renamed: serverOffers / serverData → data
The SSR pre-fetch prop has been unified to data across all hooks and components that accept server-side data. The RequirementsServerData type is also renamed to RequirementsData.Migration: rename the prop/option to
data wherever you pass pre-fetched server results.LoadRequirementsState CONTENT shape: requirementsByOptionId → requirements
If you read the CONTENT state returned by useRequirements (or previously useCoinListRequirements) directly, the field name has changed.Base* components removed
BaseOffersGrid, BaseOffersGridProps, BaseRequirementsChecklist, and BaseRequirementsChecklistProps have been removed. The connected components (OffersGrid, RequirementsChecklist) now accept all the same customization props directly — including the optional data prop for pre-fetched server data — so there is no longer a need for a separate base variant.Migration: replace BaseOffersGrid and BaseRequirementsChecklist usage with the main components and pass the same props directly.Import paths restructured — new @coinlist-co/react/shared entry point
Sub-path exports (/client, /client/hooks, /client/components, /client/core) have been removed. The package now exposes three canonical entry points:Domain types that were previously re-exported from both
@coinlist-co/react and @coinlist-co/react/server are now the sole responsibility of @coinlist-co/react/shared. They remain re-exported from the client and server entry points as well, so most imports will continue to work without changes. However, if you were importing from the now-deleted sub-paths, update your imports:OffersGridProps type renamed from Props
The exported type for OffersGrid props was the generic name Props. It is now exported as OffersGridProps.April 30, 2026 — SSR data fetching, useParticipations hook, and requirement action defaults
Added
Server (@coinlist-co/react/server)
CoinListServernow exposes the full data-fetching surface previously only available onCoinListClient:fetchAllOffers(),fetchOffersPage(),fetchOfferDetails(),fetchAllParticipations(),fetchParticipationsPage(),fetchParticipation(),createParticipation(),fetchOfferRequirements(), andfetchRequirementStatuses(). Use these in Next.js Route Handlers and Server Components without shipping any client bundle.@coinlist-co/react/servernow re-exports all shared types —Offer,OfferDetail,Participation,Requirement,RequirementStatusInfo, pagination helpers,NotAuthenticatedError, and related types — so you no longer need to import them from the client entry.
Client (@coinlist-co/react, @coinlist-co/react/client)
useParticipations(offerId?)hook — loads all participations for the authenticated user with aLOADING / CONTENT / ERRORstate machine, optionally filtered by offer. New exported types:UseParticipationsResult,LoadParticipationsState,LoadParticipationsReason.handleRequirement(requirement)onCoinListClient— opens the corresponding CoinList page for completing a requirement in a new tab (/verify-identity,/wallet, etc.). No-op forjurisdictionrequirements.contactSupport()onCoinListClient— opens the CoinList support ticket page in a new tab.
Changed
Components
RequirementsChecklist:onRequirementActionandonContactSupportprops have been renamed toonRequirementActionOverrideandonContactSupportOverride. Both now default toCoinListClient#handleRequirementandCoinListClient#contactSupportrespectively, so most integrations can omit them entirely. Passnullto disable the corresponding button/link.RequirementItem:onActionandonContactSupportnow acceptnull(in addition toundefined) to suppress rendering of action buttons.
Removed
Client (@coinlist-co/react, @coinlist-co/react/client)
sandboxoption removed fromClientConfig. Sandbox offers are no longer toggled via the SDK config.StaticRequirementsChecklistPropstype removed. UseRequirementsChecklistwith the connected API orBaseRequirementsChecklistfor fully custom rendering.
April 23, 2026 — Requirements API, sandbox mode, and participations filtering
Added
Client (@coinlist-co/react, @coinlist-co/react/client)
fetchOfferRequirements(offerId)onCoinListClient— returns requirements grouped by option ID (Record<OfferOptionId, Requirement[]>).fetchRequirementStatuses(offerId)onCoinListClient— returns the authenticated user’s status for each requirement.useCoinListRequirements(offerId)hook for loading requirements and statuses with aLOADING / CONTENT / ERRORstate machine.sandboxoption onClientConfig— whentrue, passessandbox=trueto offers API calls so sandbox offers are included.- New exported types:
Requirement,RequirementId,RequirementType,RequirementStatusValue,RequirementStatusInfo,ParticipationsPaginationParams. - New exported enums/constants:
RequirementVariant,RequirementStatus,ChecklistStatus. - New hook types:
UseCoinListRequirementsResult,LoadRequirementsState,LoadRequirementsReason. fetchAllParticipations(offerId?)andfetchParticipationsPage(params)now accept an optionalofferIdto fetch participations for a specific offer.fetchParticipationsPagetakes the newParticipationsPaginationParamstype (superset ofPaginationParams) which carries theofferIdfield.
Fixed
Components
- Fixed missing dark mode CSS variables in Next.js projects — prebuilt styles now include the full set of design-system tokens so components render correctly under
darkclass orprefers-color-scheme: dark.
April 13, 2026 — Participations API
Added
Client (@coinlist-co/react, @coinlist-co/react/client)
fetchAllParticipations()onCoinListClientfor fetching all participations across pages.fetchParticipationsPage()onCoinListClientfor paginated participation fetching.fetchParticipation()onCoinListClientfor fetching a single participation by id.createParticipation()onCoinListClientfor creating a new participation.- New exported types:
Participation,ParticipationId,ParticipationStatus,CreateParticipationParams,Blockchain,WalletAddress.
April 8, 2026 — Support UI component customization
Added
Client (@coinlist-co/react, @coinlist-co/react/client)
- Added support for passing
classNameandcontainerClassNameto<OffersGrid />,<OfferCard />, and<CoinListSignInCard />.
April 7, 2026 — Publish missing exports
Fixed
Client (@coinlist-co/react, @coinlist-co/react/client)
- Re-export offer, offer detail, and pagination types from the package root and client core entry so you can type against
Offer,OfferDetail, pagination params, and related helpers without reaching into internal modules. - Export
BaseCoinListSignInCardandBaseCoinListSignInCardPropsfrom@coinlist-co/react/clientfor custom sign-in layouts built on the same primitives asCoinListSignInCard.
April 2026 — Offers UI and OAuth helpers
Added
Client (@coinlist-co/react/client)
fetchAllOffers(),fetchOffersPage(), andfetchOfferDetails()onCoinListClientfor fetching offers data.<OffersGrid />— batteries-included component that loads offers and displays them in a responsive grid, with<OfferCard />for each tile.useCoinListOffers()hook for loading offers when you want full control over layout instead of the grid.useCoinListOfferDetails()hook for a single offer’s details.useCompleteCoinListOAuth()hook to run the OAuth redirect callback once on mount.- Optional
authorizationPageUrlon client config so you can pointstartOAuth()at a non-production authorization page.
March 2026 — Initial release
Added
Client (@coinlist-co/react/client)
CoinListProvideranduseCoinList()hook for React context-based SDK initialization.createCoinListClient(config)factory for manual or non-React usage.- OAuth 2.0 with PKCE via
startOAuth()andcompleteOAuth(). getAuthState()andlogout().
Server (@coinlist-co/react/server)
createCoinListServer(config)factory for BFF / Next.js API routes.completeOAuth(),accessToken()(auto-refreshing), andlogout().
Components
CoinListSignInCardandCoinListSignInButtonfor OAuth sign-in flows.RequirementsChecklist/RequirementItem— eligibility checklist with accordion UI.PoweredByCoinListattribution badge.- Prebuilt CSS — no
tailwindcsspeer dependency required.
Infrastructure
- Subpath exports:
@coinlist-co/react,@coinlist-co/react/client,@coinlist-co/react/server. - Requires React 18+.