WebSockets

Live market-data streams over WebSocket for the Aftermath Perpetuals API.

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 UIarrow-up-right 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.

{
  "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:

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:

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:

Server frame:

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:

Server frame:

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 (Pytharrow-up-right, Storkarrow-up-right, SEDAarrow-up-right, Switchboardarrow-up-right). 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 Specificationsarrow-up-right) or the Oracles page.

market — market configuration and state

Subscribe:

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

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

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.

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

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

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

Python

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.

Last updated