> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agg.market/llms.txt
> Use this file to discover all available pages before exploring further.

# WebSocket Protocol

> Stream live orderbooks, trades, arbitrage returns, heartbeats, and user events over WebSocket

# 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.

```text theme={null}
wss://ws.agg.market/ws?appId=YOUR_APP_ID
```

## Getting connected

<Steps>
  <Step title="Open a connection">
    Pass your `appId` as a query parameter.

    ```javascript theme={null}
    const ws = new WebSocket(`wss://ws.agg.market/ws?appId=${appId}`);
    ```

    If the user is already signed in, you can also include `&token=eyJ...` for immediate
    user-level authentication.
  </Step>

  <Step title="Wait for the connected message">
    The server acknowledges the connection with app and optional user context:

    ```json theme={null}
    {
      "type": "connected",
      "appId": "app_demo123",
      "userId": "usr_xyz789"
    }
    ```

    `userId` is present only when a valid JWT is supplied on connect.
  </Step>

  <Step title="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.

    ```json theme={null}
    {
      "action": "subscribe",
      "channel": "orderbook",
      "outcomeIds": ["vmo_1", "vmo_2"]
    }
    ```

    `outcomeId` is a `VenueMarketOutcome.id` (venue-scoped, not cross-venue).
  </Step>
</Steps>

<Info>
  A single connection supports up to **100 total subscriptions** across orderbook and trade
  channels. Requests beyond that limit return an `error` message.
</Info>

## 1. Orderbook stream

Subscribe to an outcome's aggregated orderbook to receive a full snapshot followed by incremental
deltas. Each outcome has its own orderbook — there is no complement derivation. Aggregated levels
include per-venue attribution. Per-venue books are also included.

### Subscribe

```json theme={null}
{
  "action": "subscribe",
  "channel": "orderbook",
  "outcomeIds": ["vmo_1"]
}
```

You receive a confirmation before the initial snapshot:

```json theme={null}
{
  "type": "subscribed",
  "outcomeIds": ["vmo_1"],
  "channel": "orderbook"
}
```

### Initial snapshot

```json theme={null}
{
  "type": "orderbook_snapshot",
  "outcomeId": "vmo_1",
  "seq": 1710000001,
  "checksum": 2918476531,
  "bids": [
    [0.55, 1500, { "predict": 800, "polymarket": 700 }],
    [0.54, 900, { "predict": 400, "polymarket": 500 }]
  ],
  "asks": [
    [0.56, 1200, { "predict": 600, "polymarket": 600 }],
    [0.57, 800, { "polymarket": 800 }]
  ],
  "venueOrderbooks": {
    "predict": {
      "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": {
    "predict": { "bestBid": 0.55, "bestAsk": 0.56 },
    "polymarket": { "bestBid": 0.55, "bestAsk": 0.56 }
  },
  "midpoint": 0.555,
  "spread": 0.01,
  "timestamp": 1710000000000
}
```

<Tip>
  Aggregated levels use the wire format `[price, totalSize, { venue: sizeAtPrice }]`. Per-venue
  levels use `[price, size]`. Bids are sorted descending by price, asks ascending.
</Tip>

### Incremental deltas

```json theme={null}
{
  "type": "orderbook_delta",
  "outcomeId": "vmo_1",
  "seq": 1710000002,
  "checksum": 3847261950,
  "bidChanges": [
    [0.55, 1600, { "predict": 900, "polymarket": 700 }]
  ],
  "askChanges": [],
  "venueDeltaBooks": {
    "predict": {
      "bidChanges": [[0.55, 900]],
      "askChanges": []
    }
  },
  "venues": {
    "predict": { "bestBid": 0.55, "bestAsk": 0.56 },
    "polymarket": { "bestBid": 0.55, "bestAsk": 0.56 }
  },
  "midpoint": 0.555,
  "spread": 0.01,
  "timestamp": 1710000001000
}
```

### Sequencing and resync

Every orderbook message includes a monotonic `seq` value for that outcome. Your client is
responsible for maintaining local book state and detecting integrity issues. The gateway is a
stateless fan-out — it forwards deltas from the aggregation engine without server-side seq tracking.

**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.

**Drop stale deltas** where `delta.seq <= book.seq` — these can arrive after a subscribe when
Kafka 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:

```json theme={null}
{
  "action": "resnapshot",
  "outcomeIds": ["vmo_1"]
}
```

<Tip>
  The `@agg-build/sdk` handles all of this automatically — seq tracking, stale delta filtering,
  checksum validation, and resnapshot requests. Use `AggWebSocket` or the `useLiveMarket` hook
  and these details are managed for you.
</Tip>

### Raw WebSocket example

This keeps the protocol logic visible without pulling in SDK helpers:

```javascript theme={null}
const ws = new WebSocket(`wss://ws.agg.market/ws?appId=${appId}`);

ws.onopen = () => {
  ws.send(
    JSON.stringify({
      action: "subscribe",
      channel: "orderbook",
      outcomeIds: ["vmo_1"],
    }),
  );
};

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);

  switch (msg.type) {
    case "subscribed":
      console.log("Subscribed:", msg.outcomeIds, msg.channel);
      break;
    case "orderbook_snapshot":
      replaceLocalBook(msg);
      break;
    case "orderbook_delta":
      applyDeltaToLocalBook(msg);
      break;
    case "heartbeat":
      lastHeartbeatAt = msg.ts;
      break;
    case "error":
      console.error(msg.message);
      break;
  }
};
```

For SDK, hooks, and UI implementations, see [Real-Time Orderbook](/recipes/websocket-orderbook).

## 2. Trade stream

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

### Subscribe

```json theme={null}
{
  "action": "subscribe",
  "channel": "trades",
  "outcomeIds": ["vmo_1"]
}
```

Confirmation:

```json theme={null}
{
  "type": "subscribed",
  "outcomeIds": ["vmo_1"],
  "channel": "trades"
}
```

### Trade event

```json theme={null}
{
  "type": "trade",
  "outcomeId": "vmo_1",
  "venue": "predict",
  "side": "buy",
  "price": 0.55,
  "size": 100,
  "timestamp": 1710000000000
}
```

### Unsubscribe

```json theme={null}
{
  "action": "unsubscribe",
  "channel": "trades",
  "outcomeIds": ["vmo_1"]
}
```

For chart-building patterns on top of orderbook and trade data, see
[Real-Time Charts](/recipes/websocket-charts).

## 3. Authenticated user events

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

### Authenticate

<Tabs>
  <Tab title="On connect">
    Include the JWT in the connection URL:

    ```text theme={null}
    wss://ws.agg.market/ws?appId=YOUR_APP_ID&token=eyJhbG...
    ```
  </Tab>

  <Tab title="Mid-session">
    Upgrade an existing app-level connection after sign-in:

    ```json theme={null}
    {
      "action": "authenticate",
      "token": "eyJhbGciOiJIUzI1NiIs..."
    }
    ```

    ```json theme={null}
    {
      "type": "authenticated",
      "userId": "usr_xyz789"
    }
    ```
  </Tab>
</Tabs>

<Tip>
  Mid-session auth supports re-authentication after token refresh or user switching without opening
  a second socket.
</Tip>

### Order submitted

```json theme={null}
{
  "type": "order_submitted",
  "venue": "predict",
  "orderId": "ord_abc123",
  "side": "buy",
  "price": 0.55,
  "size": 100,
  "outcomeId": "vmo_1",
  "timestamp": 1710000000000
}
```

### Balance update

```json theme={null}
{
  "type": "balance_update",
  "venue": "predict",
  "tradingBalanceCents": 94500,
  "walletBalanceCents": 100000,
  "timestamp": 1710000000000
}
```

### Order event

Venue-agnostic stream of DAG 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)

```json theme={null}
{
  "type": "order_event",
  "event": "step_started",
  "userId": "usr_xyz789",
  "orderId": "ord_abc123",
  "dagRunId": "dag_mnp...",
  "stepId": "execute-...",
  "stepType": "submit-order",
  "sequence": 3,
  "totalSteps": 7,
  "venue": "polymarket",
  "timestamp": 1710000000000
}
```

`event` values: `step_started` | `step_completed` | `step_waiting` | `step_failed` | `step_retrying`.

`sequence` is the 1-based declaration index of the step within the DAG, 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 DAGs, `sequence` reflects declaration order, not strict execution order — parallel branches may emit non-monotonic sequence values.

#### Terminal fill (success toast + position refresh)

```json theme={null}
{
  "type": "order_event",
  "event": "filled",
  "userId": "usr_xyz789",
  "orderId": "ord_abc123",
  "venue": "polymarket",
  "filledAmountRaw": "1000000",
  "remainingAmountRaw": "0",
  "timestamp": 1710000000000
}
```

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

#### Failure (error toast)

```json theme={null}
{
  "type": "order_event",
  "event": "failed",
  "userId": "usr_xyz789",
  "orderId": "ord_abc123",
  "venue": "polymarket",
  "errorReason": "Polymarket CLOB error (400): ...",
  "timestamp": 1710000000000
}
```

`event` values: `failed` (order-level) | `dag_failed` (DAG-level).

#### DAG lifecycle (optional)

```json theme={null}
{
  "type": "order_event",
  "event": "dag_started",
  "userId": "usr_xyz789",
  "orderId": "ord_abc123",
  "dagRunId": "dag_mnp...",
  "templateName": "route-execution",
  "totalSteps": 7,
  "timestamp": 1710000000000
}
```

`event` values: `dag_started` | `dag_completed` | `dag_failed` | `dag_cancelled`.

<Note>
  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.
</Note>

For a notification-focused recipe, see [User Notifications](/recipes/websocket-notifications).

## 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](/api-reference/discovery/listvenueevents); 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.

<Info>
  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](/api-reference/discovery/listvenueevents).
</Info>

### Per-market channel

Subscribe with one or more `marketId`s (a `VenueMarket.id`) to receive an update whenever that
market's arbitrage return moves.

```json theme={null}
{
  "action": "subscribe",
  "channel": "arb",
  "marketIds": ["vm_1", "vm_2"]
}
```

Confirmation (the subscribed market ids are echoed in `outcomeIds`):

```json theme={null}
{
  "type": "subscribed",
  "outcomeIds": ["vm_1", "vm_2"],
  "channel": "arb"
}
```

Update message — published per market as the value changes:

```json theme={null}
{
  "type": "arb_market_update",
  "marketId": "vm_1",
  "activeVenuesOnly": true,
  "venueEventId": "ve_1",
  "arbReturn": 0.012,
  "ts": 1710000000000
}
```

`activeVenuesOnly: true` certifies that the producer excluded globally inactive venues. The
gateway fails closed and drops arb messages from older producers that omit this marker.
`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:

