WebSockets
Live market-data streams over WebSocket for the Aftermath Perpetuals API.
All Aftermath Perpetuals live data flows over a single WebSocket:
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 dedicated candle endpoint /api/perpetuals/ws/market-candles/{market_id}/{interval_ms} was removed. Candles now arrive through a marketCandles subscription on /ws/updates. See Migrating from /ws/market-candles.
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 withSubscription 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
400with the standard JSONErrorResponsepayload (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:
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.).
The market frame does not carry freeze state — it always reports isFrozen: false. Read the real value from the REST market endpoints before acting on it.
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 resolution — resolution 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.
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
orderbookdeltas, seed fromPOST /api/ccxt/orderbookbefore applying frames so a reconnect replaces rather than merges. FortopOfOrderbookeach frame is already a full snapshot of the top-N, so no REST seed is required.Use the
nonce: theorderbookstream'snoncestrictly 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.
oraclein 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/updatesproxy 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