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.
type | Required fields | Optional fields | Response channel |
|---|---|---|---|
rawTrades | — | filter | rawTrades |
orders | — | filter | rawOrders |
bookUpdates | — | filter | rawBookUpdates |
events | — | filter | rawEvents |
twap | — | filter | rawTwap |
writerActions | — | filter | rawWriterActions |
l2Book | coin | nSigFigs, nLevels, mantissa | l2Book |
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:
| Parameter | Type | Default | Description |
|---|---|---|---|
coin | string | — (required) | Ticker, e.g. "BTC" |
nLevels | u32 | 20 | How many levels to return per side (bids/asks), max 100 |
nSigFigs | u32 | — | Significant figures for price rounding |
mantissa | u64 | — | Mantissa 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
coinandsiderequires 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 insideaction.destination. - Numbers and booleans are compared as strings.
"oid": ["12345"]matchesoid: 12345.
Special values
| Value | Meaning |
|---|---|
"*" 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.
| Parameter | Limit |
|---|---|
| Total values across the whole filter | 500 |
In the user / users field | 100 |
In the coin field | 50 |
In the type field | 20 |
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)
| Field | Type | Description |
|---|---|---|
coin | string | Ticker |
px | string | Execution price |
sz | string | Size |
side | string | "A" (ask) / "B" (bid) |
time | u64 | Timestamp, ms |
startPosition | string | Position before the trade |
dir | string | Direction |
closedPnl | string | Realized PnL |
hash | string | Block hash |
oid | u64 | Order ID |
crossed | bool | Spread cross |
fee | string | Fee |
tid | u64 | Trade ID |
feeToken | string | Fee token |
liquidation | Liquidation|null | Liquidation details (if any) |
user in the filter is the hex address from the [userAddress, Fill] pair.
Liquidation: { liquidatedUser, markPx, method }.
OrderStatus (element of rawOrders)
| Field | Type | Description |
|---|---|---|
time | datetime | Event time |
user | address | Owner |
status | string | "open", "filled", "canceled", "triggered", ... |
order | L4Order | The order itself |
L4Order:
| Field | Type | Description |
|---|---|---|
user | address|null | — |
coin | string | — |
side | string | "A" / "B" |
limitPx | string | Limit price |
sz | string | Size |
oid | u64 | Order ID |
timestamp | u64 | Created at, ms |
triggerCondition | string | For trigger orders |
isTrigger | bool | — |
triggerPx | string | Trigger price |
isPositionTpsl | bool | Position TP/SL |
reduceOnly | bool | — |
orderType | string | "Limit", "Market", ... |
tif | string|null | "Ioc", "Gtc", ... |
cloid | string|null | Client ID |
OrderDiff (element of rawBookUpdates)
| Field | Type | Description |
|---|---|---|
user | address | Owner |
oid | u64 | Order ID |
px | string | Level price |
coin | string | Ticker |
rawBookDiff | enum | What changed |
rawBookDiff is one of:
{ "new": { "sz": "..." } }— new order at the level{ "update": { "origSz": "...", "newSz": "..." } }— size change"remove"— removal
MiscEvent (element of rawEvents)
| Field | Type | Description |
|---|---|---|
time | string | Timestamp |
hash | string | Block hash |
inner | any JSON | Event body (the kind is inside as type) |
TwapStatus (element of rawTwap)
| Field | Type | Description |
|---|---|---|
time | string | Timestamp |
twapId | u64 | TWAP ID |
state | any JSON | State |
status | string | Current status |
WriterAction (element of rawWriterActions)
| Field | Type | Description |
|---|---|---|
user | string | Address |
nonce | u64 | Nonce |
evmTxHash | string | EVM transaction hash |
action | any JSON | Action details |
Level (in l2Book.levels)
| Field | Type | Description |
|---|---|---|
px | string | Level price |
sz | string | Total size |
n | usize | Number 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
echosends 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
filteris omitted or{}, all events pass through. Thefilterfield is always optional. - Unsubscribe must match 1:1 — if you subscribed with
{"coin":["BTC"]}, theunsubscribemust carry the same filter. - Duplicate subscription — an identical
subscribereturnserror: "Already subscribed". - Keep-alive — the server does not send
pingframes. If the client sends neither subscriptions nor pings, network equipment may close the idle connection. Send pings yourself or reconnect. - Spot books —
l2Bookonly 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())