Prerequisites
PassageProviderwith workinggetAccessToken(session established on your backend)- User signed in: your
getAccessTokenreturns a non-null access token after OAuth (see Set up OAuth authentication)
Recommended: OffersGridContainer
OffersGridContainer uses useOffers internally and renders a responsive grid of offer cards with loading, error, and empty states. It also loads CoinList’s public token registry, so each card shows the registry’s name and logo for the offer’s token where the registry lists it, and the offer’s own fields otherwise. The grid waits for both loads before it shows the cards, so each card paints once with its final artwork. If the registry fails to load, the cards fall back to the offer’s own fields instead of showing an error.
The grid lists every offer type, including Centrifuge vaults (centrifuge::vault). An offer that runs no sale window, an Ondo swap or a Centrifuge vault, renders with no tagline and no countdown.
maxColumns- upper bound for the grid when space allows:1or2(default2; larger values are clamped to2)loading,error,emptyState- replace default slots with your own React nodesonOfferClick- when set, cards become interactive; omit for read-only listsdata-OffersGridData,{ offers?, tokens? }pre-fetched on your server for SSR:offersfrompassage.offers.list(),tokensfrompassage.tokens.list()onPassageServer. Each half skips its own client-side request; pass both for a grid that makes no request of its own.enabled- defaulttrue. Passfalseto mount the grid without fetching until it is needed.className,containerClassName- class names for the grid element and its outer wrapper
useOffers and useTokenRegistry and render the public OfferSaleCard (or OfferAssetCard) yourself:
Server-side pre-fetch
The offer hooks and containers acceptdata. Pass it and the component uses that data as-is and skips the initial client-side fetch - the user sees offers in the first paint instead of a spinner.
This is the SSR path. Your Server Component calls PassageServer, and the browser renders with the answer already in hand:
Use a read-only session store in Server Components: they can read cookies but not write them, so a writable store would silently consume a refresh token. See read-only session stores.
useOffers({ data }), useOfferDetails(id, { data }), useTokenRegistry({ data })) and on the other data-backed containers, including RequirementsChecklistContainer. useOffers and useOfferDetails have no refetch(), so a seed replaces their fetch entirely. Where a hook exposes refetch(), the seed answers the initial render only - an explicit refetch still goes to the network.
passage.offers.list() on the server also takes an optional app-level token from passage.auth.clientCredentials(), so a public catalog page can render with no user session at all:
Going lower
Open these when you need custom layouts or non-React code.Build your own UI with hooks (L2)
Build your own UI with hooks (L2)
Use Pass
useOffers from @passage-cl/react when you want full control over markup but still prefer reactive loading state.The hook exposes offersState:LOADING- provider not ready or request in flightERROR-reasonisnot-authenticatedwhen no user is signed in (NotAuthenticatedError), andgeneric-errorfor any other failureCONTENT-offersis every page of offers merged (same data aspassage.offers.list())
useOffers has no DISABLED state: with enabled: false and no data, it stays LOADING. It also has no refetch().data if you have server-fetched offers to hydrate from: useOffers({ data }). Pass enabled: false to skip the fetch while the list is not needed yet.Drive it yourself with the client (L1)
Drive it yourself with the client (L1)
Inside React you usually reach these through For imperative, non-hook usage, create a
usePassage():passage.offers.list()- iterates every paginated page and returns all offers (throwsNotAuthenticatedErrorif the user is not logged in)passage.offers.listPage(params)- one page of API results when you implement your own pagination UI. Pass the previous response’sstartingAfterasafterto advance.
PassageClient with createPassageClient from @passage-cl/react. Server code (Next.js Route Handlers, Server Components) should use the equivalent passage.offers.list() on PassageServer from @passage-cl/react/server instead - on the server it also takes an optional app-level token from passage.auth.clientCredentials(), so it can run with no user session.Raw HTTP
Raw HTTP
The SDK calls the same API documented in the API reference. Use raw HTTP only if you are not using React, or you need fields or flows the SDK does not expose yet. You still need a valid Bearer access token from your OAuth session.
Token display metadata
Offers carry chain and address pairs inoffer.tokens, but not the name, symbol, decimals or logo you need to render them. Those come from CoinList’s public token registry through passage.tokens.
TokenMetadata carries identifier, name, symbol, decimals, logo and logoDark. logo is a union: VECTOR is a single SVG url, while RASTER carries an original plus webp variants at 32 to 512px - pick the smallest variant that covers your render size, or original if none does.
passage.tokens is the one unauthenticated namespace: no method needs a logged-in user, and reads go to the registry’s CDN rather than the CoinList API. It works with no session at all, and with a read-only server session store.
passage.tokens.get(token)returns metadata for one token, ornullwhen the registry does not list it. The registry is curated, sonullis a normal answer: fall back to your own display defaults rather than treating it as an error.passage.tokens.list(chain?)returns every token the registry lists, in one request: one chain’s snapshot when you pass achain, or the complete snapshot when you omit it. The complete snapshot leaves out tokens on chains the SDK does not model. Prefer it over callinggetin a loop when rendering a catalog - two hundred tokens is still a single snapshot download. A missing snapshot throws rather than returning[], because its absence is a deployment problem and not an empty catalog.
useTokenMetadata takes enabled and data like the other data hooks, and exposes refetch(). A data of null is a valid seed meaning “not in the registry”, and is distinct from leaving data undefined. Its state is DISABLED, LOADING, ERROR (always generic-error, because the registry is public) or CONTENT. With enabled: false and no answer in hand it reports DISABLED; a seeded or already-fetched answer survives being switched off. For a whole catalog, useTokenRegistry loads every token in one request and has the same states.
Next step
Display offer details
Load the full
OfferDetail for an id from the grid, which is what checkout takes.