Skip to content
aeon/feed

WebSocket protocol

All messages are JSON text frames. The current protocol version is v1. Every server message has a type and a ts (server time in ms). The message reference below shows every message's JSON and explains each field.

Connection lifecycle

  1. Connect to /ws (optionally with ?subscribe=ch1,ch2).
  2. The server sends welcome.
  3. You send subscribe. For each new channel the server replies with subscribed, then a snapshot.
  4. The server streams ticker / agg deltas until you unsubscribe or disconnect.

Client → server

opFieldsDescription
subscribechannels: string[]Add channels. Already subscribed channels are ignored.
unsubscribechannels: string[]Remove channels.
pingApplication-level ping. The server replies with pong.
json
{ "op": "subscribe", "channels": ["ticker:SOLUSDT"] }
{ "op": "unsubscribe", "channels": ["ticker:SOLUSDT"] }
{ "op": "ping" }

Server → client

typeDescription
welcomeSent once on connect: version, pairs, exchanges, limits.
subscribed / unsubscribedAcknowledges channel changes.
snapshotFull current state of one channel, sent right after subscribing (and after a resync).
tickerChanged exchange quotes for one pair (real-time).
aggChanged cross-exchange aggregates (every second).
pongReply to ping.
errorcode and msg. The connection stays open.

Timestamps

All timestamps are Unix epoch milliseconds from the server's clock.

FieldWhereMeaning
tsevery server messageWhen the server built this message. Use it to measure delivery latency (Date.now() - ts) and to correct for your local clock (offset = welcome.ts - Date.now()). Cached agg:* snapshots can be up to 1 s older than the moment they are sent.
teach quote in ticker / snapshotReceive time: when the quote arrived from the exchange. Unchanged quotes are re-sent at least every 10 s with a fresh t.
eteach quote, optionalExchange event time: the timestamp the exchange itself put on the update. t - et is the lag: network latency plus how old the exchange's state already was. Feeds that only emit on trades (Coinbase ticker) can show large lags on illiquid pairs, which is a useful staleness signal. Missing for Binance, Bitfinex and Gate, which don't send one.
teach row in agg / snapshotThe newest t among the quotes in that aggregate.

Message reference

welcome

Sent once, right after you connect.

json
{
  "type": "welcome",
  "ts": 1731400000000,
  "version": "v1",
  "channels": ["agg:*", "ticker:<SYMBOL>"],
  "pairs": ["BTCUSDT", "ETHUSDT", "SOLUSDT"],
  "exchanges": ["binance", "bybit", "okx"],
  "limits": {
    "max_subscriptions": 200,
    "max_connections_per_ip": 10,
    "ticker_interval_ms": 0,
    "agg_interval_ms": 1000
  },
  "docs": "/docs",
  "disclaimer": "For informational purposes only. Not investment advice. See /terms"
}
FieldTypeDescription
typestringAlways "welcome"
tsnumberServer time (ms). Compute your clock offset as ts - Date.now().
versionstringProtocol version ("v1")
channelsstring[]Channel patterns you can subscribe to
pairsstring[]All published symbols
exchangesstring[]Exchange ids that may appear in ex
limits.max_subscriptionsnumberMax channels per connection
limits.max_connections_per_ipnumberMax WebSocket connections per IP
limits.ticker_interval_msnumber0 = tickers are real-time
limits.agg_interval_msnumberHow often agg:* is published
docs / disclaimerstringDocumentation path and usage notice

subscribed / unsubscribed

Confirms a subscribe or unsubscribe request. It lists only the channels that actually changed.

json
{ "type": "subscribed", "ts": 1731400000012, "channels": ["ticker:BTCUSDT", "agg:*"] }
FieldTypeDescription
typestring"subscribed" or "unsubscribed"
tsnumberServer time (ms)
channelsstring[]Channels added or removed

snapshot (ticker channel)

The full current state of one pair, sent right after you subscribe and again if your connection falls behind. Replace your local state for that pair.

json
{
  "type": "snapshot",
  "ts": 1731400000013,
  "channel": "ticker:BTCUSDT",
  "s": "BTCUSDT",
  "data": [
    { "ex": "binance", "b": 97012.5, "a": 97012.51, "bq": 3.214, "aq": 0.82, "t": 1731400000001 },
    { "ex": "okx", "b": 97011.9, "a": 97012.0, "bq": 0.51, "aq": 1.07, "t": 1731399999987, "et": 1731399999861 }
  ]
}

ticker

Real-time updates for one pair. Each message contains only the exchanges whose quote changed: merge them into your state by ex.

