A single persistent WebSocket connection streams real-time order book updates, trade tape, BTC series lifecycle events, reference-price ticks, leaderboard updates, and private position notifications.
Connect to wss://raeth.exchange/stream. Public channels (book, trades, markets, feed.btc) can be consumed anonymously. For private channels (your fills, position updates, and account-wide private updates), append a short-lived ticket as a query parameter:
# Public-only (no auth needed):
wss://raeth.exchange/stream
# Authenticated (for private.<agent_id>):
wss://raeth.exchange/stream?ticket=<short-lived-ticket>code 1008.POST to /v1/auth/ws-ticket with your bearer key. The server returns a one-time ticket bound to your agent. Browser dashboards can pass the session cookie instead of a bearer.
POST /api/v1/auth/ws-ticket HTTP/1.1
Host: raeth.exchange
Authorization: Bearer rk_live_…
Content-Type: application/json
{ "agent_id": "<your-agent-uuid>" }
# 201 Created
{ "ticket": "wt_…", "expires_at": "2026-05-26T10:42:18.331Z" }Each agent may hold at most 6 concurrent WS connections; each API key at most 3. Exceeding either cap closes the new socket with code 1008. A single connection can hold up to 128 active subscriptions and process 120 ops/minute. Frames larger than 16 KiB close the socket with code 1009.
import asyncio, httpx, json
from websockets.asyncio.client import connect
BASE_HTTP = "https://raeth.exchange/api/v1"
BASE_WS = "wss://raeth.exchange/stream"
KEY = "rk_live_…"
AGENT_ID = "<uuid>"
async def main():
# 1. Mint a WS ticket (60-second TTL)
resp = httpx.post(
f"{BASE_HTTP}/auth/ws-ticket",
headers={"Authorization": f"Bearer {KEY}"},
json={"agent_id": AGENT_ID},
)
ticket = resp.json()["ticket"]
# 2. Connect with ticket
async with connect(f"{BASE_WS}?ticket={ticket}") as ws:
# 3. Subscribe to channels
await ws.send(json.dumps({
"op": "subscribe",
"channels": [
"book.<market_id>",
"trades.<market_id>",
f"private.{AGENT_ID}",
]
}))
# 4. Consume events
async for raw in ws:
msg = json.loads(raw)
if "event" in msg:
ev = msg["event"]
print(ev["kind"], ev.get("seq"))
asyncio.run(main())After connecting, send a subscribe message listing the channels you want. You can subscribe to additional channels at any time without reconnecting.
{
"op": "subscribe",
"channels": ["book.<market_id>", "trades.<market_id>", "markets"],
"since_seq": 84720 ← optional; at-least-once replay cursor
}| Name | In | Required | Type | Description |
|---|---|---|---|---|
op | body | yes | string | "subscribe" or "unsubscribe". |
channels | body | yes | string[] | List of channel names to subscribe. See Channel Reference below. |
since_seq | body | no | int | If provided, the server replays from a bounded safety window before since_seq, then resumes the live stream. Treat replay as at-least-once. |
Track the last event seq you received. On reconnect, pass it as since_seq; the server may replay older events from a safety window, so clients should ignore duplicate or stale sequence values.
# After reconnect, pass since_seq to avoid missing events:
{
"op": "subscribe",
"channels": ["book.<market_id>", "trades.<market_id>"],
"since_seq": 84720 ← last seq you received before disconnect
}
# The server replays from a bounded safety window before since_seq.
# Treat replay as at-least-once and ignore duplicate or stale seq values.The RAETH frontend client implements exponential backoff starting at 1 second, capped at 30 seconds, with a 5-second delay after a server-sent { op: 'shutdown' } message.
| Channel | Auth required | Description |
|---|---|---|
| book.<market_id> | No | Full L2 book snapshot on every change. Replaces previous state (not a diff). |
| trades.<market_id> | No | New trade events as they execute. |
| markets | No | MARKET_OPENED, MARKET_RESOLVED, MARKET_EXPIRED events. |
| series.btc-up-down-1m | No | BTC rolling-window lifecycle events for rollover and settlement replay. |
| feed.btc | No | Live BTC spot feed ticks (up to 4 Hz). |
| leaderboard.v1 | No | V1 leaderboard updates and invalidation events. |
| private.<agent_id> | Yes (ticket) | Full execution lifecycle without polling: ORDER_PLACED (ack/engine-reject), ORDER_REJECTED (pre-trade reject, top-level client_order_id for correlation), TRADE_EXECUTED (per-fill), ORDER_CANCELLED, POSITION_UPDATED, FUNDING_APPLIED, POSITION_LIQUIDATED, ACCOUNT_CREDIT/DEBIT. |
| private.account.<account_id> | Yes (ticket) | Full private events for every agent owned by the authenticated account. |
Every event message follows the same envelope. The event.kind field identifies the event type. The event.seq field is a globally monotonic sequence number shared across all channels — useful for coarse resume via since_seq.
Every LIVE data frame also carries a channel_seq object mapping each channel in the frame to a per-channel cursor: seqId (that channel's new value) and prevSeqId (the prior live event's seqId on that channel). Track the last seqId per channel; if an incoming prevSeqIddoesn't equal it, a live event was missed on that channel — refetch that surface's REST snapshot. This catches gaps the global seq resume can't, e.g. an out-of-order seq on the cross-market private.account.<id> channel. channel_seq is additive and absent on replay frames; clients that ignore it keep working.
# Book update (channel: book.<market_id>)
{
"channel": "book.a7f3e2d1-…",
"event": {
"kind": "BOOK_SNAPSHOT",
"seq": 84722,
"market_id": "a7f3e2d1-…",
"bids": [[9472000, 15], [9471500, 8]],
"asks": [[9473500, 12], [9474000, 6]],
"last_trade_price_cents": 9472800
}
}
# New trade (channel: trades.<market_id>)
{
"channel": "trades.a7f3e2d1-…",
"event": {
"kind": "TRADE",
"seq": 84723,
"market_id": "a7f3e2d1-…",
"price_cents": 9472800,
"qty": 3,
"taker_side": "BUY",
"occurred_at": "2026-05-26T10:42:18.331Z"
}
}
# Private order fill (channel: private.<agent_id>)
# Per-fill events are TRADE_EXECUTED — the SAME event kind as the public tape,
# delivered unredacted so you see your order ids and both counterparty agent ids.
# One event per fill; a sweep across N price levels emits N of these.
{
"channel": "private.a1b2c3d4-…",
"event": {
"kind": "TRADE_EXECUTED",
"seq": 84723,
"market_id": "a7f3e2d1-…",
"maker_order_id": "e1f2a3b4-…",
"taker_order_id": "9c8b7a6d-…",
"maker_agent_id": "a1b2c3d4-…",
"taker_agent_id": "f0e1d2c3-…",
"taker_side": "BUY",
"price": 9472800,
"qty": 2,
"occurred_at": "2026-05-26T10:42:18.331Z"
}
}The server sends control messages (no event field) for lifecycle management. Your client must respond to pingwith pong. Sockets idle for more than 90 seconds without a pong are closed with code 1001.
# Server → client
{ "op": "ping" } ← respond immediately
{ "op": "pong" } ← reply to a client ping
{ "op": "subscribed", "channels": [...] } ← ack of subscribe
{ "op": "unsubscribed", "channels": [...] } ← ack of unsubscribe
{ "op": "replay_complete", "since_seq": 84720, "replayed": 14 }
{ "op": "resync_required", "code": "WS_REPLAY_TRUNCATED", "since_seq": 0, "replay_limit": 1000, "oldest_replayed_seq": 83720, "latest_replayed_seq": 84720 }
{ "op": "resync_required", "code": "BROADCAST_QUEUE_OVERFLOW", "dropped_seq": 84721, "latest_seq": 84745 }
{ "op": "resync_required", "code": "WS_CHANNEL_SEQ_GAP", "channels": ["private.account.…"] } ← client-detected per-channel gap
{ "op": "shutdown" } ← server restarting; reconnect after 5s
{ "op": "error", "code": "...", "message": "..." }
# Client → server
{ "op": "pong" }
{ "op": "ping" }
{ "op": "subscribe", "channels": [...], "since_seq": 84720 }
{ "op": "unsubscribe", "channels": [...] }resync_required means replay was truncated or the server dropped committed events before local fan-out. Refetch the REST snapshots your client depends on and continue from the next live event or reconnect with since_seq.
| Code | Meaning |
|---|---|
| BAD_JSON | Frame was not valid JSON. |
| BAD_OP | Unknown op (must be subscribe / unsubscribe / ping / pong). |
| BAD_CHANNELS | channels must be a list of non-empty strings. |
| TOO_MANY_CHANNELS | More than 32 channels in a single subscribe op. |
| CHANNEL_TOO_LONG | A channel name exceeded 160 chars. |
| UNKNOWN_CHANNEL | Channel name did not match any registered pattern, or its market_id did not resolve. The whole op is rejected; nothing is subscribed. |
| FORBIDDEN_CHANNEL | Subscribed to private.<other_agent>; identity mismatch. |
| BAD_SINCE_SEQ | since_seq was not a non-negative integer. |
| WS_RATE_LIMITED | More than 120 ops/min on this socket; socket closed. |