Skip to main content
After OAuth, you can load every sale the current user can see and render them with the SDK - no hand-written HTTP integration for the common case.

Prerequisites

  • PassageProvider with working getAccessToken (session established on your backend)
  • User signed in: your getAccessToken returns a non-null access token after OAuth (see Set up OAuth authentication)
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.
Optional props:
  • maxColumns - upper bound for the grid when space allows: 1 or 2 (default 2; larger values are clamped to 2)
  • loading, error, emptyState - replace default slots with your own React nodes
  • onOfferClick - when set, cards become interactive; omit for read-only lists
  • data - OffersGridData, { offers?, tokens? } pre-fetched on your server for SSR: offers from passage.offers.list(), tokens from passage.tokens.list() on PassageServer. Each half skips its own client-side request; pass both for a grid that makes no request of its own.
  • enabled - default true. Pass false to mount the grid without fetching until it is needed.
  • className, containerClassName - class names for the grid element and its outer wrapper
The grid’s viewmodel and view are internal. For your own layout, such as a carousel, build it from useOffers and useTokenRegistry and render the public OfferSaleCard (or OfferAssetCard) yourself:

Server-side pre-fetch

The offer hooks and containers accept data. 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.
The same prop exists on the hooks (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:
When a user session exists it takes precedence, and the app-level token is only a fallback.

Going lower

Open these when you need custom layouts or non-React code.
Use 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 flight
  • ERROR - reason is not-authenticated when no user is signed in (NotAuthenticatedError), and generic-error for any other failure
  • CONTENT - offers is every page of offers merged (same data as passage.offers.list())
useOffers has no DISABLED state: with enabled: false and no data, it stays LOADING. It also has no refetch().
Pass 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.
Inside React you usually reach these through usePassage():
  • passage.offers.list() - iterates every paginated page and returns all offers (throws NotAuthenticatedError if 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’s startingAfter as after to advance.
For imperative, non-hook usage, create a 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.
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 in offer.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.
A 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, or null when the registry does not list it. The registry is curated, so null is 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 a chain, or the complete snapshot when you omit it. The complete snapshot leaves out tokens on chains the SDK does not model. Prefer it over calling get in 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.