WebSocket
One socket for every market and topic: sequence numbered book deltas, best bid and offer, oracle marks, trades, klines and the list feed, with exact frames.
The WebSocket is the transport for market makers and latency sensitive bots. One connection carries every market and topic you ask for; the order book arrives as sequence numbered deltas and bars are pushed as they form. The data is identical to the SSE feed; what changes is the delivery.
GET wss://app.truefinance.ai/api/ws/perps During the rollout the socket is allowlisted per account. Send the app session as Authorization: Bearer <token> on the upgrade (or ?token= from a browser). A refused account gets HTTP 403 on the upgrade and should fall back to SSE, which carries the same data. Planned: API key auth on the upgrade for bots and market makers, see Limits and Security.
Lifecycle
Subscribing
{"op":"subscribe","args":["orderbook:BTCUSDC","bbo:BTCUSDC","mark:BTCUSDC","trades:BTCUSDC","ticker:BTCUSDC","kline:1m:BTCUSDC","marks:ALL","stats:ALL","tape:ALL"]}
Acknowledged with {"op":"subscribe","success":[…],"failed":[{"topic":"…","reason":"bad_topic"|"unknown_market"|"topic_limit"|"rate_limited"|"feed_limit"}]}. rate_limited: this connection started too many new market feeds at once (40 at once, then 1 per second); retry the topic after a second. feed_limit: the server is at its feed capacity; retry later. Drop topics with {"op":"unsubscribe","args":[…]}. At most 40 topics per connection.
Topics
| Topic | Frames |
|---|---|
orderbook:SYM | type:"snapshot" then type:"delta", sequence numbered (below) |
bbo:SYM | type:"update", {bid, bid_size, ask, ask_size, …} |
ticker:SYM | type:"update", the 24h stats row, only when a non timestamp field changed |
mark:SYM | type:"update", {market, mark_price}, the oracle mark |
trades:SYM | type:"delta", array of new fills; no replay on subscribe |
kline:<interval>:SYM | type:"update" for the forming bar, type:"closed" when it rolls; intervals 1s 1m 5m 15m 1h 4h 1d; the open bar is sent on subscribe |
marks:ALL | type:"update", {marks:{SYM:px}, ts, src:"pyth"}, only when a mark moved |
stats:ALL | type:"snapshot", {results:[…]} for every market, on change |
tape:ALL | type:"update", {trades_24h, trades_today, ts} |
Order book deltas
Snapshot:
{"topic":"orderbook:BTCUSDC","type":"snapshot","ts":1789232208665,"data":{"type":"snapshot","market":"BTCUSDC","u":123,"bids":[["77302","0.3593"],["77307","0.4"]],"asks":[["77350","0.4061"]]}}
Delta:
{"topic":"orderbook:BTCUSDC","type":"delta","ts":1789232209271,"data":{"type":"delta","market":"BTCUSDC","u":124,"prevU":123,"bids":[["77302","0"]],"asks":[["77350","0.5"]]}}
Each level is [price, size]; size "0" removes the level. Apply a delta only when prevU equals the u you hold. Otherwise you missed a frame: resubscribe (subscribing a book topic you already hold returns a fresh snapshot) orderbook:SYM to receive a fresh snapshot. Never render a book after a gap. Measured over 1,000 polls, a delta averages 105 bytes where the full book is 2,462, and 4 in 10 polls emit nothing at all.
A minimal client
const ws = new WebSocket("wss://app.truefinance.ai/api/ws/perps", [], { headers: { Authorization: `Bearer ${TOKEN}` } });
let book = null;
ws.onopen = () => ws.send(JSON.stringify({ op: "subscribe", args: ["orderbook:BTCUSDC", "mark:BTCUSDC"] }));
ws.onmessage = (ev) => {
const f = JSON.parse(ev.data);
if (f.op === "ping") return ws.send(JSON.stringify({ op: "pong" }));
if (f.op) return; // ready, subscribe ack
if (f.topic === "orderbook:BTCUSDC") {
const d = f.data;
if (d.type === "snapshot") book = { u: d.u, bids: new Map(d.bids), asks: new Map(d.asks) };
else if (book && d.prevU === book.u) { apply(book, d); book.u = d.u; }
else resubscribe("orderbook:BTCUSDC"); // gap
}
if (f.topic === "mark:BTCUSDC") markToMarket(f.data.mark_price);
};
Limits
40 topics per socket, 30 client frames in a burst and 5 a second sustained (ping and pong not counted), 6 sockets and 60 handshakes a minute per account, 32 sockets and 240 handshakes a minute per client IP. Reconnect with backoff, 1 s doubling to 15 s; a socket that connected and later dropped is reconnected by you, the server does not replay.