json
{
  "type": "ticker",
  "ts": 1731400000140,
  "channel": "ticker:BTCUSDT",
  "s": "BTCUSDT",
  "data": [
    { "ex": "okx", "b": 97012.1, "a": 97012.2, "bq": 0.33, "aq": 2.4, "t": 1731400000139, "et": 1731400000012 }
  ]
}
FieldTypeDescription
typestring"ticker" (or "snapshot")
tsnumberServer time (ms) when the message was built
channelstringticker:<SYMBOL>
sstringSymbol, e.g. BTCUSDT
dataarrayQuotes, one per exchange (fields below)
data[].exstringExchange id: binance, bybit, okx, mexc, gateio, kucoin, bitget, coinbase, kraken, bitfinex, upbit, cryptocom
data[].bnumberBest bid price
data[].anumberBest ask price
data[].bqnumberSize at the best bid (base asset, e.g. BTC)
data[].aqnumberSize at the best ask (base asset)
data[].tnumberReceive time: when the quote arrived from the exchange (ms). Unchanged quotes are re-sent at least every 10 s with a fresh t.
data[].etnumber, optionalExchange event time (ms) as reported by the exchange. t - et ≈ latency. Omitted for Binance, Bitfinex and Gate.

snapshot (agg:*)

The current summary of all pairs, sent right after you subscribe to agg:*. Replace your local state.

json
{
  "type": "snapshot",
  "ts": 1731400000000,
  "channel": "agg:*",
  "data": [
    { "s": "BTCUSDT", "p": 97012.3, "bb": 97014.0, "bbx": "kraken", "bbq": 0.42, "ba": 97011.8, "bax": "bybit", "baq": 1.37, "sp": -0.0023, "n": 11, "ch": 1.84, "v": 1834455120, "t": 1731399999990 }
  ]
}

agg

Sent every second with only the pairs that changed: merge by s. A row with n: 0 means the pair is currently unavailable, so remove it.

json
{
  "type": "agg",
  "ts": 1731400001000,
  "channel": "agg:*",
  "data": [
    { "s": "ETHUSDT", "p": 3561.2, "bb": 3561.3, "bbx": "okx", "bbq": 4.1, "ba": 3561.1, "bax": "binance", "baq": 2.0, "sp": -0.0056, "n": 12, "ch": 2.1, "v": 912003311, "t": 1731400000990 }
  ]
}
FieldTypeDescription
typestring"agg" (or "snapshot")
tsnumberServer time (ms) when the message was built
channelstringagg:*
data[].sstringSymbol
data[].pnumberReference price: median of exchange mid prices
data[].bbnumberHighest bid across exchanges
data[].bbxstringExchange with the highest bid
data[].bbqnumberSize at that bid (base asset)
data[].banumberLowest ask across exchanges
data[].baxstringExchange with the lowest ask
data[].baqnumberSize at that ask (base asset)
data[].spnumberCross-exchange spread in % of p: (ba - bb) / p × 100. Negative = crossed books.
data[].nnumberExchanges with a fresh quote. 0 = pair unavailable, remove it.
data[].chnumber, optional24h change in % (reference exchange)
data[].vnumber, optional24h quote volume in USDT (reference exchange)
data[].tnumberNewest quote time among the included exchanges (ms)

pong

Reply to {"op":"ping"}.

json
{ "type": "pong", "ts": 1731400005000 }

error

Your request was rejected. The connection stays open.

json
{ "type": "error", "ts": 1731400000015, "code": "unknown_channel", "msg": "`ticker:NOPEUSDT` is not a published pair" }
FieldTypeDescription
codestringbad_request, unknown_channel or too_many_subscriptions
msgstringHuman-readable explanation

Errors

codeMeaning
bad_requestThe message is not valid JSON or has an unknown op.
unknown_channelThe channel name is malformed or the pair is not published.
too_many_subscriptionsThe per-connection channel limit was reached.

Connection-level rejections happen before the upgrade, as plain HTTP:

  • 429: too many connections from your IP.
  • 503: the server is at capacity. Retry later with backoff.

Heartbeats

The server sends a WebSocket ping frame every 30 s. Browsers and most libraries answer automatically. Connections with no traffic for 90 s are closed. A client that cannot keep up with sending is disconnected.

Reconnecting

Reconnect with exponential backoff and jitter (for example 0.5 s, 1 s, 2 s … capped at 15 s), then re-send your subscriptions. A fresh snapshot follows, so there is no state to repair. @aeon-feed/client does this for you.

Snapshots and resyncs

If your connection falls behind on a channel, the server skips the missed deltas and sends a new snapshot for that channel instead. Always treat a snapshot as replace, and a delta as merge.

Aeon Feed is free, with no ads and no paid tier. Donations keep it that way.

  • Keeping the project alive: servers and bandwidth for a free, public real-time feed.
  • Maintenance: keeping up with exchange API changes, fixing bugs, adding pairs and exchanges.
  • New features: cross-exchange arbitrage signals are next on the roadmap.
Tron (TRC-20)
Tether (USDT)
TXFraDKB…ZCAmpi

Send USDT only on the network you selected. TRC-20 has the lowest fees.