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.
| Helper | Returns inside data | Source |
|---|---|---|
getPolymarketHomepage() | Categories, topics, carousel and discovery pins together | One Polymarket homepage document |
getPolymarketMainCats() | Main navigation labels, slugs and paths | Shared homepage snapshot |
getPolymarketHotTopics() | Curated titles, volume strings and destination paths | Shared homepage snapshot |
getPolymarketTrendingCarousels() | Ordered slides with event, type, shortName and slot | Website carousel endpoint |
getPolymarketEvents(query) | Active event items and nextCursor | Gamma keyset feed |
getPolymarketEvent(slug) | One event with normalized markets/outcomes | Gamma event detail |
searchPolymarketEvents(query) | Up to 20 matching events and hasMore | Gamma 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
| Surface | Direct source | Contract |
|---|---|---|
| Active event feed | GET https://gamma-api.polymarket.com/events/keyset | Public Gamma API; cursor pagination |
| Event detail | GET https://gamma-api.polymarket.com/events/slug/{slug} | Public Gamma API |
| Tag catalogue | GET https://gamma-api.polymarket.com/tags | Public metadata; not homepage navigation |
| Resolve a tag | GET https://gamma-api.polymarket.com/tags/slug/{slug} | Public Gamma API |
| Search | GET https://gamma-api.polymarket.com/public-search?q=... | Public Gamma API |
| Carousel alone | GET https://polymarket.com/api/homepage/carousel | Website interface, not a guaranteed Gamma contract |
| Main navigation, hot topics and pins | GET 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.