Live charts and order books
Load initial market data and keep it updated with one SDK store.
Use createPolymarketMarketData() from SDK 0.13.0 to load an outcome's
order book and chart history, then maintain its public WebSocket subscription.
You render the state using your existing components. The SDK handles HTTP reads,
stream snapshots and deltas, heartbeats, reconnects, refreshes and cleanup.
This is a public market-data stream directly from Polymarket. It does not use CRISP credentials, a managed account, or a realtime session. The origins configured in the Partner Dashboard apply to CRISP account realtime, not this public stream. Use account realtime separately for balances and orders.
One store per selected outcome
import { createPolymarketMarketData } from '@crisp-trade/partner-sdk/market-data';
const market = createPolymarketMarketData({
tokenId, // String outcome token ID from market discovery.
history: { interval: '1d', fidelity: 5 },
});
const unsubscribe = market.subscribe(() => {
const state = market.getSnapshot();
console.log(state.book.data, state.chart.points, state.connection);
});
market.start();
// On page unmount or before replacing this store with another outcome:
unsubscribe();
market.stop();Construction performs no network requests. start() is idempotent while running;
stop() closes the socket, aborts pending reads and cancels timers. A stopped
store can be started again for the same outcome unless it observed market resolution. Create a new store when changing
the outcome or history range. Unsubscribing a listener does not stop the store;
the owner of the store must call stop().
The entry point is browser-safe and also works in Node.js 24+. Import
/market-data in browser code, not the main server SDK entry point. Keep the
outcome token ID as a string. A Gamma event ID or condition ID is not a token ID.
React hook
'use client';
import { useEffect, useMemo, useSyncExternalStore } from 'react';
import { createPolymarketMarketData } from '@crisp-trade/partner-sdk/market-data';
export function useMarketData(tokenId: string) {
const store = useMemo(() => createPolymarketMarketData({
tokenId,
history: { interval: '1d', fidelity: 5 },
}), [tokenId]);
const state = useSyncExternalStore(
store.subscribe,
store.getSnapshot,
store.getSnapshot,
);
useEffect(() => {
store.start();
return () => store.stop();
}, [store]);
return state;
}Use state.book.data?.bids and asks in your order-book component, and
state.chart.points in your chart. Snapshots are stable between notifications
and deeply immutable. Notification bursts are coalesced over 50ms by default.
One broken listener cannot stop updates for other listeners.
What the state means
| Field | Meaning |
|---|---|
connection | idle, connecting, synchronizing, live, reconnecting, or stopped |
book.data | Current display book or null; bid prices descend and ask prices ascend |
book.source | rest for an HTTP snapshot, stream after a full WebSocket book |
book.stale | Keep the last display if useful, but indicate that it may be outdated |
book.error | Read failure; a failure is not an empty order book |
chart.points | Ordered { timestamp, price, source } display points, bounded by maxPoints |
chart.stale / chart.error | History refresh failed or recovery is in progress |
chart.refreshing | Canonical history is being loaded in the background |
chart.truncated | Earlier points were omitted to keep the configured memory bound |
lastTrade | Latest observed trade price/size and upstream timestamp in milliseconds |
resolved | A market-resolution event was observed; the store stops and retains stale display data |
streamError | The public stream was interrupted and is recovering |
Book prices and sizes remain decimal strings. Chart timestamps are Unix seconds
and prices are numbers for display. updatedAt is a local read/update time, not a
venue execution guarantee. A live connection does not imply that history loaded
successfully; check each resource independently.
Historical samples and the live tail
Points with source: 'history' come from Polymarket's /prices-history API.
Points with source: 'live_trade' are provisional last-trade observations newer
than the most recent historical sample. They update at most once per timestamp
second and do not represent OHLC candles or the venue's historical sampling
method. Use a different style for the provisional tail if your chart needs to
distinguish it; filter to history when displaying canonical historical data only.
The SDK refreshes canonical history every 60 seconds and during reconnects.
New history replaces provisional points it covers. It does not fabricate points
for periods without trades or fill gaps with invented prices. A fixed
startTs/endTs range does not receive live points outside those bounds.
Recovery behavior
The initial HTTP book lets you show a snapshot while the stream connects. The SDK waits for a full book from that WebSocket connection before applying its price-level updates. A delta replaces the quantity at a price; zero removes it. Late HTTP responses cannot overwrite live levels, and old sockets cannot update a new connection. Tick-size changes update book metadata.
After an interruption, the SDK marks the retained display stale, reconnects with bounded exponential backoff and jitter, reloads history, and requests a new full stream book. Heartbeats run every ten seconds. Missing initial snapshots, stalled connections and malformed relevant data trigger recovery. Reads have a ten-second default deadline, including custom loaders that ignore cancellation.
The upstream channel does not provide a durable replay cursor. The SDK cannot promise gap-free trading records or cryptographically verify every book delta. Use this store for display. Trading still requires current CRISP order requirements and authoritative order/fill responses.
Optional backend HTTP loaders
If direct browser HTTP requests are unavailable because of CORS, network policy or your preferred caching architecture, provide loaders for your own backend. The SDK still owns the stream and state lifecycle:
const market = createPolymarketMarketData({
tokenId,
loadBook: async (tokenId, signal) => {
const response = await fetch(`/api/public/book?tokenId=${encodeURIComponent(tokenId)}`, { signal });
if (!response.ok) throw new Error('Book unavailable');
return response.json();
},
loadHistory: async (query, signal) => {
const response = await fetch('/api/public/history', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(query),
signal,
});
if (!response.ok) throw new Error('History unavailable');
return response.json();
},
});These are example routes you implement. Return the unchanged result envelope
from getPolymarketOrderBook(tokenId) or getPolymarketPriceHistory(query) in
@crisp-trade/partner-sdk/discovery. Pass cancellation through to the upstream
read and apply bounded public-data caching as appropriate. No CRISP credentials
are required. Do not attach them to these requests.
Options and boundaries
history: falseruns the book and last-trade stream without chart reads.refreshIntervalMsdefaults to 60,000. It refreshes history and provides an HTTP book fallback while the stream is not synchronized; it does not poll live books.maxPointsdefaults to 2,000 and caps retained chart data.notifyIntervalMsdefaults to 50;timeoutMsdefaults to 10,000.signalstops the lifecycle when aborted.refresh()requests background reads without overlapping an already-running read of the same resource.fetchandcreateSocketallow transport injection and deterministic tests.
Shared application client (SDK 0.13.0)
For multiple outcomes, create one app-scoped client. Construction makes no requests. Matching views share a store and HTTP reads; all views and quote subscriptions share one physical socket. The standalone store above still supports explicit start/stop.
import { createPolymarketMarketDataClient } from '@crisp-trade/partner-sdk/market-data';
const client = createPolymarketMarketDataClient();
const market = client.market({ tokenId, history: false });
const unsubscribe = market.subscribe(() => render(market.getSnapshot()));
// React: memoize market by token/history, then pass its subscribe,
// getSnapshot and getServerSnapshot to useSyncExternalStore.
unsubscribe(); // final consumer releases the store; StrictMode safeSpecialized chart/quote reducers can share this same connection without creating an HTTP book/history store per card:
const quotes = client.subscribe(tokenIds, {
onMessage: frames => applyOwnPriceRules(frames),
onReset: () => markQuotesStale(),
});
quotes.update(nextTokenIds);
quotes.close();Callbacks receive arrays of public upstream frames. Filter multi-token price changes
to the tokens your reducer owns. Keep midpoint-based chart tails distinct from
live_trade samples. History views are keyed by token/range/fidelity; distinct ranges
are not resampled into one another. Use history: false for depth-only views.
The connection owns heartbeat, reference-counted subscriptions and reconnect/backoff.
Late depth subscribers renew only their token subscriptions on the same connection
to obtain a fresh snapshot; deltas are gated until it arrives. Market resolution stops
only that outcome's store. Call client.dispose() only when destroying the owning
application, never when one component unmounts. Optional backend loaders follow the
same unsigned envelope and AbortSignal contract described above.
Underlying sources are https://clob.polymarket.com/book,
https://clob.polymarket.com/prices-history, and
wss://ws-subscriptions-clob.polymarket.com/ws/market. Polymarket availability,
limits and data terms apply. See its market stream documentation.