WebSocket Client Guide


1. Connection

ws://<host>:<port>/ws

Text JSON frames (UTF-8).

For testing, websocat is handy:

# Linux: prebuilt binary
cargo install websocat
# or download a prebuilt binary from https://github.com/vi/websocat/releases

websocat ws://<host>:<port>/ws

2. Protocol

Client → Server:

{ "method": "subscribe" | "unsubscribe", "subscription": { "type": "...", ... } }

Server → Client:

{ "channel": "...", "data": ... }

Here channel is the message type and data is its payload.

Exchange data arrives batched per block. It is not a tick-by-tick stream but a batch of events per block.


3. Subscriptions

7 types. The type field uses camelCase.

typeRequired fieldsOptional fieldsResponse channel
rawTradesfilterrawTrades
ordersfilterrawOrders
bookUpdatesfilterrawBookUpdates
eventsfilterrawEvents
twapfilterrawTwap
writerActionsfilterrawWriterActions
l2BookcoinnSigFigs, nLevels, mantissal2Book

3.1 rawTrades — executed trades (fills)

{ "method": "subscribe", "subscription": { "type": "rawTrades", "filter": { "coin": ["BTC"] } } }

3.2 orders — order statuses

open, filled, canceled, triggered, etc.

{ "method": "subscribe", "subscription": { "type": "orders", "filter": { "user": ["0xabc..."] } } }

3.3 bookUpdates — low-level book diffs

New / Update / Remove per each oid.

{ "method": "subscribe", "subscription": { "type": "bookUpdates", "filter": { "coin": ["ETH"] } } }

3.4 events — misc events

inner holds arbitrary JSON (the event type is in there as well).

{ "method": "subscribe", "subscription": { "type": "events", "filter": { "type": ["ValidatorRewards"] } } }

3.5 twap — TWAP order statuses

{ "method": "subscribe", "subscription": { "type": "twap" } }

3.6 writerActions — core-writer actions (EVM → L1)

{ "method": "subscribe", "subscription": { "type": "writerActions" } }

3.7 l2Book — aggregated book

filter does not apply. Use aggregation parameters instead:

ParameterTypeDefaultDescription
coinstring— (required)Ticker, e.g. "BTC"
nLevelsu3220How many levels to return per side (bids/asks), max 100
nSigFigsu32Significant figures for price rounding
mantissau64Mantissa for price aggregation
{ "method": "subscribe", "subscription": { "type": "l2Book", "coin": "BTC", "nLevels": 50 } }

Unsubscribe

Same structure, but method: "unsubscribe". Fields must match 1:1 with what you subscribed to.

{ "method": "unsubscribe", "subscription": { "type": "rawTrades", "filter": { "coin": ["BTC"] } } }

4. Filtering (StreamFilter)

Applies to all subscriptions except l2Book.

Format

"filter": {
  "<fieldName>": ["val1", "val2", ...],
  "<fieldName2>": ["val3"]
}

Field names follow the response payload (section 6), in camelCase.

Semantics

  • OR within a field"coin": ["BTC", "ETH"] matches either one.
  • AND across fields — specifying both coin and side requires both to match.
  • Case-insensitive"btc" matches "BTC".
  • Nested lookup — the key is searched at any JSON depth (including array elements). For example, "destination": ["0x7925"] is found inside action.destination.
  • Numbers and booleans are compared as strings. "oid": ["12345"] matches oid: 12345.

Special values

ValueMeaning
"*" or "exists"Field is present and not null
"null"Field is absent or null

Example (liquidations only):

{ "method": "subscribe", "subscription": { "type": "rawTrades", "filter": { "liquidation": ["*"] } } }

Limits

Exceed these and the subscription is rejected with an error.

ParameterLimit
Total values across the whole filter500
In the user / users field100
In the coin field50
In the type field20

5. Response channels

subscriptionResponse

Echo of a successful subscribe/unsubscribe:

{ "channel": "subscriptionResponse", "data": { "method": "subscribe", "subscription": { ... } } }

error

{ "channel": "error", "data": "Invalid subscription: ..." }

