Assymetrix Launches the Deepest Independent Prediction Market Data APIs
Read more
Read more
Assymetrix Launches the Deepest Independent Prediction Market Data APIs
Read more
Read more
Node.js Prediction Market API: Fetch, Stream and Normalize
Node.js Prediction Market API: Fetch, Stream and Normalize
Node.js Prediction Market API: Fetch, Stream and Normalize
Build a Node.js client for prediction market data: fetch markets and prices, handle auth and rate limits, stream updates, and normalize Polymarket, Kalshi and Limitless.

Node.js Prediction Market API: Fetch, Stream and Normalize
A Node.js prediction market API integration comes down to four jobs: pull markets and outcomes, read prices and order books, stream changes, and map every venue’s identifiers onto one schema. Node 18 and later ships fetch and a stable event loop, so a single process can poll REST endpoints and hold WebSocket connections without extra frameworks. The hard part is not the HTTP call. It is that Polymarket, Kalshi and Limitless each describe the same real-world event differently, authenticate differently and throttle differently.
This guide walks through a working structure for a Node.js client: what data to request, how to handle authentication, pagination and rate limits, how to stream, how to normalize across venues, and what to check before you trust the feed.
Assymetrixdata.assymetrix.comUnify prediction market dataAssymetrix brings Polymarket, Kalshi, and Limitless data together through one API for developers building Node.js integrations.Explore the Data API
TL;DR
Separate data access from order execution. Public market data is mostly unauthenticated reads. Trading needs signed requests and keys you must protect.
Use REST for snapshots and backfills, WebSocket for live changes, and reconcile the two with sequence or timestamp checks.
Venue identifiers do not match. Store a canonical event ID in your own database and map each venue’s market ID, ticker or token ID to it.
Build rate-limit handling (backoff, queues, caching) before you scale polling, not after your first 429.
A unified API removes three integrations and three schema maps. A direct venue integration gives you the shortest path to that venue’s order entry.
Table of Contents
What a prediction market API returns
Project setup
Retrieving markets, prices and trades
Authentication, identifiers and rate limits
Streaming real-time updates in Node.js
Normalizing Polymarket, Kalshi and Limitless
Our perspective: why the data layer should be venue-neutral
Unified API or direct venue integrations
Reliability and access checks before you ship
Where to run it
Start with the Assymetrix Data API
Sources
What a prediction market API returns
A prediction market API gives programmatic access to prices, markets and, on venues that allow it, trading. For research and automation, the useful data falls into six groups:
Data type | What it holds | Typical use |
|---|---|---|
Events and markets | Titles, rules, close times, outcome lists, status | Discovery, filtering, universe selection |
Prices and quotes | Last price, bid, ask, midpoint | Signals, dashboards, alerts |
Order books | Depth by price level | Liquidity and slippage estimates |
Trades | Time, price, size, side, sometimes wallet or account fingerprint | Flow analysis, volume, wallet tracking |
Settlements | Resolution outcome and time | Labels for backtests and calibration |
Lifecycle events | Created, opened, paused, resolved | Pipeline triggers, data quality checks |
Settlements matter more than most developers expect. Without resolved outcomes you cannot score a model, measure calibration or compute realized profit for any trader you track. Store them from day one, with the resolution timestamp, because venues sometimes dispute or amend outcomes.
Which signals are worth storing
If you plan to research trader behavior, not just prices, capture more than closing quotes. An academic study of trading success on a prediction-style market used orders, order modifications, executed trades, anonymous trader IDs and five-minute top-of-book quotes, then measured profitability, exposure and holding time. The same study classified traders by maximum exposure and by whether they repeatedly opened and closed positions, and found trading efficiency was more indicative of success than information models or funding. That is a useful shopping list for a schema: every trade with a stable trader key, timestamps precise enough to order events, and enough book snapshots to reconstruct the spread when a trade happened.
Project setup
Start with a small, boring project. You want typed responses, a retry layer and a place to put credentials that is not your repository.
mkdir pm-client && cd pm-client npm init -y npm install ws zod dotenv npm install -D typescript tsx @types/node @types/ws
mkdir pm-client && cd pm-client npm init -y npm install ws zod dotenv npm install -D typescript tsx @types/node @types/ws
ws handles WebSockets. zod validates responses at the boundary, which saves you when a venue adds a field or changes a type. Keep API keys in environment variables and load them with dotenv in development only.
A minimal fetch wrapper with retries
Every venue integration needs the same plumbing. Write it once.
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); export async function getJson<T>(url: string, init: RequestInit = {}, tries = 5): Promise<T> { for (let attempt = 0; attempt < tries; attempt++) { const res = await fetch(url, { ...init, signal: AbortSignal.timeout(10_000) }); if (res.ok) return (await res.json()) as T; const retryable = res.status === 429 || res.status >= 500; if (!retryable || attempt === tries - 1) { throw new Error(`${res.status} ${res.statusText} for ${url}
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); export async function getJson<T>(url: string, init: RequestInit = {}, tries = 5): Promise<T> { for (let attempt = 0; attempt < tries; attempt++) { const res = await fetch(url, { ...init, signal: AbortSignal.timeout(10_000) }); if (res.ok) return (await res.json()) as T; const retryable = res.status === 429 || res.status >= 500; if (!retryable || attempt === tries - 1) { throw new Error(`${res.status} ${res.statusText} for ${url}
Three details earn their place here: a hard timeout so a stalled socket cannot freeze your loop, respect for a Retry-After header when the server sends one, and jitter so parallel workers do not retry in lockstep.
Retrieving markets, prices and trades
The shape of the calls differs by venue, but the flow is the same: list events, pick markets, read prices, then pull trades.

Listing markets with pagination
Most venues return markets in pages. Some use a cursor token, others use limit and offset. Write a generator so the rest of your code never cares which.
export async function* paginate<T>( fetchPage: (cursor?: string) => Promise<{ items: T[]; next?: string }> ) { let cursor: string | undefined; do { const page = await fetchPage(cursor); for (const item of page.items) yield item; cursor = page.next || undefined; } while (cursor); }
export async function* paginate<T>( fetchPage: (cursor?: string) => Promise<{ items: T[]; next?: string }> ) { let cursor: string | undefined; do { const page = await fetchPage(cursor); for (const item of page.items) yield item; cursor = page.next || undefined; } while (cursor); }
For offset-based endpoints, wrap the offset in the cursor string and increment it by the page size. Stop when a page comes back shorter than the limit or empty. Never assume a page count from a first response; markets open and close while you iterate, so deduplicate by ID and expect occasional repeats.
Kalshi: signed requests
Kalshi public market data can be read without credentials, but authenticated calls and WebSocket connections need signing. According to a developer guide to the Kalshi API, authenticated requests carry three headers: an access key ID, a timestamp in milliseconds, and an RSA-PSS SHA-256 signature of the timestamp, HTTP method and request path. For WebSocket connections, the path to sign is the WebSocket path rather than a REST path.
Node’s built-in crypto module covers it:
import { createSign, constants } from 'node:crypto'; export function kalshiHeaders(keyId: string, privateKeyPem: string, method: string, path: string) { const ts = Date.now().toString(); const signer = createSign('RSA-SHA256'); signer.update(ts + method.toUpperCase() + path); const signature = signer.sign({ key: privateKeyPem, padding: constants.RSA_PKCS1_PSS_PADDING, saltLength: constants.RSA_PSS_SALTLEN_DIGEST, }); return { 'KALSHI-ACCESS-KEY': keyId, 'KALSHI-ACCESS-TIMESTAMP': ts, 'KALSHI-ACCESS-SIGNATURE': signature.toString('base64'), }; }
import { createSign, constants } from 'node:crypto'; export function kalshiHeaders(keyId: string, privateKeyPem: string, method: string, path: string) { const ts = Date.now().toString(); const signer = createSign('RSA-SHA256'); signer.update(ts + method.toUpperCase() + path); const signature = signer.sign({ key: privateKeyPem, padding: constants.RSA_PKCS1_PSS_PADDING, saltLength: constants.RSA_PSS_SALTLEN_DIGEST, }); return { 'KALSHI-ACCESS-KEY': keyId, 'KALSHI-ACCESS-TIMESTAMP': ts, 'KALSHI-ACCESS-SIGNATURE': signature.toString('base64'), }; }
Sign the path without the query string, and confirm the exact string-to-sign rules against Kalshi’s current documentation before shipping. Clock drift is the most common cause of mysterious 401 errors, so run NTP on any server that signs requests.
Polymarket: separate services for separate jobs
Polymarket splits its public surface across services: one for event and market metadata, one for the central limit order book (prices, books, trading) and one for user and trade activity. Market metadata is public and unauthenticated. Order placement needs wallet-based credentials. Keep those code paths in different modules so a research script never imports anything that can sign an order.
Polymarket markets are identified by condition and by per-outcome token IDs. Your Node code will often join a market record from the metadata service to a book request keyed by token ID. Store both IDs on every row.
Reading order books
A book snapshot is a pair of price-sorted arrays. Compute a midpoint and spread from the best levels, and estimate depth to a target size:
type Level = { price: number; size: number }; export function bookStats(bids: Level[], asks: Level[], notional: number) { const bestBid = Math.max(...bids.map((l) => l.price)); const bestAsk = Math.min(...asks.map((l) => l.price)); const mid = (bestBid + bestAsk) / 2; let remaining = notional, cost = 0; for (const l of [...asks].sort((a, b) => a.price - b.price)) { const take = Math.min(remaining, l.size * l.price); cost += take; remaining -= take; if (remaining <= 0) break; } return { bestBid, bestAsk, mid, spread: bestAsk - bestBid, filled: notional - remaining }; }
type Level = { price: number; size: number }; export function bookStats(bids: Level[], asks: Level[], notional: number) { const bestBid = Math.max(...bids.map((l) => l.price)); const bestAsk = Math.min(...asks.map((l) => l.price)); const mid = (bestBid + bestAsk) / 2; let remaining = notional, cost = 0; for (const l of [...asks].sort((a, b) => a.price - b.price)) { const take = Math.min(remaining, l.size * l.price); cost += take; remaining -= take; if (remaining <= 0) break; } return { bestBid, bestAsk, mid, spread: bestAsk - bestBid, filled: notional - remaining }; }
Prices on binary contracts live between 0 and 1 on some venues and in cents (1 to 99) on others. Convert at the boundary, in one function, and never again.
Authentication, identifiers and rate limits
These three decide whether your pipeline survives a week in production.
Authentication. Separate read credentials from trading credentials. A data pipeline should hold none that can move money. Where a venue offers a demo or sandbox environment, point development at it.
Identifiers. Expect at least three per market: an event identifier, a market identifier, and an outcome or token identifier. Kalshi uses human-readable tickers. Polymarket uses hashes and long token IDs. Treat them as opaque strings, store them as text, never as JavaScript numbers (large integers lose precision above 2^53).
Rate limits. Every venue publishes limits, often per endpoint group, and they change. Build a token-bucket limiter per host and per endpoint group, configured from environment variables so you can adjust without a deploy:
export class Bucket { private tokens: number; private last = Date.now(); constructor(private rate: number, private burst: number) { this.tokens = burst; } async take() { for (;;) { const now = Date.now(); this.tokens = Math.min(this.burst, this.tokens + ((now - this.last) / 1000) * this.rate); this.last = now; if (this.tokens >= 1) { this.tokens -= 1; return; } await new Promise((r) => setTimeout(r, 50)); } } }
export class Bucket { private tokens: number; private last = Date.now(); constructor(private rate: number, private burst: number) { this.tokens = burst; } async take() { for (;;) { const now = Date.now(); this.tokens = Math.min(this.burst, this.tokens + ((now - this.last) / 1000) * this.rate); this.last = now; if (this.tokens >= 1) { this.tokens -= 1; return; } await new Promise((r) => setTimeout(r, 50)); } } }
Cache aggressively. Market metadata changes rarely, so a five-minute cache removes most of your calls. Prices and books are the only things that deserve frequent refresh, and for those you should be on a WebSocket, not a polling loop.

Streaming real-time updates in Node.js
Polling a book every second across hundreds of markets burns your rate budget and still misses moves between polls. Subscribe instead.
import WebSocket from 'ws'; export function stream(url: string, subscribe: object, onMsg: (m: any) => void) { let attempt = 0; const connect = () => { const ws = new WebSocket(url); ws.on('open', () => { attempt = 0; ws.send(JSON.stringify(subscribe)); }); ws.on('message', (d) => onMsg(JSON.parse(d.toString()))); ws.on('close', () => setTimeout(connect, Math.min(30_000, 500 * 2 ** attempt++))); ws.on('error', () => ws.close()); }; connect(); }
import WebSocket from 'ws'; export function stream(url: string, subscribe: object, onMsg: (m: any) => void) { let attempt = 0; const connect = () => { const ws = new WebSocket(url); ws.on('open', () => { attempt = 0; ws.send(JSON.stringify(subscribe)); }); ws.on('message', (d) => onMsg(JSON.parse(d.toString()))); ws.on('close', () => setTimeout(connect, Math.min(30_000, 500 * 2 ** attempt++))); ws.on('error', () => ws.close()); }; connect(); }
A socket that reconnects is not a socket that is correct. After every reconnect you have a gap. Handle it in three steps:
On reconnect, mark the affected markets as stale.
Fetch a REST snapshot of each book and trade list since the last timestamp you stored.
Apply buffered stream messages on top, discarding any older than the snapshot.
Also add an application-level heartbeat check. If no message arrives for a market that normally ticks, treat the connection as half-open and recycle it.
Normalizing Polymarket, Kalshi and Limitless
To compare venues you need one schema. A workable core looks like this:
export interface CanonicalMarket { canonicalEventId: string; // your ID, stable across venues venue: 'polymarket' | 'kalshi' | 'limitless'; venueMarketId: string; venueOutcomeId: string; question: string; outcome: string; // 'YES' | 'NO' or a named outcome closeTime: string; // ISO 8601 UTC status: 'open' | 'closed' | 'resolved'; resolution?: 'YES' | 'NO' | string; }
export interface CanonicalMarket { canonicalEventId: string; // your ID, stable across venues venue: 'polymarket' | 'kalshi' | 'limitless'; venueMarketId: string; venueOutcomeId: string; question: string; outcome: string; // 'YES' | 'NO' or a named outcome closeTime: string; // ISO 8601 UTC status: 'open' | 'closed' | 'resolved'; resolution?: 'YES' | 'NO' | string; }
The decisions that cost teams time are not in the type definition:
Price units. Convert everything to a probability between 0 and 1 as a decimal string or integer basis points to avoid float drift in stored data.
Timestamps. Store UTC ISO 8601 and keep the venue’s original value in a raw column. Different venues stamp at match time, at API receipt or at block confirmation.
Outcome mapping. A binary “Will X happen?” market on one venue may be two tokens on another, or a multi-outcome market split into several binary contracts. Map outcomes, not markets.
Fees. Gross prices are not comparable until you apply each venue’s fee schedule. Any spread you compute between venues before fees is a hypothesis, not an edge.
Settlement rules. Two markets with similar titles can resolve on different sources or at different times. Compare the rule text before declaring them equivalent.
Matching events across venues is the genuinely hard part. A first pass using normalized titles, close dates and category gets you candidates, and a human or a rules table confirms them. Keep the confirmed mapping in a table with a reviewed-by and reviewed-at column. We cover the identifier approach in more depth in our guide to canonical IDs for cross-venue prediction market data.
Our perspective: why the data layer should be venue-neutral
We built Assymetrix because the same three problems kept appearing in every prediction market project we saw: schema drift between venues, gaps in history, and a quiet conflict of interest when the data comes from a party that also runs the exchange.
Our view is that a research or bot pipeline should consume data that is independent of any single venue. We aggregate real-time and historical data across Polymarket, Kalshi and Limitless into one layer, backed by roughly 1.5 terabytes of history and nearly one billion rows of trading activity. That depth is what makes a backtest credible: you can replay a strategy over resolved markets on every venue with the same field names and the same identifiers, instead of rebuilding a slightly different adapter each time.
We also think the most valuable fields are not prices. They are derived from behavior: which wallets are consistently early and right, how skilled a trader is over many resolved markets, and where the same event is priced differently across venues. Those signals only make sense when trades from all venues sit in one normalized store.
For Node.js teams, our advice is practical. Keep your trading code and your data code in separate services. Validate every response at the boundary. Treat the first integration as a throwaway and invest your real time in the canonical event table, because that is the asset that outlives any single API.
— Dean
Unified API or direct venue integrations
Neither is always right. Choose by what you are building.
Criterion | Direct venue APIs | Unified data API |
|---|---|---|
Integrations to maintain | One per venue | One |
Schema and ID mapping | You build and maintain it | Provided as canonical IDs |
Historical depth | Whatever each venue exposes, formats differ | Single history in one schema |
Order entry | Native, shortest path | Usually separate from data access |
Rate limit management | Per venue, per endpoint group | One contract |
Cross-venue signals | You compute them | Can be precomputed |
If you only trade one venue and want the lowest-latency order path, integrate that venue directly for execution. If you research, backtest, run bots across venues or feed AI agents, a single normalized source saves the most engineering time. Many teams do both: unified data for decisions, direct venue connections for execution.
Reliability and access checks before you ship
Run through this list before a pipeline touches real money or real users:
Data is not execution. Confirm which calls only read data and which place orders. Keep order credentials out of research code entirely.
Venue access. Availability and eligibility rules differ by venue and by jurisdiction. Kalshi operates in the US as a regulated exchange, while other venues have different access terms. Read each venue’s current terms and consult counsel for anything beyond read-only research.
Completeness. Compare your stored trade count with the venue’s reported volume for a sample of days. Gaps point to missed reconnects or pagination bugs.
Idempotency. Insert trades with a unique key (venue plus trade ID) so retries and backfills never double count.
Monitoring. Alert on stale markets, 429 rates, WebSocket reconnect frequency and schema validation failures.
Version drift. Pin SDK versions and watch venue changelogs. Unofficial community SDKs often cover only part of an API, so check which endpoints they implement before relying on them.
Where to run it
A long-running Node.js collector wants a small always-on server rather than a laptop or a serverless function with short timeouts. For a modest collector, memory and a stable network matter more than raw CPU; the guide to sizing a VPS for Node.js APIs covers how much RAM small services need and when to scale up.
Start with the Assymetrix Data API
If you would rather skip three adapters and one mapping table, the Assymetrix Data API gives developers, bots and AI agents unified access to Polymarket, Kalshi and Limitless data through one integration. Our prediction market data feed guide explains real-time and historical access, and the backtesting walkthrough shows how to replay strategies on price snapshots. Pricing is not published on the site, so check the Data API page for current tiers. If you work in Python as well, see our Python prediction market data guide.
FAQ
Is there an official Node.js SDK for prediction markets?
It depends on the venue. Some venues publish official clients in other languages, and community TypeScript clients exist, but they are often unofficial and partial. One popular unofficial Kalshi TypeScript client states that it implements only the market endpoints, so check coverage before depending on any SDK. Plain fetch plus ws and zod is a reliable fallback.
Can I use the same Node.js code for Polymarket and Kalshi?
Only above a normalization layer. Authentication, identifiers, price units and WebSocket protocols differ, so each venue needs its own adapter that outputs one canonical shape. Your analysis code should read only that shape. A unified API moves the adapters out of your codebase.
Should I poll or stream?
Use REST to discover markets and backfill history, and WebSocket for live prices, books and trades. Polling many markets quickly exhausts rate limits and still misses intra-interval moves. Always reconcile a reconnect with a REST snapshot so you do not keep a silent gap.
Does market data access let me place trades?
No. Reading prices and trades is separate from order entry, which needs signed, authenticated requests on the venue itself. Keep the two in separate services with separate credentials, and confirm that your use is permitted where you live.
Sources
Node.js Prediction Market API: Fetch, Stream and Normalize
A Node.js prediction market API integration comes down to four jobs: pull markets and outcomes, read prices and order books, stream changes, and map every venue’s identifiers onto one schema. Node 18 and later ships fetch and a stable event loop, so a single process can poll REST endpoints and hold WebSocket connections without extra frameworks. The hard part is not the HTTP call. It is that Polymarket, Kalshi and Limitless each describe the same real-world event differently, authenticate differently and throttle differently.
This guide walks through a working structure for a Node.js client: what data to request, how to handle authentication, pagination and rate limits, how to stream, how to normalize across venues, and what to check before you trust the feed.
Assymetrixdata.assymetrix.comUnify prediction market dataAssymetrix brings Polymarket, Kalshi, and Limitless data together through one API for developers building Node.js integrations.Explore the Data API
TL;DR
Separate data access from order execution. Public market data is mostly unauthenticated reads. Trading needs signed requests and keys you must protect.
Use REST for snapshots and backfills, WebSocket for live changes, and reconcile the two with sequence or timestamp checks.
Venue identifiers do not match. Store a canonical event ID in your own database and map each venue’s market ID, ticker or token ID to it.
Build rate-limit handling (backoff, queues, caching) before you scale polling, not after your first 429.
A unified API removes three integrations and three schema maps. A direct venue integration gives you the shortest path to that venue’s order entry.
Table of Contents
What a prediction market API returns
Project setup
Retrieving markets, prices and trades
Authentication, identifiers and rate limits
Streaming real-time updates in Node.js
Normalizing Polymarket, Kalshi and Limitless
Our perspective: why the data layer should be venue-neutral
Unified API or direct venue integrations
Reliability and access checks before you ship
Where to run it
Start with the Assymetrix Data API
Sources
What a prediction market API returns
A prediction market API gives programmatic access to prices, markets and, on venues that allow it, trading. For research and automation, the useful data falls into six groups:
Data type | What it holds | Typical use |
|---|---|---|
Events and markets | Titles, rules, close times, outcome lists, status | Discovery, filtering, universe selection |
Prices and quotes | Last price, bid, ask, midpoint | Signals, dashboards, alerts |
Order books | Depth by price level | Liquidity and slippage estimates |
Trades | Time, price, size, side, sometimes wallet or account fingerprint | Flow analysis, volume, wallet tracking |
Settlements | Resolution outcome and time | Labels for backtests and calibration |
Lifecycle events | Created, opened, paused, resolved | Pipeline triggers, data quality checks |
Settlements matter more than most developers expect. Without resolved outcomes you cannot score a model, measure calibration or compute realized profit for any trader you track. Store them from day one, with the resolution timestamp, because venues sometimes dispute or amend outcomes.
Which signals are worth storing
If you plan to research trader behavior, not just prices, capture more than closing quotes. An academic study of trading success on a prediction-style market used orders, order modifications, executed trades, anonymous trader IDs and five-minute top-of-book quotes, then measured profitability, exposure and holding time. The same study classified traders by maximum exposure and by whether they repeatedly opened and closed positions, and found trading efficiency was more indicative of success than information models or funding. That is a useful shopping list for a schema: every trade with a stable trader key, timestamps precise enough to order events, and enough book snapshots to reconstruct the spread when a trade happened.
Project setup
Start with a small, boring project. You want typed responses, a retry layer and a place to put credentials that is not your repository.
mkdir pm-client && cd pm-client npm init -y npm install ws zod dotenv npm install -D typescript tsx @types/node @types/ws
ws handles WebSockets. zod validates responses at the boundary, which saves you when a venue adds a field or changes a type. Keep API keys in environment variables and load them with dotenv in development only.
A minimal fetch wrapper with retries
Every venue integration needs the same plumbing. Write it once.
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); export async function getJson<T>(url: string, init: RequestInit = {}, tries = 5): Promise<T> { for (let attempt = 0; attempt < tries; attempt++) { const res = await fetch(url, { ...init, signal: AbortSignal.timeout(10_000) }); if (res.ok) return (await res.json()) as T; const retryable = res.status === 429 || res.status >= 500; if (!retryable || attempt === tries - 1) { throw new Error(`${res.status} ${res.statusText} for ${url}
Three details earn their place here: a hard timeout so a stalled socket cannot freeze your loop, respect for a Retry-After header when the server sends one, and jitter so parallel workers do not retry in lockstep.
Retrieving markets, prices and trades
The shape of the calls differs by venue, but the flow is the same: list events, pick markets, read prices, then pull trades.

Listing markets with pagination
Most venues return markets in pages. Some use a cursor token, others use limit and offset. Write a generator so the rest of your code never cares which.
export async function* paginate<T>( fetchPage: (cursor?: string) => Promise<{ items: T[]; next?: string }> ) { let cursor: string | undefined; do { const page = await fetchPage(cursor); for (const item of page.items) yield item; cursor = page.next || undefined; } while (cursor); }
For offset-based endpoints, wrap the offset in the cursor string and increment it by the page size. Stop when a page comes back shorter than the limit or empty. Never assume a page count from a first response; markets open and close while you iterate, so deduplicate by ID and expect occasional repeats.
Kalshi: signed requests
Kalshi public market data can be read without credentials, but authenticated calls and WebSocket connections need signing. According to a developer guide to the Kalshi API, authenticated requests carry three headers: an access key ID, a timestamp in milliseconds, and an RSA-PSS SHA-256 signature of the timestamp, HTTP method and request path. For WebSocket connections, the path to sign is the WebSocket path rather than a REST path.
Node’s built-in crypto module covers it:
import { createSign, constants } from 'node:crypto'; export function kalshiHeaders(keyId: string, privateKeyPem: string, method: string, path: string) { const ts = Date.now().toString(); const signer = createSign('RSA-SHA256'); signer.update(ts + method.toUpperCase() + path); const signature = signer.sign({ key: privateKeyPem, padding: constants.RSA_PKCS1_PSS_PADDING, saltLength: constants.RSA_PSS_SALTLEN_DIGEST, }); return { 'KALSHI-ACCESS-KEY': keyId, 'KALSHI-ACCESS-TIMESTAMP': ts, 'KALSHI-ACCESS-SIGNATURE': signature.toString('base64'), }; }
Sign the path without the query string, and confirm the exact string-to-sign rules against Kalshi’s current documentation before shipping. Clock drift is the most common cause of mysterious 401 errors, so run NTP on any server that signs requests.
Polymarket: separate services for separate jobs
Polymarket splits its public surface across services: one for event and market metadata, one for the central limit order book (prices, books, trading) and one for user and trade activity. Market metadata is public and unauthenticated. Order placement needs wallet-based credentials. Keep those code paths in different modules so a research script never imports anything that can sign an order.
Polymarket markets are identified by condition and by per-outcome token IDs. Your Node code will often join a market record from the metadata service to a book request keyed by token ID. Store both IDs on every row.
Reading order books
A book snapshot is a pair of price-sorted arrays. Compute a midpoint and spread from the best levels, and estimate depth to a target size:
type Level = { price: number; size: number }; export function bookStats(bids: Level[], asks: Level[], notional: number) { const bestBid = Math.max(...bids.map((l) => l.price)); const bestAsk = Math.min(...asks.map((l) => l.price)); const mid = (bestBid + bestAsk) / 2; let remaining = notional, cost = 0; for (const l of [...asks].sort((a, b) => a.price - b.price)) { const take = Math.min(remaining, l.size * l.price); cost += take; remaining -= take; if (remaining <= 0) break; } return { bestBid, bestAsk, mid, spread: bestAsk - bestBid, filled: notional - remaining }; }
Prices on binary contracts live between 0 and 1 on some venues and in cents (1 to 99) on others. Convert at the boundary, in one function, and never again.
Authentication, identifiers and rate limits
These three decide whether your pipeline survives a week in production.
Authentication. Separate read credentials from trading credentials. A data pipeline should hold none that can move money. Where a venue offers a demo or sandbox environment, point development at it.
Identifiers. Expect at least three per market: an event identifier, a market identifier, and an outcome or token identifier. Kalshi uses human-readable tickers. Polymarket uses hashes and long token IDs. Treat them as opaque strings, store them as text, never as JavaScript numbers (large integers lose precision above 2^53).
Rate limits. Every venue publishes limits, often per endpoint group, and they change. Build a token-bucket limiter per host and per endpoint group, configured from environment variables so you can adjust without a deploy:
export class Bucket { private tokens: number; private last = Date.now(); constructor(private rate: number, private burst: number) { this.tokens = burst; } async take() { for (;;) { const now = Date.now(); this.tokens = Math.min(this.burst, this.tokens + ((now - this.last) / 1000) * this.rate); this.last = now; if (this.tokens >= 1) { this.tokens -= 1; return; } await new Promise((r) => setTimeout(r, 50)); } } }
Cache aggressively. Market metadata changes rarely, so a five-minute cache removes most of your calls. Prices and books are the only things that deserve frequent refresh, and for those you should be on a WebSocket, not a polling loop.

Streaming real-time updates in Node.js
Polling a book every second across hundreds of markets burns your rate budget and still misses moves between polls. Subscribe instead.
import WebSocket from 'ws'; export function stream(url: string, subscribe: object, onMsg: (m: any) => void) { let attempt = 0; const connect = () => { const ws = new WebSocket(url); ws.on('open', () => { attempt = 0; ws.send(JSON.stringify(subscribe)); }); ws.on('message', (d) => onMsg(JSON.parse(d.toString()))); ws.on('close', () => setTimeout(connect, Math.min(30_000, 500 * 2 ** attempt++))); ws.on('error', () => ws.close()); }; connect(); }
A socket that reconnects is not a socket that is correct. After every reconnect you have a gap. Handle it in three steps:
On reconnect, mark the affected markets as stale.
Fetch a REST snapshot of each book and trade list since the last timestamp you stored.
Apply buffered stream messages on top, discarding any older than the snapshot.
Also add an application-level heartbeat check. If no message arrives for a market that normally ticks, treat the connection as half-open and recycle it.
Normalizing Polymarket, Kalshi and Limitless
To compare venues you need one schema. A workable core looks like this:
export interface CanonicalMarket { canonicalEventId: string; // your ID, stable across venues venue: 'polymarket' | 'kalshi' | 'limitless'; venueMarketId: string; venueOutcomeId: string; question: string; outcome: string; // 'YES' | 'NO' or a named outcome closeTime: string; // ISO 8601 UTC status: 'open' | 'closed' | 'resolved'; resolution?: 'YES' | 'NO' | string; }
The decisions that cost teams time are not in the type definition:
Price units. Convert everything to a probability between 0 and 1 as a decimal string or integer basis points to avoid float drift in stored data.
Timestamps. Store UTC ISO 8601 and keep the venue’s original value in a raw column. Different venues stamp at match time, at API receipt or at block confirmation.
Outcome mapping. A binary “Will X happen?” market on one venue may be two tokens on another, or a multi-outcome market split into several binary contracts. Map outcomes, not markets.
Fees. Gross prices are not comparable until you apply each venue’s fee schedule. Any spread you compute between venues before fees is a hypothesis, not an edge.
Settlement rules. Two markets with similar titles can resolve on different sources or at different times. Compare the rule text before declaring them equivalent.
Matching events across venues is the genuinely hard part. A first pass using normalized titles, close dates and category gets you candidates, and a human or a rules table confirms them. Keep the confirmed mapping in a table with a reviewed-by and reviewed-at column. We cover the identifier approach in more depth in our guide to canonical IDs for cross-venue prediction market data.
Our perspective: why the data layer should be venue-neutral
We built Assymetrix because the same three problems kept appearing in every prediction market project we saw: schema drift between venues, gaps in history, and a quiet conflict of interest when the data comes from a party that also runs the exchange.
Our view is that a research or bot pipeline should consume data that is independent of any single venue. We aggregate real-time and historical data across Polymarket, Kalshi and Limitless into one layer, backed by roughly 1.5 terabytes of history and nearly one billion rows of trading activity. That depth is what makes a backtest credible: you can replay a strategy over resolved markets on every venue with the same field names and the same identifiers, instead of rebuilding a slightly different adapter each time.
We also think the most valuable fields are not prices. They are derived from behavior: which wallets are consistently early and right, how skilled a trader is over many resolved markets, and where the same event is priced differently across venues. Those signals only make sense when trades from all venues sit in one normalized store.
For Node.js teams, our advice is practical. Keep your trading code and your data code in separate services. Validate every response at the boundary. Treat the first integration as a throwaway and invest your real time in the canonical event table, because that is the asset that outlives any single API.
— Dean
Unified API or direct venue integrations
Neither is always right. Choose by what you are building.
Criterion | Direct venue APIs | Unified data API |
|---|---|---|
Integrations to maintain | One per venue | One |
Schema and ID mapping | You build and maintain it | Provided as canonical IDs |
Historical depth | Whatever each venue exposes, formats differ | Single history in one schema |
Order entry | Native, shortest path | Usually separate from data access |
Rate limit management | Per venue, per endpoint group | One contract |
Cross-venue signals | You compute them | Can be precomputed |
If you only trade one venue and want the lowest-latency order path, integrate that venue directly for execution. If you research, backtest, run bots across venues or feed AI agents, a single normalized source saves the most engineering time. Many teams do both: unified data for decisions, direct venue connections for execution.
Reliability and access checks before you ship
Run through this list before a pipeline touches real money or real users:
Data is not execution. Confirm which calls only read data and which place orders. Keep order credentials out of research code entirely.
Venue access. Availability and eligibility rules differ by venue and by jurisdiction. Kalshi operates in the US as a regulated exchange, while other venues have different access terms. Read each venue’s current terms and consult counsel for anything beyond read-only research.
Completeness. Compare your stored trade count with the venue’s reported volume for a sample of days. Gaps point to missed reconnects or pagination bugs.
Idempotency. Insert trades with a unique key (venue plus trade ID) so retries and backfills never double count.
Monitoring. Alert on stale markets, 429 rates, WebSocket reconnect frequency and schema validation failures.
Version drift. Pin SDK versions and watch venue changelogs. Unofficial community SDKs often cover only part of an API, so check which endpoints they implement before relying on them.
Where to run it
A long-running Node.js collector wants a small always-on server rather than a laptop or a serverless function with short timeouts. For a modest collector, memory and a stable network matter more than raw CPU; the guide to sizing a VPS for Node.js APIs covers how much RAM small services need and when to scale up.
Start with the Assymetrix Data API
If you would rather skip three adapters and one mapping table, the Assymetrix Data API gives developers, bots and AI agents unified access to Polymarket, Kalshi and Limitless data through one integration. Our prediction market data feed guide explains real-time and historical access, and the backtesting walkthrough shows how to replay strategies on price snapshots. Pricing is not published on the site, so check the Data API page for current tiers. If you work in Python as well, see our Python prediction market data guide.
FAQ
Is there an official Node.js SDK for prediction markets?
It depends on the venue. Some venues publish official clients in other languages, and community TypeScript clients exist, but they are often unofficial and partial. One popular unofficial Kalshi TypeScript client states that it implements only the market endpoints, so check coverage before depending on any SDK. Plain fetch plus ws and zod is a reliable fallback.
Can I use the same Node.js code for Polymarket and Kalshi?
Only above a normalization layer. Authentication, identifiers, price units and WebSocket protocols differ, so each venue needs its own adapter that outputs one canonical shape. Your analysis code should read only that shape. A unified API moves the adapters out of your codebase.
Should I poll or stream?
Use REST to discover markets and backfill history, and WebSocket for live prices, books and trades. Polling many markets quickly exhausts rate limits and still misses intra-interval moves. Always reconcile a reconnect with a REST snapshot so you do not keep a silent gap.
Does market data access let me place trades?
No. Reading prices and trades is separate from order entry, which needs signed, authenticated requests on the venue itself. Keep the two in separate services with separate credentials, and confirm that your use is permitted where you live.
Sources
Other Blog



