Skip to main content

WebSocket Protocol

Connect to the AGG WebSocket API to receive live aggregated orderbook updates, public trades, cross-venue arbitrage-return updates, JSON heartbeat events, and authenticated user events across connected venues.
There is no midpoint channel. A midpoint field on an orderbook frame is not a subscription.
The JSON frames on this page are the protocol.

Getting connected

1

Open a connection

Pass your appId as a query parameter.
The handshake must carry an Origin header that matches one of your app’s allowed origins, or the connection closes with 4003. Browsers send it automatically. A server or native client must set it explicitly.If the user is already signed in, you can include &token=eyJ... for immediate user-level authentication. Sending the token with the authenticate message instead keeps it out of URLs and logs.
2

Wait for the connected message

The server acknowledges the connection with app and optional user context:
userId is present only when a valid JWT is supplied on connect. Send subscriptions after this message arrives.
3

Subscribe to outcome streams

Send one or more outcome IDs with a channel to start receiving orderbook or trade updates. Each outcome has its own orderbook — there is no complement derivation.
outcomeId is an outcome’s id (outcomes[].id on REST market responses). Each outcome belongs to one venue.
Keep orderbook and trade subscriptions together at or under 500 per connection. Outcomes beyond the limit are rejected with an error message.

1. Orderbook stream

Subscribe to an outcome’s orderbook to receive a full snapshot followed by incremental deltas. Each outcome has its own orderbook — there is no complement derivation. Levels carry venue attribution for the outcome’s venue, and the venue’s own book is included alongside.

Subscribe

You receive a confirmation before the initial snapshot:

Initial snapshot

Orderbook timestamp is Unix milliseconds on the wire, and the SDK reports it as seconds.
Aggregated levels use the wire format [price, totalSize, { venue: sizeAtPrice }]. Per-venue levels use [price, size]. Bids are sorted descending by price, asks ascending. A change with size 0 removes that level. All timestamp values on the wire are Unix milliseconds.
If no orderbook is available for an id, you get an error whose message starts with Snapshot unavailable for <outcomeId> instead of a snapshot. The subscription stays active, so deltas may still arrive: ignore deltas for an outcome you have no book for. Check that the id is an outcome id (outcomes[].id), not a market id, then retry with resnapshot after a delay rather than in a loop.

Incremental deltas

Market resolved

After you subscribe to orderbook for an outcome, a resolution or close arrives once as market_resolved. The outcome’s orderbook and trade subscriptions end with it: nothing further arrives, and you don’t need to unsubscribe. Trade-only subscribers are not sent this message. result is the label of the first outcome with winner: true, or null when none is marked winner. outcomes[] are the sibling outcomes for that market.
status is resolved or closed. outcomes can be empty, and winner is omitted when it isn’t known. payoutPerShare and refundAtCost are omitted when the venue did not report them. Settlement meaning is on Market resolution.

Sequencing and resync

Every orderbook message includes a seq value for that outcome. seq is monotonic within one stream of the book. Your client is responsible for maintaining local book state and detecting integrity issues. Apply deltas when delta.seq === book.seq + 1. After applying, recompute the checksum (XOR of per-level CRC32 values) and compare it to checksum in the message. If they match, the book is consistent. The exact level encoding is in the market-data guide. Drop stale deltas where delta.seq <= book.seq — these can arrive after a subscribe when deltas predate the initial snapshot. Silently discard them. Request a resnapshot if you detect a forward gap (delta.seq > book.seq + 1) or a checksum mismatch:
Always replace your book with an orderbook_snapshot, even when its seq is lower than yours. If AGG rebuilds an outcome’s book, that book starts a new seq stream. When the first update from the rebuilt book arrives, the gateway sends you a fresh orderbook_snapshot without being asked. Apply the following deltas against the snapshot’s seq.

Unsubscribe

Staleness checks