```json theme={null}
{
  "action": "unsubscribe",
  "channel": "arb",
  "marketIds": ["vm_1"]
}
```

### 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.

```json theme={null}
{
  "action": "subscribe",
  "channel": "arb-feed"
}
```

Confirmation:

```json theme={null}
{
  "type": "subscribed",
  "channel": "arb-feed"
}
```

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):

```json theme={null}
{
  "type": "arb_feed_batch",
  "feed": "arb",
  "activeVenuesOnly": true,
  "entries": [
    { "marketId": "vm_1", "venueEventId": "ve_1", "arbReturn": 0.012, "ts": 1710000000000 },
    { "marketId": "vm_2", "venueEventId": "ve_1", "arbReturn": 0.004, "ts": 1710000000000 }
  ],
  "flushTs": 1710000000300,
  "chunk": 0,
  "chunkCount": 1
}
```

<Tip>
  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.
</Tip>

Unsubscribe:

```json theme={null}
{
  "action": "unsubscribe",
  "channel": "arb-feed"
}
```

<Tip>
  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](/recipes/websocket-arb).
</Tip>

## Connection management

### Heartbeat

The service emits a JSON heartbeat event that clients can observe directly:

```json theme={null}
{
  "type": "heartbeat",
  "ts": 1710000000000
}
```

