> For the complete documentation index, see [llms.txt](https://docs.aftermath.finance/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.aftermath.finance/for-developers/api/websockets.md).

# WebSockets

The Aftermath Perpetuals API exposes two live streams over WebSocket:

| Endpoint                                                                             | Purpose                                                                                    |
| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ |
| `wss://aftermath.finance/api/perpetuals/ws/updates`                                  | Unified live-updates proxy for multiplexing multiple perpetuals streams on a single socket |
| `wss://aftermath.finance/api/perpetuals/ws/market-candles/{market_id}/{interval_ms}` | Live OHLCV candle stream for one market                                                    |

The `/ws/updates` stream is bidirectional — the client sends subscribe / unsubscribe messages and the server pushes JSON frames on each update. The `/ws/market-candles` stream is push-only; the market and interval come from the URL and no subscribe message is required.

Both endpoints are visible in the [Swagger UI](https://aftermath.finance/docs/) under the `perpetuals` tag. The Swagger page only documents the HTTP upgrade handshake — the frame schemas below are the actual wire protocol observed in production.

## `/api/perpetuals/ws/updates`

### Frame model

* **Client → Server**: JSON-encoded `ClientSubscriptionMessage` — one per subscribe / unsubscribe action.
* **Server → Client**: JSON-encoded `ClientWsEnvelope` — one top-level key per frame naming the stream that produced it.
* **Subscription ack**: immediately after a successful `subscribe`, the server sends a non-JSON plain-text frame starting with `Subscription established. ...` summarising current subscriptions. Clients should tolerate frames that do not parse as JSON and ignore them.
* **Runtime error frames**: `{ "error": "<kind>", "detail": "<human-readable>" }`. After a runtime error frame the server closes the socket.
* **Failed upgrades**: if the HTTP upgrade itself fails, the server returns a normal HTTP `400` with the standard JSON `ErrorResponse` payload (`error_code`, `message`, `short_message`).

### Subscribing

Send a message of the shape below on `open`. `subscriptionType` is a tagged object — exactly one stream key per message.

```json
{
  "action": "subscribe",
  "subscriptionType": {
    "topOfOrderbook": {
      "marketId": "0x<market-id>",
      "priceBucketSize": 0.0001,
      "bucketsNumber": 10
    }
  }
}
```

Use `"action": "unsubscribe"` with the same `subscriptionType` payload to stop a stream without closing the connection. A single socket can hold multiple subscriptions at once — send one message per stream you want to add.

### Integer serialisation

Several fields in the `market` frame (and elsewhere) come as decimal strings with a trailing `n` — for example `"3600000n"` or `"100n"`. These represent `u128` / `i128` values that exceed safe JavaScript number range. Strip the `n` and parse as BigInt or decimal string on receipt. Fixed-precision decimals such as prices and sizes are serialised as JSON numbers.

### Stream types

#### `topOfOrderbook` — bucketed top-of-book snapshots

Subscribe:

```json
{
  "action": "subscribe",
  "subscriptionType": {
    "topOfOrderbook": {
      "marketId": "0x<market-id>",
      "priceBucketSize": 0.0001,
      "bucketsNumber": 10
    }
  }
}
```

| Field             | Type    | Notes                                                                             |
| ----------------- | ------- | --------------------------------------------------------------------------------- |
| `marketId`        | string  | Hex market id (e.g. the `id` field returned by `GET /api/ccxt/markets`).          |
| `priceBucketSize` | number  | Price-tick grouping. Pass the market's tick (e.g. `0.0001`) for ungrouped levels. |
| `bucketsNumber`   | integer | Levels per side. Verified values: `10`, `25`, `50`, `100`, `200`, `500`.          |

Server frame:

```json
{
  "topOfOrderbook": {
    "marketId": "0x<market-id>",
    "bids": [
      { "price": 78011.3233, "size": 0.001923, "totalSize": 0.001923, "sizeUsd": 150.015, "totalSizeUsd": 150.015 },
      { "price": 78011.1034, "size": 0.4136,   "totalSize": 0.4155,   "sizeUsd": 32266.37, "totalSizeUsd": 32416.38 }
    ],
    "asks": [ { "price": 78012.1, "size": 0.01, "totalSize": 0.01, "sizeUsd": 780.12, "totalSizeUsd": 780.12 } ]
  }
}
```

Each level carries the per-level `size` / `sizeUsd` plus the running cumulative `totalSize` / `totalSizeUsd` through that level. Frames are full snapshots of the top `bucketsNumber` levels per side, not deltas. Level order is not guaranteed — sort `asks` ascending and `bids` descending on receipt.

#### `orderbook` — full orderbook deltas

Subscribe:

```json
{
  "action": "subscribe",
  "subscriptionType": {
    "orderbook": {
      "marketId": "0x<market-id>",
      "depth": 50
    }
  }
}
```

Server frame:

```json
{
  "orderbook": {
    "marketId": "0x<market-id>",
    "orderbookDeltas": {
      "asksDeltas": [ { "size": 0.03, "price": 4714.2193 }, { "size": -0.03, "price": 4717.8511 } ],
      "bidsDeltas": [ { "size": 0.09, "price": 4698.3801 } ],
      "asksTotalSizeDelta": 0,
      "bidsTotalSizeDelta": 0,
      "nonce": "268002941n"
    }
  }
}
```

Deltas are additive: positive `size` adds liquidity to the level, negative `size` removes it, and a level that reaches zero size is considered cleared. Use the rising `nonce` to order deltas and detect gaps. Seed from `POST /api/ccxt/orderbook` before applying deltas; on gap, re-fetch the snapshot and resume.

If you are using the TypeScript SDK rather than the raw wire protocol, prefer its `subscribeOrderbook({ marketId })` helper and let the SDK manage the current subscription payload shape.

#### `oracle` — aggregated oracle price updates

Subscribe:

```json
{
  "action": "subscribe",
  "subscriptionType": { "oracle": { "marketId": "0x<market-id>" } }
}
```

Server frame:

```json
{
  "oracle": {
    "marketId": "0x<market-id>",
    "basePrice": 78034.7236,
    "collateralPrice": 0.99971
  }
}
```

`basePrice` is the base-asset oracle price; `collateralPrice` is the collateral oracle price (USDC ≈ 1). Frames arrive on each oracle tick — typically several times per second on active markets.

Aftermath's oracle aggregator sources each market from one of several providers ([Pyth](https://www.pyth.network/price-feeds), [Stork](https://www.stork.network/), [SEDA](https://seda.xyz/), [Switchboard](https://switchboard.xyz/)). The WebSocket frame is provider-agnostic — it always delivers the aggregated price the protocol uses on-chain. To see which provider backs a given market, consult the feed id in the `market` frame's `marketParams.basePriceFeedId` (cross-referenced with the tables under [Market Specifications](https://github.com/AftermathFinance/docs/blob/main/perpetuals/architecture/market-specifications/README.md)) or the [Oracles](/perpetuals/architecture/oracle-prices.md) page.

#### `market` — market configuration and state

Subscribe:

```json
{
  "action": "subscribe",
  "subscriptionType": { "market": { "marketId": "0x<market-id>" } }
}
```

A full snapshot is pushed on subscribe; further frames arrive when market state changes (funding, open interest, etc.).

Server frame (fields abridged for brevity; the full list mirrors the on-chain `Market` object):

```json
{
  "market": {
    "packageId": "0x<pkg>",
    "objectId": "0x<market-id>",
    "collateralCoinType": "0x<usdc-type>::usdc::USDC",
    "marketParams": {
      "marginRatioInitial": 0.05,
      "marginRatioMaintenance": 0.025,
      "baseAssetSymbol": "XAUTUSD",
      "basePriceFeedId": "0x<pyth-feed>",
      "collateralPriceFeedId": "0x<pyth-feed>",
      "fundingFrequencyMs": "3600000n",
      "fundingPeriodMs": "28800000n",
      "makerFee": -0.00005,
      "takerFee": 0.00045,
      "liquidationFee": 0.005,
      "minOrderUsdValue": 1,
      "lotSize": "100n",
      "tickSize": "100000n",
      "scalingFactor": 0.000001,
      "maxOpenInterest": 150
    },
    "marketState": {
      "cumFundingRateLong": -1.275,
      "cumFundingRateShort": -1.275,
      "fundingLastUpdateTimestamp": 1776931216534,
      "premiumTwap": -0.66,
      "spreadTwap": -0.56,
      "openInterest": 5.51,
      "feesAccrued": 4496.72
    },
    "collateralPrice": 0.9998,
    "indexPrice": 4705.49,
    "estimatedFundingRate": -0.0000176,
    "nextFundingTimestampMs": "1776934800000n"
  }
}
```

Use this stream if you need live `indexPrice`, `openInterest`, or the raw funding inputs without polling `/api/perpetuals/markets/*` REST endpoints. For funding calculations, derive the live rate from `marketState.premiumTwap / indexPrice`; do not treat `estimatedFundingRate` as the canonical 8h funding value.

#### Account and execution streams

The same `/ws/updates` socket also carries account- and execution-scoped updates. The OpenAPI description summarizes these broadly as `user` and `order` updates, while the current TypeScript SDK exposes higher-level helpers such as `subscribeUser`, `subscribeMarketOrders`, `subscribeUserOrders`, and `subscribeUserCollateralChanges`.

Treat those SDK helpers as the source of truth for these variants rather than hard-coding raw JSON from stale examples. Some account-scoped variants also require additional signed auth material, for example `withStopOrders` when including encrypted stop-order data.

### Error frame

```json
{ "error": "invalid_request", "detail": "unknown variant `foo`, expected one of ..." }
```

The socket is closed after a runtime error frame. Clients should treat `close` after an error as final and reconnect from a known-good state.

### Multiplexing

A single `/ws/updates` socket can carry any mix of the stream types above. Send one subscribe message per stream; frames arrive interleaved, keyed by the envelope top-level field. Unsubscribe from individual streams without closing the socket.

```ts
ws.send(JSON.stringify({ action: "subscribe", subscriptionType: { oracle: { marketId } } }));
ws.send(JSON.stringify({ action: "subscribe", subscriptionType: { orderbook: { marketId, depth: 50 } } }));
ws.send(JSON.stringify({ action: "subscribe", subscriptionType: { market: { marketId } } }));
```

## `/api/perpetuals/ws/market-candles/{market_id}/{interval_ms}`

Push-only candle stream. The path encodes the market and bucket width; no client message is sent.

```
wss://aftermath.finance/api/perpetuals/ws/market-candles/0x<market-id>/60000
```

streams one-minute candles for that market. Server frame:

```json
{
  "marketId": "0x<market-id>",
  "lastCandle": {
    "timestamp": 1776931380000,
    "open": 4704.7898,
    "close": 4704.90085,
    "high": 4704.90085,
    "low": 4704.7898,
    "volume": 0
  }
}
```

`lastCandle` is the rolling current bucket — its fields mutate with every push until the bucket closes, at which point a fresh `lastCandle` for the next bucket starts. `timestamp` is the UTC epoch-millis start of the bucket. No subscription ack is sent on this endpoint. Runtime errors, when present, use the same `{ "error", "detail" }` JSON shape as `/ws/updates`; invalid path parameters fail the HTTP upgrade with a `400` before the socket is established.

## Client examples

### Bun / TypeScript

```ts
const WS = "wss://aftermath.finance/api/perpetuals/ws/updates";

const markets = await fetch("https://aftermath.finance/api/ccxt/markets").then(r => r.json());
const marketId: string = markets[0].id;

const ws = new WebSocket(WS);

ws.onopen = () => {
  ws.send(JSON.stringify({
    action: "subscribe",
    subscriptionType: { topOfOrderbook: { marketId, priceBucketSize: 0.0001, bucketsNumber: 10 } },
  }));
  ws.send(JSON.stringify({
    action: "subscribe",
    subscriptionType: { oracle: { marketId } },
  }));
};

ws.onmessage = (e) => {
  const raw = String(e.data);
  // The server emits a plain-text ack after subscribe; ignore non-JSON frames.
  if (!raw.startsWith("{")) return;
  const msg = JSON.parse(raw);

  if (msg.error) {
    console.error("ws error:", msg);
    return;
  }
  if (msg.topOfOrderbook) {
    const top = msg.topOfOrderbook;
    const bestBid = [...top.bids].sort((a, b) => b.price - a.price)[0];
    const bestAsk = [...top.asks].sort((a, b) => a.price - b.price)[0];
    console.log(`bid ${bestBid?.price} × ${bestBid?.size}  /  ask ${bestAsk?.price} × ${bestAsk?.size}`);
    return;
  }
  if (msg.oracle) {
    console.log(`oracle base=${msg.oracle.basePrice} collateral=${msg.oracle.collateralPrice}`);
    return;
  }
  // Ignore unrecognised envelope keys — new stream types may be added.
};
```

### Python

```python
import json, asyncio, websockets, httpx

WS = "wss://aftermath.finance/api/perpetuals/ws/updates"

async def main():
    markets = httpx.get("https://aftermath.finance/api/ccxt/markets").json()
    market_id = markets[0]["id"]

    async with websockets.connect(WS) as ws:
        await ws.send(json.dumps({
            "action": "subscribe",
            "subscriptionType": {
                "topOfOrderbook": {"marketId": market_id, "priceBucketSize": 0.0001, "bucketsNumber": 10}
            }
        }))

        async for raw in ws:
            if not raw.startswith("{"):
                continue  # subscription-established ack
            msg = json.loads(raw)
            if "error" in msg:
                print("error:", msg); break
            top = msg.get("topOfOrderbook")
            if not top: continue
            asks = sorted(top["asks"], key=lambda l: l["price"])
            bids = sorted(top["bids"], key=lambda l: l["price"], reverse=True)
            if asks and bids:
                print(f"bid {bids[0]['price']} / ask {asks[0]['price']}")

asyncio.run(main())
```

## Operational guidance

* **Snapshot before delta**: for `orderbook` deltas, seed from `POST /api/ccxt/orderbook` before applying frames so a reconnect replaces rather than merges. For `topOfOrderbook` each frame is already a full snapshot of the top-N, so no REST seed is required.
* **Use the `nonce`**: the `orderbook` stream's `nonce` strictly increases. Treat a gap as a resync signal — drop local state and re-fetch the snapshot.
* **Reconnect with backoff**: exponential backoff starting at 1s, capped at 30s. Re-send every subscription on each successful `open`.
* **Detect silent sockets**: keep a watchdog that falls back to REST polling if no frames arrive for \~5s on an open socket. `oracle` in particular updates many times per second on active markets; prolonged silence means the socket is stuck.
* **Ignore unrecognised envelope keys**: the envelope is extensible — new top-level stream keys may be added. Route on the key you subscribed to and ignore the rest.
* **Public endpoint**: the `/ws/updates` proxy is public and does not require authentication on the socket itself. Account-scoped streams are keyed by public account identifiers, but some optional account data exposed by the SDK (such as stop-order details) requires signed auth material in the subscription payload.
