Live orderbooks

Orderbook firehose

Stream every non-empty book for selected venues from your backend. Requires an app API key, and stays off until Snag enables it.

The firehose sends every book that has levels for the venues you name, then the live changes. It runs on the orderbook socket, not on the shared wss://ws.t.sufwankhalid.com/ws socket. It is a server channel. It is not a per-outcome orderbook subscribe, and a browser cannot open it.

Access

Snag enables the firehose for an app. You cannot turn it on. Email support@agg.market with your app id to request it.

The handshake requires a valid x-app-api-key. That is the same header as Server connections. A browser Origin is not enough, including when your app does not require an API key on other sockets. If the firehose is off, or the socket has no valid app API key, subscribe is refused:

{
  "type": "error",
  "code": "firehose_not_enabled"
}

Branch on code, not on message. If Snag turns the firehose off, or the key is revoked, open firehose sockets close with 4003 within a minute. The same happens when a venue you are streaming is removed from your enablement: reconnect and subscribe again, and the venues you still have are served. A short outage on our side does not close the socket.

Each enabled app has a connection cap. The default is 2 concurrent firehose connections. Snag sets the number when enabling the app. Stay within the cap you were given.

Each server also has a firehose capacity shared by all apps. When it is full, subscribe is refused and the socket stays open:

{
  "type": "error",
  "code": "too_many_connections",
  "message": "Orderbook firehose is at capacity on this server; reconnect with backoff"
}

Close the socket and reconnect with backoff. A new connection usually lands on another server.

Subscribe

Open wss://ob.agg.market/ws?appId=YOUR_APP_ID, send the API key on the handshake, and wait for connected. Heartbeats and the other close codes are on the WebSocket Protocol page.

import WebSocket from "ws";

const ws = new WebSocket(`wss://ob.agg.market/ws?appId=${appId}`, {
  headers: { "x-app-api-key": process.env.AGG_API_KEY },
});

Then subscribe:

{
  "action": "subscribe",
  "channel": "firehose",
  "venues": ["polymarket"],
  "latency": "realtime",
  "batchMs": 10
}

venues is the list of venue slugs to stream. Slugs are on Venues. You only receive venues included when the firehose was enabled for your app. An empty enablement list means every venue your app is allowed to see. This message cannot add venues beyond that list.

latency follows the orderbook tiers. Omit it, or send "conflated", for the default 100 ms tier. "realtime" opts in. seq is per tier here too.

batchMs is milliseconds and applies only to realtime. 0 sends each change as an orderbook_delta. Any other value groups those changes into orderbook_delta_batch over that many milliseconds. The conflated tier ignores batchMs. It sends one orderbook_delta_batch per 100 ms flush, covering the books that changed in that window. Set batchMs when you opt into realtime, including 0 when you want no grouping.

For each venue the socket then sends:

  1. subscribed
  2. One or more orderbook_snapshot_batch messages
  3. firehose_ready
  4. orderbook_delta_batch, or orderbook_delta when realtime batchMs is 0
{ "type": "subscribed", "channel": "firehose" }
{ "type": "firehose_ready", "venue": "polymarket" }

Ignore fields on those acks that you do not use. firehose_ready means that venue's snapshot has been sent. Live deltas for the venue follow. A venue snapshot is every non-empty book at the same full depth as a per-outcome snapshot. Read the socket while it arrives.

A server sends a limited number of venue snapshots at once. When several sockets subscribe together, yours can get subscribed right away and its first orderbook_snapshot_batch some seconds later. Heartbeats continue meanwhile. Keep the socket open and keep reading; do not resubscribe.

Messages

Each entry in a batch is a complete orderbook_snapshot or orderbook_delta object, the same objects as the per-outcome stream. The batch envelopes are the only new message types.

{
  "type": "orderbook_snapshot_batch",
  "venue": "polymarket",
  "part": 12,
  "parts": 480,
  "snapshots": [
    {
      "type": "orderbook_snapshot",
      "outcomeId": "vmo_1",
      "seq": 1710000001,
      "checksum": 2918476531,
      "bids": [[0.55, 800, { "polymarket": 800 }]],
      "asks": [[0.56, 600, { "polymarket": 600 }]],
      "venueOrderbooks": {
        "polymarket": {
          "bids": [[0.55, 800]],
          "asks": [[0.56, 600]]
        }
      },
      "venues": {
        "polymarket": { "bestBid": 0.55, "bestAsk": 0.56 }
      },
      "midpoint": 0.555,
      "spread": 0.01,
      "timestamp": 1710000000000
    }
  ]
}

part and parts identify the slice of that venue's snapshot. Apply every slice. Do not treat the venue as live until firehose_ready for it. Books in different slices do not depend on each other. Replace a book on every snapshot object, whatever its seq is.

{
  "type": "orderbook_delta_batch",
  "deltas": [
    {
      "type": "orderbook_delta",
      "outcomeId": "vmo_1",
      "seq": 1710000002,
      "checksum": 3847261950,
      "bidChanges": [[0.55, 900, { "polymarket": 900 }]],
      "askChanges": [],
      "venueDeltaBooks": {
        "polymarket": {
          "bidChanges": [[0.55, 900]],
          "askChanges": []
        }
      },
      "venues": {
        "polymarket": { "bestBid": 0.55, "bestAsk": 0.56 }
      },
      "midpoint": 0.555,
      "spread": 0.01,
      "timestamp": 1710000001000
    }
  ]
}

A delta batch is not split by venue. Each delta names its outcomeId. With realtime batchMs set to 0, the socket sends that orderbook_delta object on its own, with no envelope.

Empty books are omitted. A book that does not appear in the snapshot has no levels. When a book first gets levels, it arrives as an orderbook_snapshot by itself, not inside a batch. Apply it the same way as any other snapshot.

Drop a delta whose seq is less than or equal to the book's current seq. Deltas from before that book's snapshot can still show up, and they are stale. For example, a batch can repeat a change that a single orderbook_snapshot sent just before it already includes. After a resync, rebuild from the new snapshot and drop books that are no longer in it. They are empty.

Falling behind

The per-outcome rule (about 1 MB behind for 10 seconds) does not apply to this channel. If your backlog of live deltas exceeds 64 MB, or you fall more than 30 seconds behind, the socket sends the error below. While a venue snapshot is being sent, 30 seconds behind means the socket accepted none of it for 30 seconds. Deltas that arrive during the snapshot count toward the 64 MB, so read the snapshot quickly.

{
  "type": "error",
  "code": "firehose_resync"
}

A fresh snapshot follows: orderbook_snapshot_batch slices, then firehose_ready for the venues you are streaming. Replace your books.

Three firehose_resync errors in five minutes closes the connection as a slow consumer (4008). Reconnect with backoff and subscribe again.

firehose_resync is also sent when our upstream feed is lost or replaced. The fresh snapshot follows once the server's books are complete again, which can take some seconds. Keep reading. This resync does not count toward the three.

On this page