Skip to main content
In a hurry? Skip straight to Set up OAuth authentication and come back when something surprises you. Nothing here is a prerequisite - it is the map, not the road.
The SDK is small in ideas and large in surface. Learn the five patterns on this page and the rest of the documentation stops being a list of APIs to memorise: you will be able to guess a method you have not read, and you will know which rung to reach for before you look anything up.

The same feature at three levels

Every feature is available at several levels. Here is one feature - listing offers - at all three:
All three do the same work. They differ only in how much of it you take over. Start at L3 and drop a rung when the one above cannot give you what you need. Two things follow from this that are easy to miss:
  • You pick a rung per feature, not per application. Rendering <CheckoutContainer> while driving coinlist.offers.list() yourself is normal, not a compromise. The rungs are built from each other, so mixing them cannot desynchronise anything.
  • A feature may stop at a rung, but never skips one. CoinList token sales have a namespace and a hook and no component yet. That absence is deliberate and readable: it means the flow is not drop-in yet, so its UI is yours to render.
L2 and L3 are browser-only, because a backend renders nothing. L1 is the only level present everywhere, which is why coinlist.offers.list() reads identically on your server and in your React tree. See the grid in the quickstart.

Namespaces

The entire L1 surface is namespaces. coinlist.init() is the only top-level member on the browser client, because it is lifecycle rather than domain; CoinListServer has none at all. So every operation you will ever want is coinlist.{something}.{verb}(), and finding it means picking the right noun. Two naming rules tell you which noun:
  • CoinList’s own capabilities are named after what they do: offers, requirements, wallets, auth, erc20, tokens, support.
  • A third party’s namespace is named after the provider: superstate, ondo. Not after what it does. A second provider ships its own swap, so coinlist.swap could only ever have meant one of them - which is exactly why it was renamed in v0.11.0.
Methods share one vocabulary, with the namespace’s own noun dropped, so offers.list() rather than offers.listOffers(): Any method taking more than one argument takes a single params object. Single-argument methods take the value directly. You never have to remember an argument order. API-backed methods throw NotAuthenticatedError when there is no session. The one exception is coinlist.tokens, which is public: it reads a CDN, needs no login, and works with a read-only server session store.

Package overview

The full table of namespaces and their methods, for looking one up.

Components

Every component you render is a container: CheckoutContainer, OffersGridContainer, RequirementsChecklistContainer. You hand it input and config, and it owns everything else. Three habits will keep you out of trouble:
  • Prefer the container that decides for you. <CheckoutContainer> routes on offer.type so a new provider is a version bump rather than your problem. Reaching past it for <OndoBuyCheckoutContainer> means you have taken on that routing.
  • Optional onX callbacks are overrides, not wiring. They default to something sensible - RequirementsChecklistContainer’s action button already calls coinlist.requirements.handle. Omit them until a default is wrong for you.
  • You never need a style provider. Every exported component wraps its own output in a style scope, so it renders fully styled wherever you put it.

Hooks

State is a machine, not a pair of booleans. Loading, error and content are one discriminated union with machine-readable reason codes:
Switch on it exhaustively and assign the default branch to const exhaustive: never = state. A state added in a future release then becomes a compile error rather than a blank screen your users find first. The field is named after what it holds - offersState, requirementsState - except on viewmodels, which return { state, onEvent }. Two options recur, and both exist because of how React works rather than as conveniences:
  • data is how server rendering works. Pass data your backend already fetched with CoinListServer and the hook uses it as-is, skipping the initial client fetch. Where a refetch() exists it still goes to the network: the seed answers the first render only. Worked example.
  • enabled is how you turn a hook off. React forbids calling a hook conditionally, so a hook you cannot skip must instead go quiet: enabled: false means no requests, no timers, and a benign state rather than a permanent LOADING. This is what lets CheckoutContainer call every provider’s viewmodel on every render and enable only the matching one. Hooks that poll or drive a flow take it; the plain catalogue reads take data only.

Flows return, they don’t throw

Anything that drives a wallet - coinlist.superstate.execute, coinlist.ondo.prepareSwap, coinlist.tokenSale.execute - is a flow, and a flow is total. It does not throw. It returns a result tagged with the step that failed:
That is the difference to internalise: wrap namespace calls in try/catch, and branch on a flow’s result. Every step is documented per provider, so you can write real copy for a rejected signature instead of one generic message. Flows also report progress through onProgress, a callback of named phases, so you can tell a user which wallet popup they are looking at. Let your wallet library’s own errors propagate out of your EvmWallet implementation. The SDK catches and classifies them into a typed WalletError, so you never parse a wallet error string. See Wallets.

Next step

Set up OAuth authentication

Add CoinListProvider, a sign-in surface, callback handling, and server routes.