Real-time Data
Connect to the AGG WebSocket API to receive live aggregated orderbook updates, public trades, price ticks, and user-specific events across Kalshi and Polymarket.
wss://ws.agg.market?appId=YOUR_APP_ID
Getting connected
Open a connection
Pass your appId as a query parameter. The server validates the app is active and the request origin matches your configured allowedOrigins.const ws = new WebSocket(
`wss://ws.agg.market?appId=${appId}`
);
Optionally pass &token=eyJ... for immediate user-level auth. Wait for the connected message
The server confirms the connection with your app and user context:{
"type": "connected",
"appId": "app_demo123",
"userId": "usr_xyz789"
}
userId is present only if a valid JWT was provided. Subscribe to markets
Start receiving data by subscribing to one or more canonical market IDs:{
"action": "subscribe",
"marketIds": ["clv2abc123def456", "clv2xyz789ghi012"]
}
A single connection supports up to 100 subscriptions (orderbook + trade combined). The server accepts as many as fit and rejects the rest with an error message.
1. Orderbook WebSocket
Real-time aggregated orderbook with per-venue attribution. Receive a full snapshot on subscribe, then incremental deltas as the book changes.
Use cases
- Display a live aggregated orderbook across venues
- Build a local book for best-execution routing
- Track venue-level depth and best prices
Subscribe to orderbook updates
{
"action": "subscribe",
"marketIds": ["clv2abc123def456"]
}
The server confirms your subscription:
{
"type": "subscribed",
"marketIds": ["clv2abc123def456"]
}
Initial snapshot
Immediately after subscribing, you receive the full book state:
{
"type": "orderbook_snapshot",
"marketId": "clv2abc123def456",
"seq": 1710000001,
"checksum": 2918476531,
"bids": [
[0.55, 1500, { "kalshi": 800, "polymarket": 700 }],
[0.54, 900, { "kalshi": 400, "polymarket": 500 }]
],
"asks": [
[0.56, 1200, { "kalshi": 600, "polymarket": 600 }],
[0.57, 800, { "polymarket": 800 }]
],
"venueOrderbooks": {
"kalshi": {
"bids": [[0.55, 800], [0.54, 400]],
"asks": [[0.56, 600]]
},
"polymarket": {
"bids": [[0.55, 700], [0.54, 500]],
"asks": [[0.56, 600], [0.57, 800]]
}
},
"venues": {
"kalshi": { "bestBid": 0.55, "bestAsk": 0.56 },
"polymarket": { "bestBid": 0.55, "bestAsk": 0.56 }
},
"midpoint": 0.555,
"spread": 0.01,
"timestamp": 1710000000000
}
Level formats: Aggregated levels are [price, totalSize, {venue: size}]. Per-venue levels are [price, size]. Bids are sorted descending by price, asks ascending.
Real-time deltas
After the snapshot, only changed levels are sent. A size of 0 means the level was removed.
{
"type": "orderbook_delta",
"marketId": "clv2abc123def456",
"seq": 1710000002,
"checksum": 3847261950,
"bidChanges": [
[0.55, 1600, { "kalshi": 900, "polymarket": 700 }]
],
"askChanges": [],
"venueDeltaBooks": {
"kalshi": { "bidChanges": [[0.55, 900]], "askChanges": [] }
},
"venues": {
"kalshi": { "bestBid": 0.55, "bestAsk": 0.56 },
"polymarket": { "bestBid": 0.55, "bestAsk": 0.56 }
},
"midpoint": 0.555,
"spread": 0.01,
"timestamp": 1710000001000
}
Applying deltas to your local book
Upsert or remove levels
For each entry in bidChanges / askChanges: if size > 0, upsert the level. If size === 0, remove it.
Re-sort
Sort bids descending by price, asks ascending.
Verify checksum
Compare checksum against your local CRC32 of the book. On mismatch, request a resnapshot.
Sequencing and resync
Every message includes a monotonic seq per market that increments by exactly 1. If you detect a gap:
- The server automatically sends a full snapshot instead of a delta when it detects a gap
- You can also request one explicitly:
{
"action": "resnapshot",
"marketIds": ["clv2abc123def456"]
}
Sequence numbers are epoch-based (large numbers) so they never collide across server restarts.
Complete example
const ws = new WebSocket(`wss://ws.agg.market?appId=${appId}`);
// Local book state
const book = { bids: [], asks: [] };
let lastSeq = null;
ws.onopen = () => {
ws.send(JSON.stringify({
action: "subscribe",
marketIds: ["clv2abc123def456"],
}));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
switch (msg.type) {
case "orderbook_snapshot":
book.bids = msg.bids;
book.asks = msg.asks;
lastSeq = msg.seq;
renderBook(book);
break;
case "orderbook_delta":
if (lastSeq !== null && msg.seq !== lastSeq + 1) {
// Gap detected — request resnapshot
ws.send(JSON.stringify({
action: "resnapshot",
marketIds: [msg.marketId],
}));
break;
}
applyDelta(book.bids, msg.bidChanges, "desc");
applyDelta(book.asks, msg.askChanges, "asc");
lastSeq = msg.seq;
renderBook(book);
break;
case "price":
updatePriceDisplay(msg.midpoint, msg.spread);
break;
}
};
function applyDelta(levels, changes, sort) {
for (const [price, size, venues] of changes) {
const idx = levels.findIndex(([p]) => p === price);
if (size === 0) {
if (idx !== -1) levels.splice(idx, 1);
} else if (idx !== -1) {
levels[idx] = [price, size, venues];
} else {
levels.push([price, size, venues]);
}
}
levels.sort((a, b) =>
sort === "desc" ? b[0] - a[0] : a[0] - b[0]
);
}
2. Trade WebSocket
Subscribe to the public trade feed for any market. No user auth required.
Use cases
- Display a live trade ticker
- Track volume and trade direction
- Build trade history charts
Subscribe to trades
{
"action": "subscribe_trades",
"marketIds": ["clv2abc123def456"]
}
Real-time trade events
{
"type": "trade",
"marketId": "clv2abc123def456",
"venue": "kalshi",
"side": "buy",
"price": 0.55,
"size": 100,
"timestamp": 1710000000000
}
Trade subscriptions share the 100-subscription limit with orderbook subscriptions.
Unsubscribe
{
"action": "unsubscribe_trades",
"marketIds": ["clv2abc123def456"]
}
3. Price Tick WebSocket
Live midpoint and spread updates, sent automatically to orderbook subscribers.
{
"type": "price",
"marketId": "clv2abc123def456",
"midpoint": 0.555,
"spread": 0.01,
"bestBid": 0.55,
"bestAsk": 0.56,
"timestamp": 1710000000000
}
Price ticks are sent automatically when you subscribe to a market’s orderbook — no separate subscription needed.
4. User Events WebSocket
Receive order confirmations and balance updates in real time after trade execution. Requires user-level auth.
Authenticate
Pass your JWT as a query parameter:wss://ws.agg.market?appId=YOUR_APP_ID&token=eyJhbG...
Upgrade an existing connection after the user signs in:{
"action": "authenticate",
"token": "eyJhbGciOiJIUzI1NiIs..."
}
{
"type": "authenticated",
"userId": "usr_xyz789"
}
Mid-session auth supports re-authentication for token refresh or user switching without reconnecting.
Order submitted
Sent after a trade is executed on a venue:
{
"type": "order_submitted",
"venue": "kalshi",
"orderId": "ord_abc123",
"side": "buy",
"price": 0.55,
"size": 100,
"marketId": "clv2abc123def456",
"timestamp": 1710000000000
}
Balance update
Sent when a trade affects the user’s venue balance:
{
"type": "balance_update",
"venue": "kalshi",
"tradingBalanceCents": 94500,
"walletBalanceCents": 100000,
"timestamp": 1710000000000
}
Connection management
Heartbeat
The server pings every 30 seconds using transport-level WebSocket ping/pong frames. Clients that miss 2 consecutive pongs are terminated.
No client-side implementation needed — browsers respond to pings automatically.
Reconnection
The server sends close code 1001 (Going Away) during planned deployments. Reconnect with exponential backoff:
function connect(attempt = 0) {
const ws = new WebSocket(`wss://ws.agg.market?appId=${appId}`);
ws.onclose = (event) => {
const delay = Math.min(1000 * Math.pow(2, attempt) + Math.random() * 1000, 30000);
setTimeout(() => connect(attempt + 1), delay);
};
ws.onopen = () => {
attempt = 0;
// Re-subscribe to all channels — server does not
// persist subscriptions across connections
resubscribeAll(ws);
};
}
Error handling
The server sends error messages for validation failures, auth issues, or protocol violations:
{ "type": "error", "message": "Invalid marketIds: must be a non-empty array" }
Fatal errors close the connection with a descriptive close code:
| Code | Meaning |
|---|
4001 | Missing appId query parameter |
4003 | Invalid/inactive app or origin not allowed |
1001 | Server restarting — reconnect with backoff |
Next steps