CRISP partners

Build the Market Discovery like Polymarket does

Build category navigation, curated carousels, hot topics and a paginated event feed with the public discovery SDK.

A discovery page combines several independent surfaces: category navigation, curated carousel slides, hot-topic links and a browsable event feed. A list of markets sorted by volume is useful, but it does not reproduce the curated homepage. Keep those surfaces separate so each can load and fail independently.

SDK 0.13.0 adds public Polymarket discovery helpers. They call Polymarket directly from your backend, without a CRISP key. Trading, managed accounts and balances continue through your authenticated CRISP client.

Start with two reads

Install the SDK 0.13.0 archive on Node.js 24+, then load the combined homepage and first feed page:

import {
  getPolymarketHomepage,
  getPolymarketEvents,
} from '@crisp-trade/partner-sdk/discovery';

// Run in your backend loader, not in a browser with API credentials.
const [homepage, feed] = await Promise.allSettled([
  getPolymarketHomepage(),
  getPolymarketEvents({ limit: 20 }),
]);

const discovery = {
  homepage: homepage.status === 'fulfilled' ? homepage.value : null,
  feed: feed.status === 'fulfilled' ? feed.value : null,
};
// Return this public display data through your own server route.
// null here means that surface failed; it does not mean there are no markets.

The homepage result includes mainCats, hotTopics, trendingCarousels and pins. Render those above the event grid. Keep the feed visible if homepage curation is unavailable; retain the last valid display during background refresh. Promise.allSettled prevents a failed optional carousel from removing the feed.

Which helper to use

These are named exports, also available as methods on a separate PolymarketDiscoveryClient. They are not methods on CrispClient.

HelperReturns inside dataSource
getPolymarketHomepage()Categories, topics, carousel and discovery pins togetherOne Polymarket homepage document
getPolymarketMainCats()Main navigation labels, slugs and pathsShared homepage snapshot
getPolymarketHotTopics()Curated titles, volume strings and destination pathsShared homepage snapshot
getPolymarketTrendingCarousels()Ordered slides with event, type, shortName and slotWebsite carousel endpoint
getPolymarketEvents(query)Active event items and nextCursorGamma keyset feed
getPolymarketEvent(slug)One event with normalized markets/outcomesGamma event detail
searchPolymarketEvents(query)Up to 20 matching events and hasMoreGamma public search

Every result has { data, source, fetchedAt, stale }. fetchedAt is when the SDK fetched that snapshot, not the venue's last trade time. stale: true means an older homepage snapshot was used after a failed refresh. These reads do not return CRISP request IDs or authenticate a user.

For a full homepage, use getPolymarketHomepage() rather than requesting its parts and the dedicated carousel separately. The combined helper extracts all surfaces from one document. Individual main-category and hot-topic calls share that document through the same client cache.

1. Build the navigation

import { getPolymarketMainCats } from '@crisp-trade/partner-sdk/discovery';
const { data: categories } = await getPolymarketMainCats();
// Each item: { label, slug, path }. Use path as the navigation key.

This reads current website navigation, preserving upstream order instead of shipping a frozen category list. The navigation includes special surfaces such as Trending, New, Sports Live and potentially products your app does not support. Filter or map entries to routes you actually implement. In particular, a website link to another product does not make that product available through CRISP.

A navigation slug is not universally a Gamma tag slug. Politics can use tagSlug: 'politics'; Sports Live requires schedule/live logic; New and Breaking are distinct discovery experiences. Do not feed every navigation path directly into a tag filter. Gamma's tag catalogue is metadata, not the ordered main menu.

2. Render curated slides and hot topics

import {
  getPolymarketTrendingCarousels,
  getPolymarketHotTopics,
} from '@crisp-trade/partner-sdk/discovery';

const slides = await getPolymarketTrendingCarousels();
const topics = await getPolymarketHotTopics();