Reasons: invalid JSON, filter limits exceeded, empty coin in l2Book, duplicate subscribe/unsubscribe.

rawTrades

Array of [userAddress, Fill] pairs:

{ "channel": "rawTrades", "data": [ ["0xabc...", { "coin": "BTC", "px": "...", "sz": "...", ... }], ... ] }

rawOrders, rawBookUpdates, rawEvents, rawTwap, rawWriterActions

Array of objects of the corresponding type (see section 6).

l2Book

{
  "channel": "l2Book",
  "data": {
    "coin": "BTC",
    "time": 1713441234567,
    "levels": [
      [ { "px": "65000.0", "sz": "1.23", "n": 5 }, ... ],
      [ { "px": "65010.0", "sz": "0.87", "n": 3 }, ... ]
    ]
  }
}

levels[0] is bids, levels[1] is asks.


6. Payload structures

All fields are camelCase. Addresses are hex strings "0x...". Prices, sizes, and fees are strings (for precision).

Fill (element of rawTrades)

FieldTypeDescription
coinstringTicker
pxstringExecution price
szstringSize
sidestring"A" (ask) / "B" (bid)
timeu64Timestamp, ms
startPositionstringPosition before the trade
dirstringDirection
closedPnlstringRealized PnL
hashstringBlock hash
oidu64Order ID
crossedboolSpread cross
feestringFee
tidu64Trade ID
feeTokenstringFee token
liquidationLiquidation|nullLiquidation details (if any)

user in the filter is the hex address from the [userAddress, Fill] pair.

Liquidation: { liquidatedUser, markPx, method }.

OrderStatus (element of rawOrders)

FieldTypeDescription
timedatetimeEvent time
useraddressOwner
statusstring"open", "filled", "canceled", "triggered", ...
orderL4OrderThe order itself

L4Order:

FieldTypeDescription
useraddress|null
coinstring
sidestring"A" / "B"
limitPxstringLimit price
szstringSize
oidu64Order ID
timestampu64Created at, ms
triggerConditionstringFor trigger orders
isTriggerbool
triggerPxstringTrigger price
isPositionTpslboolPosition TP/SL
reduceOnlybool
orderTypestring"Limit", "Market", ...
tifstring|null"Ioc", "Gtc", ...
cloidstring|nullClient ID

OrderDiff (element of rawBookUpdates)

FieldTypeDescription
useraddressOwner
oidu64Order ID
pxstringLevel price
coinstringTicker
rawBookDiffenumWhat changed

rawBookDiff is one of:

  • { "new": { "sz": "..." } } — new order at the level
  • { "update": { "origSz": "...", "newSz": "..." } } — size change
  • "remove" — removal

MiscEvent (element of rawEvents)

FieldTypeDescription
timestringTimestamp
hashstringBlock hash
innerany JSONEvent body (the kind is inside as type)

TwapStatus (element of rawTwap)

FieldTypeDescription
timestringTimestamp
twapIdu64TWAP ID
stateany JSONState
statusstringCurrent status

WriterAction (element of rawWriterActions)

FieldTypeDescription
userstringAddress
nonceu64Nonce
evmTxHashstringEVM transaction hash
actionany JSONAction details

Level (in l2Book.levels)

FieldTypeDescription
pxstringLevel price
szstringTotal size
nusizeNumber of orders

7. Subscription examples

Ready-to-use JSON:

{ "method": "subscribe", "subscription": { "type": "rawTrades",     "filter": { "coin": ["BTC"] } } }
{ "method": "subscribe", "subscription": { "type": "rawTrades",     "filter": { "coin": ["ETH"], "liquidation": ["*"] } } }
{ "method": "subscribe", "subscription": { "type": "orders",        "filter": { "user": ["0xabcdef0123456789abcdef0123456789abcdef01"] } } }
{ "method": "subscribe", "subscription": { "type": "bookUpdates",   "filter": { "coin": ["BTC", "ETH"] } } }
{ "method": "subscribe", "subscription": { "type": "events",        "filter": { "type": ["ValidatorRewards"] } } }
{ "method": "subscribe", "subscription": { "type": "twap" } }
{ "method": "subscribe", "subscription": { "type": "writerActions" } }
{ "method": "subscribe", "subscription": { "type": "l2Book", "coin": "BTC", "nLevels": 50 } }

