Skip to main content

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

1

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.
Connect
const ws = new WebSocket(
  `wss://ws.agg.market?appId=${appId}`
);
Optionally pass &token=eyJ... for immediate user-level auth.
2

Wait for the connected message

The server confirms the connection with your app and user context:
Connected response
{
  "type": "connected",
  "appId": "app_demo123",
  "userId": "usr_xyz789"
}
userId is present only if a valid JWT was provided.
3

Subscribe to markets

Start receiving data by subscribing to one or more canonical market IDs:
Subscribe
{
  "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

Subscribe to orderbooks
{
  "action": "subscribe",
  "marketIds": ["clv2abc123def456"]
}
The server confirms your subscription:
Confirmation
{
  "type": "subscribed",
  "marketIds": ["clv2abc123def456"]
}

Initial snapshot

Immediately after subscribing, you receive the full book state:
Orderbook snapshot
{
  "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.
Orderbook delta
{
  "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

1

Upsert or remove levels

For each entry in bidChanges / askChanges: if size > 0, upsert the level. If size === 0, remove it.
2

Re-sort

Sort bids descending by price, asks ascending.
3

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:
Request resnapshot
{
  "action": "resnapshot",
  "marketIds": ["clv2abc123def456"]
}
Sequence numbers are epoch-based (large numbers) so they never collide across server restarts.

Complete example

Orderbook client
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

Subscribe to trades
{
  "action": "subscribe_trades",
  "marketIds": ["clv2abc123def456"]
}

Real-time trade events

Trade event
{
  "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

Unsubscribe from trades
{
  "action": "unsubscribe_trades",
  "marketIds": ["clv2abc123def456"]
}

3. Price Tick WebSocket

Live midpoint and spread updates, sent automatically to orderbook subscribers.
Price tick
{
  "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...
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:
Order submitted
{
  "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:
Balance update
{
  "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:
Reconnection logic
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:
Error message
{ "type": "error", "message": "Invalid marketIds: must be a non-empty array" }
Fatal errors close the connection with a descriptive close code:
CodeMeaning
4001Missing appId query parameter
4003Invalid/inactive app or origin not allowed
1001Server restarting — reconnect with backoff

Next steps