For the complete documentation index, see llms.txt. This page is also available as Markdown.

WebSockets

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

All Aftermath Perpetuals live data flows over a single WebSocket:

Endpoint
Purpose

wss://aftermath.finance/api/perpetuals/ws/updates

Unified live-updates proxy for multiplexing every perpetuals stream — orderbook, oracle, market, account, and candles — on one socket

The stream is bidirectional — the client sends subscribe / unsubscribe messages and the server pushes JSON frames on each update.

The endpoint is visible in the Swagger UI 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 can source markets from several providers (Pyth, Stork, SEDA, Switchboard). The WebSocket frame is provider-agnostic — it always delivers the aggregated price the protocol uses on-chain. marketParams.basePriceFeedId is the protocol's internal numeric feed storage ID, not a provider-specific feed ID. See Market Specifications for the current market feeds and Oracles for the aggregation model.

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.

marketCandles — live OHLCV candles

Subscribe:

interval is a CCXT-style timeframe label — one of 1m, 5m, 15m, 30m, 1h, 4h, 12h, 1d, 3d, 1w, 1mo. Subscribe once per interval you need; a single socket can carry several.

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.

For historical candles, use POST /api/perpetuals/market/candle-history with marketId, fromTimestamp, toTimestamp, and resolutionresolution takes the same timeframe labels.

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.

Migrating from /ws/market-candles

The dedicated push-only candle endpoint /api/perpetuals/ws/market-candles/{market_id}/{interval_ms} no longer exists. Candles are now one more subscription on /ws/updates.

Before
Now

One socket per market and interval

One socket, one marketCandles subscription per market and interval

Market and interval encoded in the URL path

Sent in the subscribe frame as marketId and interval

Interval in milliseconds (60000)

Timeframe label ("1m")

Frame is { marketId, lastCandle }

Frame is { marketCandles: { marketId, interval, lastCandle } }

No subscription ack

Plain-text ack after subscribe, as with every other stream

The same rename applies over REST: POST /api/perpetuals/market/candle-history takes resolution as a timeframe label, replacing the old intervalMs. CCXT OHLCV uses timeframe.

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