8. websocat one-liners

Set environment variables before running:

export HOST=localhost
export PORT=8000

Template: echo '<JSON>' | websocat -n ws://$HOST:$PORT/ws

  • echo sends the subscription to stdin
  • -n (no-close) keeps the connection open after EOF on stdin, so you keep receiving the stream
  • Stop with Ctrl+C

Ready-to-use commands (one per subscription):

# rawTrades — all BTC trades
echo '{"method":"subscribe","subscription":{"type":"rawTrades","filter":{"coin":["BTC"]}}}' | websocat -n ws://$HOST:$PORT/ws

# rawTrades — ETH liquidations only
echo '{"method":"subscribe","subscription":{"type":"rawTrades","filter":{"coin":["ETH"],"liquidation":["*"]}}}' | websocat -n ws://$HOST:$PORT/ws

# orders — orders for a specific user
echo '{"method":"subscribe","subscription":{"type":"orders","filter":{"user":["0xabcdef0123456789abcdef0123456789abcdef01"]}}}' | websocat -n ws://$HOST:$PORT/ws

# bookUpdates — BTC and ETH book diffs
echo '{"method":"subscribe","subscription":{"type":"bookUpdates","filter":{"coin":["BTC","ETH"]}}}' | websocat -n ws://$HOST:$PORT/ws

# events — ValidatorRewards events
echo '{"method":"subscribe","subscription":{"type":"events","filter":{"type":["ValidatorRewards"]}}}' | websocat -n ws://$HOST:$PORT/ws

# twap — all TWAP statuses
echo '{"method":"subscribe","subscription":{"type":"twap"}}' | websocat -n ws://$HOST:$PORT/ws

# writerActions — all core-writer actions
echo '{"method":"subscribe","subscription":{"type":"writerActions"}}' | websocat -n ws://$HOST:$PORT/ws

# l2Book — BTC order book, top 50 levels
echo '{"method":"subscribe","subscription":{"type":"l2Book","coin":"BTC","nLevels":50}}' | websocat -n ws://$HOST:$PORT/ws

Useful flags:

  • --ping-interval 30 — sends a ping every 30 seconds (so network equipment doesn't drop the idle connection)
  • --max-messages 10 — read exactly 10 messages and exit (handy for a sanity check)
  • timeout 60 … — wrap the call to auto-disconnect after 60 seconds

Example with a 10-message limit and ping:

echo '{"method":"subscribe","subscription":{"type":"rawTrades","filter":{"coin":["BTC"]}}}' | \
  websocat -n --ping-interval 30 --max-messages 10 ws://$HOST:$PORT/ws

For interactive mode, run without a pipe and type JSON line by line:

websocat ws://$HOST:$PORT/ws
> {"method":"subscribe","subscription":{"type":"rawTrades","filter":{"coin":["BTC"]}}}

9. Gotchas

  • Batching per block — don't expect millisecond reaction per event; data arrives in batches per block.
  • Empty filter = everything — if filter is omitted or {}, all events pass through. The filter field is always optional.
  • Unsubscribe must match 1:1 — if you subscribed with {"coin":["BTC"]}, the unsubscribe must carry the same filter.
  • Duplicate subscription — an identical subscribe returns error: "Already subscribed".
  • Keep-alive — the server does not send ping frames. If the client sends neither subscriptions nor pings, network equipment may close the idle connection. Send pings yourself or reconnect.
  • Spot booksl2Book only works for perp markets.
  • Slow client — if you read slower than the server writes, some messages may be dropped (without closing the connection). Parse in a thread separate from your application logic.

10. Python client

import asyncio, json, websockets

async def main():
    async with websockets.connect("ws://$HOST:$PORT/ws") as ws:
        await ws.send(json.dumps({
            "method": "subscribe",
            "subscription": { "type": "rawTrades", "filter": { "coin": ["BTC"] } }
        }))
        async for msg in ws:
            data = json.loads(msg)
            print(data["channel"], data["data"])

asyncio.run(main())