<Note>Use the JSON `heartbeat` event if you want an app-level liveness timer in a raw client.</Note>

### Reconnection

If the connection closes, reconnect with exponential backoff and re-subscribe to your channels:

```javascript theme={null}
function connect(attempt = 0) {
  const ws = new WebSocket(`wss://ws.agg.market/ws?appId=${appId}`);

  ws.onclose = () => {
    const delay = Math.min(1000 * 2 ** attempt + Math.random() * 1000, 30000);
    setTimeout(() => connect(attempt + 1), delay);
  };

  ws.onopen = () => {
    attempt = 0;
    resubscribeAll(ws);
  };
}
```

### Error handling

Protocol or validation errors arrive as JSON messages:

```json theme={null}
{
  "type": "error",
  "message": "Invalid outcomeIds: must be a non-empty array"
}
```

Fatal connection failures use close codes:

| Code   | Meaning                                   |
| ------ | ----------------------------------------- |
| `4001` | Missing `appId` query parameter           |
| `4003` | Invalid app or token scope not accepted   |
| `1001` | Connection closed; reconnect with backoff |

## Related

<CardGroup cols={2}>
  <Card title="Setup Guide" icon="gear" href="/api/setup">
    Provider wiring, auth setup, and SDK client initialization.
  </Card>

  <Card title="Real-Time Orderbook" icon="code" href="/recipes/websocket-orderbook">
    SDK, hooks, and UI orderbook implementations.
  </Card>

  <Card title="Real-Time Charts" icon="brackets-curly" href="/recipes/websocket-charts">
    Build live candles from REST history plus WebSocket events.
  </Card>

  <Card title="Real-Time Arbitrage" icon="arrow-right-arrow-left" href="/recipes/websocket-arb">
    Live cross-venue arbitrage returns via per-market and feed channels.
  </Card>

  <Card title="User Notifications" icon="key" href="/recipes/websocket-notifications">
    Authenticated order and balance events with refresh-aware reconnect logic.
  </Card>
</CardGroup>
