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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.aftermath.finance/for-developers/api/websockets.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