Heartbeats prove the connection is alive, not that each outcome is updating. A quiet market can legitimately go several minutes without a delta, so silence alone is not an error, and silence alone can’t tell you a stream has stopped reaching you. We recommend a 5-minute REST watchdog for every client that keeps books open. The gateway can only resend a snapshot when it sees a new update, so a quiet book that went wrong stays wrong. The watchdog confirms each book against the server instead of trusting that the stream is still flowing.
  1. Once a minute, find subscribed outcomes whose book has not changed for 5 minutes.
  2. Fetch up to 100 of them with GET /orderbooks/outcomes?venueMarketOutcomeIds=id1,id2,…&depth=1 (see Market-data API). Results come back in request order, with null for an id that can’t be served.
  3. Compare each returned seq with your book’s seq:
    • Same: the market is quiet and your book is current. Reset its 5-minute timer.
    • Different: you missed updates. Send resnapshot for that outcome.
    • Still different on the next check: unsubscribe and subscribe the outcome. That resets all per-subscription state on the gateway.
  4. Ignore an outcome whose book changed while the request was in flight. Its seq moved on for a normal reason.
Each outcome in a batch costs one market-data quota unit, so check only silent outcomes. On a 429, wait for Retry-After before the next check.

Best practices

Connection
  • Wait for connected before you subscribe. Only the first 20 messages sent before it are kept; the rest are dropped.
  • Answer protocol pings. Browsers and most WebSocket libraries do this automatically; a connection that misses two pings in a row is closed.
  • Treat 45 seconds without any frame, heartbeats included, as a dead connection and reconnect. Heartbeats arrive every 30 seconds.
  • Reconnect with exponential backoff and jitter. After reconnecting, discard your books, subscribe again to everything, and rebuild from the new snapshots. Subscriptions don’t survive a new connection.
  • Don’t keep reconnecting on 4001 or 4003. Those mean a missing app id, an inactive or unknown app, a disallowed origin, or a non-user token, and retrying won’t help until the configuration is fixed.
Orderbooks
  • Apply a delta only when its seq is exactly one more than your book’s, and verify its checksum.
  • On a forward seq gap or a checksum mismatch, send resnapshot. Never keep patching a book you can’t verify.
  • Replace your book on every orderbook_snapshot, whatever its seq.
  • Run the 5-minute REST watchdog.
  • On market_resolved, drop that outcome’s book.
Throughput
  • Subscribe only to outcomes you are displaying or trading, and unsubscribe when you stop.
  • Keep your message handler fast and move heavy work off it. If your client falls more than about 1 MB behind, the gateway skips frames for it. On orderbooks this shows up as a seq gap, which a resnapshot repairs. Skipped trades, heartbeats, user events, arbitrage updates and market_resolved messages are not resent.
Authentication
  • Prefer the authenticate message to a token in the URL.
  • A token is checked when you send it. After a refresh, use the new token on your next authenticate or reconnect, so an expired token never downgrades a reconnect to app level.
  • Send x-app-api-key (for example on the watchdog request) only from your backend, never from a browser.
The @agg-build/sdk handles the orderbook practices for you: seq tracking, stale delta filtering, checksum validation, resnapshots, server-sent snapshots and the 5-minute watchdog. It also reconnects with backoff after 45 seconds of silence and sends your token by message. Use client.createWebSocket() (the hooks use it) or the useLiveMarket hook. See Real-Time Orderbook for the watchdog settings.
SDK, hooks, and UI orderbooks are on Real-Time Orderbook. That page does not repeat these frames.

2. Trade stream

Subscribe to the public trade feed for an outcome. No user auth is required.

Subscribe

Confirmation:

Trade event

Unsubscribe

For chart-building patterns on top of orderbook and trade data, see Real-Time Charts.

3. Authenticated user events

Receive user-specific events, such as order confirmations and balance updates, after the connection has user-level auth.

Authenticate

Include the JWT in the connection URL:
Mid-session auth supports re-authentication after token refresh or user switching without opening a second socket.

Order submitted

Balance update

Order event

Venue-agnostic stream of execution-plan lifecycle and terminal order status events. Emitted for every active venue (Polymarket, Predict, Opinion, …) automatically — no per-venue handling on the client. Discriminate on the event field.

Step lifecycle (progress UI)

