Take advantage of pre-computed data
Struct’s defining feature is that every derived value (PnL, leaderboards, positions, market metrics, event metrics, tag metrics, condition metrics) is materialised on write, not computed on read. Rows are updated as on-chain events arrive, never on a periodic cycle, so values stay current with the chain. A query that would otherwise scan hundreds of thousands of trades is a single-row lookup. Every pattern below flows from that:- Stream metrics, not trades, when you want aggregates. If your UI shows volume, holders, or trader counts, subscribe to
polymarket_market_metrics(or event/position/tag/condition metrics) instead ofpolymarket_tradesand aggregating yourself. One materialised update per change beats N raw trade messages per change, often by 10-100x. - Use alerts for “wake me when X” logic. If you only care about probability moves above a threshold, a whale trade, or a volume milestone, subscribe to the matching alert instead of a raw stream. Alerts fire only on the crossing, so you stop paying for the messages your code would have filtered out anyway.
- Pace the snapshot streams to your UI.
polymarket_markets_streamandpolymarket_events_streamdeliver fresh snapshots at your choseninterval_ms. Tune the interval to what you actually render (1000ms for a live table, 5000ms for a sidebar). The bill scales linearly with the rate. - Narrow filters as your UI narrows. A subscription without filters on a busy room delivers every upstream event. After a user picks a market or a watchlist shrinks, send a fresh
subscribeto drop everything you no longer care about. Wider filters cost more, full stop.
Stream, don’t poll
Anything you’d refresh more than once a minute should be a websocket subscription, not a REST poll. Server-side filtering means you only pay for the messages you actually consume, and push latency is in single-digit milliseconds.| If you’re polling… | Subscribe to… |
|---|---|
| Markets list every few seconds | polymarket_markets_stream |
| Trader PnL every minute | polymarket_trader_pnl |
| Order book snapshots | polymarket_order_book |
| Trade history for a market | polymarket_trades (filter on condition_ids) |
| Crypto prices for an asset | polymarket_asset_prices |
Pool subscriptions on one connection
A single websocket can subscribe to every room. The 1-credit connection hold is per-connect, so:- One socket per process is usually enough for backend workloads.
- One socket per logged-in user is enough for in-app alerts and per-user dashboards.
- Avoid opening, closing, and reopening sockets in a tight loop. Every reconnect re-incurs the hold.
Filter at subscribe time
Every room takes filter fields on subscribe. Server-side filtering is free; once a message arrives at your client you’ve already been billed for it.subscribe message; the new filters replace the previous set.
Reconnect with exponential backoff
Subscriptions are bound to the connection. When the socket drops, you must reconnect and resubscribe.- Use exponential backoff with jitter. Start around 500ms, double on each attempt, cap at 30s.
- Never retry on auth failures (close codes
1008and4001). Fix credentials first. - Reset the backoff counter on a successful connect.
Use JWT public keys in the browser
sk_live_* keys grant full org access. Never ship them in a frontend bundle, mobile app, or anywhere a user can read them.
For client-side calls, mint a pk_jwt_* public key in the dashboard, point it at your auth provider’s JWKS URL, and pass each user’s JWT alongside the key. The key is safe to embed in your bundle, and per-user rate limits keyed on the JWT sub claim prevent one user from burning your whole quota. See Authentication.
Handle pagination cleanly
List endpoints are cursor-paginated withpagination_key. Iterate until pagination.has_more is false.
Cache long-lived data
Market metadata (slug, image, outcomes), event metadata, and tag lists rarely change. Cache them in your application layer for at least a few minutes; you can refresh on a websocket event or a TTL whichever you prefer. Volatile values (price, PnL, positions, volume) should not be cached; subscribe to the matching room instead.Pick the right surface for the access pattern
Most cost regressions come from using the wrong product, not the wrong filter. Match the surface to how often you need the data and how you react to it.| You need to… | Use | Why |
|---|---|---|
| Read a single value on demand | REST | 1 credit, single-row lookup. |
| Render a live view of changing values | WS metrics rooms | Pre-computed deltas, no client-side aggregation. |
| React to a threshold crossing | Alerts or webhooks | Fires only on the event you care about. |
| Backfill or paginate history | REST with pagination_key | Cursor pagination, batch-friendly. |
| Receive an event when you’re offline | Webhooks | Queued and retried server-side; no socket required. |