Slides retain upstream order and include a normalized event, visual type, shortName and slot. The slot is open upstream JSON: it may contain news, comments or sports context. Render only slot shapes your UI supports; a simple first version can show event title, image and outcomes and omit the slot. Never inject upstream text as HTML, and validate links in open slot data.

Hot topics are separately curated links, not the most popular event titles. Their paths may point to /predictions/... or /search?_q=.... Map those intentionally into your search/topic experience, or use a clearly identified Polymarket link. They are not necessarily event slugs or tag IDs.

For the combined homepage, a section that cannot be read is null; a present empty collection is []. Individual category/topic helpers throw a schema error when their section is unavailable. Do not replace a missing hot-topic rail with invented topics labelled as Polymarket curation.

3. Add the active event feed

import { getPolymarketEvents } from '@crisp-trade/partner-sdk/discovery';

const first = await getPolymarketEvents({
  tagSlug: 'politics', // Omit for the broad active feed.
  order: 'volume24hr',
  limit: 20,
});

const next = first.data.nextCursor
  ? await getPolymarketEvents({
      tagSlug: 'politics',
      order: 'volume24hr',
      limit: 20,
      cursor: first.data.nextCursor,
    })
  : null;

The helper fixes active=true and closed=false, accepts limits of 1–100, and supports volume24hr, volume, startDate and endDate ordering. Ascending is false by default; set it explicitly for chronological views. includeChildren can include descendant tags where supported by Gamma.

Keep the same filters while advancing the opaque cursor. Clear pagination when a user changes category or sort, deduplicate appended cards by event ID, and ignore old responses after a route change. Stop when nextCursor is null; the helper reads one page and never crawls the whole catalogue automatically.

An event groups one or more markets. Display an event card, then select one of its markets and outcomes for the trading ticket. This avoids flattening a multi-market event into unrelated duplicate cards.

4. Use pins deliberately

The combined homepage includes source-selected featured, sports and btc5m pin arrays. They are separate from the volume-ranked feed. A simple product may show them as their own row. If inserting them into the feed, preserve source order and remove duplicate event IDs.

Fast-changing crypto windows and sports schedules need additional eligibility and refresh logic. A cached BTC pin may describe a window that has ended, and a sports pin may lack detailed metadata. Fetch event detail when opening a card and check current trading requirements before allowing submission. These helpers provide discovery inputs, not every specialized sports, crypto or editorial rule used by the Polymarket frontend.

5. Search, open an event and connect to trading

import {
  searchPolymarketEvents,
  getPolymarketEvent,
} from '@crisp-trade/partner-sdk/discovery';

const matches = await searchPolymarketEvents('bitcoin');
const firstMatch = matches.data.items[0];
if (firstMatch) {
  const detail = await getPolymarketEvent(firstMatch.slug);
  console.log(detail.data.markets);
}

Search is a bounded first-page helper. hasMore reports whether additional upstream results exist; it does not fetch them. For a full search experience, use the documented public-search pagination directly. Debounce the input and cancel outdated requests.

Normalized markets include both Gamma id and conditionId. CRISP trading uses conditionId as marketId, plus the selected outcome's string tokenId. Do not submit the Gamma event ID or numeric market ID as a trading market ID. Keep token IDs as strings; JavaScript numbers cannot safely represent them. Missing fields remain null. Market prices are display strings, not executable quotes or spendable balances.

After your user selects an outcome, follow the trading guide with your authenticated CrispClient, current order requirements and a persisted intent. Discovery never authorizes an order and sends no money.

Direct Polymarket sources

SurfaceDirect sourceContract
Active event feedGET https://gamma-api.polymarket.com/events/keysetPublic Gamma API; cursor pagination
Event detailGET https://gamma-api.polymarket.com/events/slug/{slug}Public Gamma API
Tag catalogueGET https://gamma-api.polymarket.com/tagsPublic metadata; not homepage navigation
Resolve a tagGET https://gamma-api.polymarket.com/tags/slug/{slug}Public Gamma API
SearchGET https://gamma-api.polymarket.com/public-search?q=...Public Gamma API
Carousel aloneGET https://polymarket.com/api/homepage/carouselWebsite interface, not a guaranteed Gamma contract
Main navigation, hot topics and pinsGET https://polymarket.com/Website HTML and embedded page data