event values: step_started | step_completed | step_waiting | step_failed | step_retrying. sequence is the 1-based declaration index of the step within the execution plan, and totalSteps is the total step count. Pair them to render progress like “Step 3 of 7”. dag_started also carries totalSteps so the UI can show an empty progress bar before the first step runs. For branching execution plans, sequence reflects declaration order, not strict execution order — parallel branches may emit non-monotonic sequence values.

Terminal fill (success toast + position refresh)

event values: filled | partial_fill. remainingAmountRaw is present only on partial_fill.

Failure (error toast)

event values: failed (order-level) | dag_failed (execution-plan-level).

Execution plan lifecycle (optional)

event values: dag_started | dag_completed | dag_failed | dag_cancelled.
All order_event messages are broadcast verbatim — the gateway does not filter by event type. Switch on event and ignore types your UI doesn’t care about.
redeem_event reports EVM redemption completion. See Market resolution for how to track claims and update positions. For a notification-focused recipe, see User Notifications. That page does not repeat these frames.

4. Arbitrage streams

Stream the live cross-venue arbitrage return (arbReturn) for matched markets — the estimated return, as a decimal fraction (e.g. 0.012 = 1.2%), from the best prices across the venues in a matched cluster. This is the same value returned by the List Venue Events API; the stream keeps it live. There are two ways to consume it: a per-market channel for a focused view (one or a few markets), and a single coalesced feed channel for a whole listing.
Arbitrage streams are delta-only — there is no snapshot or sequence number. Load the initial arbReturn from a REST listing, then apply stream updates on top. Updates are last-value-wins and loss-tolerant: a dropped message self-heals on the next update. Subscribing requires only an app-level connection (your appId) — arbReturn is not user-scoped. For REST request and response details, use the API Reference.

Per-market channel

Subscribe with one or more marketIds (a market’s id) to receive an update whenever that market’s arbitrage return moves.
Confirmation. The ack field outcomeIds contains the marketIds you subscribed:
Update message — published per market as the value changes:
activeVenuesOnly is always true: the value only reflects active venues. venueEventId is the parent event of the market (or null), so you can roll updates up to an event-level value (e.g. the max across an event’s markets). Unsubscribe:

Feed channel

For a listing of many markets, subscribe once to the coalesced feed instead of managing a subscription per visible market. No ids are sent.
Confirmation:
Batch message — emitted on a short coalescing interval, carrying only the markets whose arbReturn changed since the last flush (deduped to the latest value per market):
A single flush is split into multiple messages when it exceeds the per-batch entry cap — chunk is the 0-based index and chunkCount the total for that flush. Merge entries into your local map by marketId regardless of chunk; the feed is a stream of deltas, not a full snapshot each flush.
Unsubscribe:
The @agg-build/sdk exposes subscribeArb(marketId, cb) and subscribeArbFeed(cb), and the @agg-build/hooks package adds useMarketArb and useArbFeed (which maintains a live byMarket map plus an event-level byEvent max rollup). See Real-Time Arbitrage.

Connection management

Heartbeat

The service emits a JSON heartbeat event that clients can observe directly:
The gateway sends this event every 30 seconds, along with a protocol-level ping. Browsers don’t expose pings to JavaScript, so use the JSON heartbeat (or any frame) as your liveness signal: if nothing arrives for 45 seconds, reconnect. A connection that doesn’t answer two pings in a row is closed by the server.

Reconnection

If the connection closes, reconnect with exponential backoff and jitter. Once the new connection sends connected, discard your books and subscribe again to everything:

Error handling

Protocol or validation errors arrive as JSON messages:
An error about a request, such as a rejected subscription, leaves the connection open. An invalid token on connect also leaves it open: the connection continues at app level and you receive an error explaining why. Connection-level failures send an error and then close with one of these codes:

Setup Guide

Provider wiring, auth setup, and SDK client initialization.

Real-Time Orderbook

SDK, hooks, and UI orderbook implementations.

Real-Time Charts

Build live candles from REST history plus WebSocket events.

Real-Time Arbitrage

Live cross-venue arbitrage returns via per-market and feed channels.

User Notifications

Authenticated order and balance events with refresh-aware reconnect logic.