Skip to main content
This changelog covers the @coinlist-co/react SDK.
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. 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.
Code that builds an 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.
The logged-out error changes. These reads no longer throw 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.
SUPERSTATE_SWAP_CONTRACT_ADDRESS_SEPOLIA now points at a different contract. The old Sepolia deployment cannot serve the contract’s current ABI. If you hard-coded the previous address instead of importing the constant, switch to the constant.

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.
A host that drives 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:
A chain you leave out falls back to CoinList’s proxy (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 viem Chain definition the SDK uses for an EthereumChain.
  • isSwapAvailable, SWAP_LEVEL and PauseFlags, for the Superstate status above.
  • Logging: a new RPC LogScope, and an rpc arm on LogCause with reason: 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 above debug.

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, useParticipations
  • useOffersGridViewModel and OffersGridContainer
  • useRequirementsChecklistViewModel and RequirementsChecklistContainer

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

  • CheckoutContainer mounts 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"].symbol and side now run only when an ondo::swap offer is showing, so they no longer have to handle Superstate and token-sale offers. enabled is 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_progress stays a required step until the user signs it. The loading, error and unsupported-jurisdiction states were redesigned.

Fixed

  • Superstate’s confirmed output amount. The Swapped event ABI declared two non-indexed words where the contract emits three. As a result, decodeSwappedOutputAmount read the protocol fee as the amount received, and executeSwap reported that fee as the confirmed outputAmount. It now decodes the amount received.
  • Amounts with a leading or trailing decimal point. parseBlockchainAmount now accepts .25 (read as 0.25) and 25. (read as 25). A bare . is still rejected.
  • Checkout loading skeletons. The price skeletons in AssetSummaryRow and 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 /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.
The other two entry points, @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 one ondo::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.
The React surface forked with it. Every buy screen’s component and viewmodel is now 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.
ondoSwapContractAddress is deleted rather than deprecated on purpose. A constant that keeps returning a stale address fails silently against the wrong contract, where a removed export is a compile error.

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.
Migration: add 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.
side is read once, at mount. The flow holds an approval granted for whichever coin that side spends, so a resolver that starts answering differently mid-flow is ignored. Remount with a key covering the offer, the chain and the side.

The Ondo checkout state is a union

OndoBuyCheckoutUiState - and the new OndoSellCheckoutUiState - are now discriminated unions rather than product types:
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 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, and level(). Implement it over your own sink in four methods.
  • Two shipped implementations, one per environment: pinoClientLogger from @coinlist-co/react and pinoServerLogger from @coinlist-co/react/server.
  • Every line is structured: a constant msg naming what happened, a scope (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 expensive debug line costs nothing at info and nothing at all with no logger supplied.
'debug' is unredacted. It reports request and response bodies, full URLs, headers and operation parameters verbatim - bearer tokens, KYC answers, tax-document fields, wallet signatures. info, warn and error are redacted by construction: those levels accept scalar fields only, so a body or a DTO cannot be put on one. Built with isDev: false, 'debug' does not typecheck. The SDK’s advice is to leave logger undefined in production entirely.
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 over get() in a loop when rendering a catalog. Unlike get, a missing snapshot throws rather than answering [].
  • Offer.tokens and OfferDetail.tokens, the (chain, address) entries an offer’s assets live at, keyed by role.
  • OffersAssetGrid, a responsive browse grid of OfferAssetCard leaves. Pure - no fetch, no provider - and distinct from OffersGridView, which lays out sale cards.
  • useErc20Balance, the address-keyed counterpart to useErc20TokenBalances, 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.
  • OndoCheckoutError and OndoCheckoutErrorReason, the flow-level error surface both Ondo products share.
  • Release candidates on npm. Every merge to main now publishes <next-patch>-rc.<short-sha> under the rc dist-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.logoUrl and 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 0 balance for a token the flow would never spend and refused on Confirm, as though the amount were the problem.
  • useTokenMetadata moved 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-react is 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.fromDto runs inside the paginated response’s map, 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 USDC with a max affordance, matching the Superstate checkout.

Removed

  • identity_verified, proof_of_address and source_of_funds are gone from RequirementType. They were left over from an abandoned modular-KYC effort and the backend no longer returns them, so a switch over 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 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.ClientServerMigration: 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.
The reads are free to poll while the user sizes an order; 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

A BCP-47 tag applied to number, date and currency formatting in every SDK component in the tree. Defaults to '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') and Chain, the union of it with EthereumChain, both with validating constructors that throw ValidationError on an unknown value. Solana offers are not executable yet - this is the type surface an offer’s tokens are parsed against, ahead of the SolanaWallet seam.
  • EvmSigner, the signing-only half of EvmWallet (address + signMessage). The external-wallet ownership proof needs nothing more, so a host can satisfy that flow without implementing on-chain capabilities.
  • tokensBaseUrl on ClientConfig and ServerConfig, to point coinlist.tokens at a non-production registry.
  • CoinListSignInButton, the button on its own for hosts that do not want the card.
  • VerificationOverlay, and every requirements viewmodel and .types module.
  • useOndoPrice, useOndoTradingStatus, useOndoSwapTransaction for composing your own Ondo viewmodel.
  • Formatting and presentation helpers: useCountdown, and the asset-icon, date and duration formatters.

Changed

  • Requirements, offers and wallets are the same namespace on both sides. coinlist.offers, coinlist.requirements and coinlist.wallets expose identical method names on CoinListClient and CoinListServer; the server adds an optional trailing clientCreds on the reads that can run without a user session, and the client adds requirements.handle.
  • Ondo reads live under /v1/ondo/swap/* and take a required side. Ondo reports its limits per side, so a trading status fetched without one answers 422 rather than guessing buy.
  • getQuote refuses a request sized by both tokenAmount and notionalValue, throwing ValidationError. TypeScript accepts an object carrying both, and the previous behavior silently dropped notionalValue.
  • Participation, SwapQuote and 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 - use coinlist.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 new tokenSale 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:
It reads the current allowance, submits an ERC-20 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.
Failures are returned as typed, step-tagged errors rather than thrown. The reset-to-zero and the main approval are reported distinctly (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.

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 a success / error result. 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 the Swapped event.
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-ownership
  • connectExternalWallet(offerId, params) → POST /v1/offers/{offer_id}/addresses
  • listOptionAddresses(offerId, offerOptionId) → GET /v1/offers/{offer_id}/addresses
  • removeOptionAddress(offerId, addressId) → DELETE /v1/offers/{offer_id}/addresses/{id}
UI: 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 forms
  • submitDocument(documentType, fields) → POST /v1/documents/{document_type}/submission
  • Components IdentityVerification and TaxDocumentModal; hooks useKycToken and useTaxDocument.
  • handleRequirement now resolves the kyc_approved, identity_verified, proof_of_address, source_of_funds, accreditation, external_wallet, whitelisted_wallet, document, and jurisdiction requirement 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 (with add / sub), parseBlockchainAmount, formatAmount, computeSlip, and the Bps newtype.
  • 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, where CoinListProvider 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)

  • CoinListServer now exposes the full data-fetching surface previously only available on CoinListClient: fetchAllOffers(), fetchOffersPage(), fetchOfferDetails(), fetchAllParticipations(), fetchParticipationsPage(), fetchParticipation(), createParticipation(), fetchOfferRequirements(), and fetchRequirementStatuses(). Use these in Next.js Route Handlers and Server Components without shipping any client bundle.
  • @coinlist-co/react/server now 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 a LOADING / CONTENT / ERROR state machine, optionally filtered by offer. New exported types: UseParticipationsResult, LoadParticipationsState, LoadParticipationsReason.
  • handleRequirement(requirement) on CoinListClient — opens the corresponding CoinList page for completing a requirement in a new tab (/verify-identity, /wallet, etc.). No-op for jurisdiction requirements.
  • contactSupport() on CoinListClient — opens the CoinList support ticket page in a new tab.

Changed

Components

  • RequirementsChecklist: onRequirementAction and onContactSupport props have been renamed to onRequirementActionOverride and onContactSupportOverride. Both now default to CoinListClient#handleRequirement and CoinListClient#contactSupport respectively, so most integrations can omit them entirely. Pass null to disable the corresponding button/link.
  • RequirementItem: onAction and onContactSupport now accept null (in addition to undefined) to suppress rendering of action buttons.

Removed

Client (@coinlist-co/react, @coinlist-co/react/client)

  • sandbox option removed from ClientConfig. Sandbox offers are no longer toggled via the SDK config.
  • StaticRequirementsChecklistProps type removed. Use RequirementsChecklist with the connected API or BaseRequirementsChecklist for fully custom rendering.
April 23, 2026 — Requirements API, sandbox mode, and participations filtering

Added

Client (@coinlist-co/react, @coinlist-co/react/client)

  • fetchOfferRequirements(offerId) on CoinListClient — returns requirements grouped by option ID (Record<OfferOptionId, Requirement[]>).
  • fetchRequirementStatuses(offerId) on CoinListClient — returns the authenticated user’s status for each requirement.
  • useCoinListRequirements(offerId) hook for loading requirements and statuses with a LOADING / CONTENT / ERROR state machine.
  • sandbox option on ClientConfig — when true, passes sandbox=true to 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?) and fetchParticipationsPage(params) now accept an optional offerId to fetch participations for a specific offer. fetchParticipationsPage takes the new ParticipationsPaginationParams type (superset of PaginationParams) which carries the offerId field.

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 dark class or prefers-color-scheme: dark.
April 13, 2026 — Participations API

Added

Client (@coinlist-co/react, @coinlist-co/react/client)

  • fetchAllParticipations() on CoinListClient for fetching all participations across pages.
  • fetchParticipationsPage() on CoinListClient for paginated participation fetching.
  • fetchParticipation() on CoinListClient for fetching a single participation by id.
  • createParticipation() on CoinListClient for 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 className and containerClassName to <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 BaseCoinListSignInCard and BaseCoinListSignInCardProps from @coinlist-co/react/client for custom sign-in layouts built on the same primitives as CoinListSignInCard.
April 2026 — Offers UI and OAuth helpers

Added

Client (@coinlist-co/react/client)

  • fetchAllOffers(), fetchOffersPage(), and fetchOfferDetails() on CoinListClient for 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 authorizationPageUrl on client config so you can point startOAuth() at a non-production authorization page.
March 2026 — Initial release

Added

Client (@coinlist-co/react/client)

  • CoinListProvider and useCoinList() hook for React context-based SDK initialization.
  • createCoinListClient(config) factory for manual or non-React usage.
  • OAuth 2.0 with PKCE via startOAuth() and completeOAuth().
  • getAuthState() and logout().

Server (@coinlist-co/react/server)

  • createCoinListServer(config) factory for BFF / Next.js API routes.
  • completeOAuth(), accessToken() (auto-refreshing), and logout().

Components

  • CoinListSignInCard and CoinListSignInButton for OAuth sign-in flows.
  • RequirementsChecklist / RequirementItem — eligibility checklist with accordion UI.
  • PoweredByCoinList attribution badge.
  • Prebuilt CSS — no tailwindcss peer dependency required.

Infrastructure

  • Subpath exports: @coinlist-co/react, @coinlist-co/react/client, @coinlist-co/react/server.
  • Requires React 18+.