The combined helper parses serialized JSON from the homepage without executing its scripts. There is no invented /hot-topics or /main-categories API route. Website markup and page data can change independently of the public API; handle unavailability and pin SDK upgrades. The SDK does not promise exact frontend parity indefinitely.

Read Polymarket's maintained market-discovery documentation and API reference for upstream semantics. These requests bypass CRISP ingress: Polymarket's limits, availability and usage terms apply separately from your CRISP allocation.

Caching, errors and cancellation

import { PolymarketDiscoveryClient } from '@crisp-trade/partner-sdk/discovery';

// Reuse one instance in your server process. No origin or credential options.
const discovery = new PolymarketDiscoveryClient({
  timeoutMs: 8000,
  cacheMs: 30000,
  staleMs: 300000,
});
const controller = new AbortController();
const result = await discovery.getPolymarketHomepage({ signal: controller.signal });
console.log(result.fetchedAt, result.stale);

Standalone functions share a default client. Homepage loads without a caller signal share in-flight work, cache complete snapshots for 30 seconds, and may serve explicitly stale snapshots up to five minutes total age after a failure. Caller-cancelled work never falls back to stale data. Requests with a signal are isolated so cancelling one user does not abort another user's load. This is an in-process cache, not a shared cache across serverless instances.

The feed, search, event-detail and dedicated-carousel reads are uncached. Add bounded caching in your application appropriate to each surface; avoid fetching all these sources separately for every card. Never cache account balances in a shared public cache.

PolymarketDiscoveryError exposes code (http, transport, schema, or too_large), nullable HTTP status and retryAfterMs. There are no automatic HTTP retries. Respect upstream Retry-After on 429s and show an unavailable or stale state rather than an empty success. Requests default to an eight-second timeout, reject redirects, omit credentials and limit each response to 4 MiB. A custom fetch can be supplied for tests; never attach CRISP credentials to it.

Run discovery on your backend for predictable website access and caching. Browser CORS support is not guaranteed. Review market data and branding for display, image and attribution considerations.

For an integrated snapshot-to-stream workflow, use Live charts and order books. The SDK handles initial loading, streaming, reconnects and cleanup; your application renders the data.

Charts and order books

SDK 0.13.0 includes unsigned CLOB reads alongside the discovery helpers:

import { getPolymarketOrderBook, getPolymarketPriceHistory } from '@crisp-trade/partner-sdk/discovery';

// Use the selected outcome's string tokenId, not conditionId or a Gamma ID.
const book = await getPolymarketOrderBook(tokenId);
const chart = await getPolymarketPriceHistory({ tokenId, interval: '1d', fidelity: 5 });

getPolymarketOrderBook calls GET https://clob.polymarket.com/book?token_id=.... It returns conditionId, tokenId, the upstream timestamp, bid/ask levels, and available tick-size, minimum-size, last-trade and negative-risk metadata. Prices and sizes remain decimal strings. Upstream level order is preserved; do not assume the first item is the best price without checking or sorting.

getPolymarketPriceHistory calls GET https://clob.polymarket.com/prices-history?market=.... The upstream market parameter takes an outcome token ID. Results contain history: [{ timestamp, price }], with timestamps in Unix seconds and numeric prices for chart display. fidelity is the sampling resolution in minutes. Supply interval (default 1d; also 1h, 6h, 1w, 1m, max, all) or both startTs and endTs in Unix seconds, never both approaches together.

Both helpers return { data, source, fetchedAt, stale }, use the existing timeout, cancellation and error handling, and send no CRISP credentials. They are uncached and perform one request without automatic retries. Empty arrays are valid empty data; HTTP errors (including a missing book) and malformed responses throw. Polymarket's limits apply directly. Books and historical prices are display data, not executable quotes or guarantees that an order will be accepted.

CRISP Partner Documentation