Realtime Socket
Live streaming over websocket: public market data for any product, and your own trading activity, pushed as it happens. The socket is a Centrifugo server, so any Centrifugo client SDK works — centrifuge-js, centrifuge-go, centrifuge-python, and so on.
Endpoint: wss://stream.tradepost.co/connection/websocket
Authenticating
The socket takes a short-lived connection token, minted by the API with your normal authentication:
query {
getRealtimeToken {
token
expiresAt
error { message }
}
}
The token is a JWT whose sub is your account id. It lives twenty minutes — wire the SDK's getToken callback to this query and it re-fetches before Centrifugo drops the socket:
import { Centrifuge } from 'centrifuge';
const centrifuge = new Centrifuge('wss://stream.tradepost.co/connection/websocket', {
getToken: async () => {
const { data } = await graphql(`query { getRealtimeToken { token } }`);
return data.getRealtimeToken.token;
},
});
centrifuge.connect();
One connection carries every subscription you need. Public channels need nothing beyond the connection; private account: channels are authorized by the #<accountId> suffix matching the token's sub — subscribing to any other account's channel is refused with 103 permission denied.
Channels
Channel names are <namespace>:<stream>:<scope>. Public streams are scoped to an asset configuration and, for most, a graded/condition key — the same (assetConfigurationId, keyId) pair that names one book. Private streams are scoped to your account.
Public market data
| Channel | Payload | What it carries |
|---|---|---|
public:book:<configId>:<keyId> | Book delta | One depth level after it changed — see Order Book |
public:book:<configId>:<keyId>:<marketId> | Book delta | The same, scoped to one venue's aggregates |
public:trades:<configId>:<keyId> | Trade print | Anonymized fill: executionId, price, qty, takerSide, marketId, executedAt |
public:stats:<configId>:<keyId> | Leaf stats | Recomputed market stats after each trade: last price, volume, VWAP, high/low, trade count, sparkline, with a seq to drop stale pushes |
Your account
| Channel | kind | What it carries |
|---|---|---|
account:fills#<accountId> | fill | A fill on one of your orders — full trade detail, your side, your order id, status and version. Re-pushed on every settlement transition |
account:fills#<accountId> | order | An order lifecycle event — orderId, status, action, reason, filledQuantity, remainingQuantity. Emitted on rest, amend, cancel, expiry, and settlement-driven changes |
account:money#<accountId> | balance / profile | Your cached balance, and your payment profile, when either changes |
account:holdings#<accountId> | holdings | A poke — your inventory changed, refetch. Never a delta |
Two payload kinds share account:fills, discriminated by kind. Fill payloads carry a version: keep the highest per executionId, not the last to arrive.
Snapshot, then stream
The socket is a delta feed. Every channel is meant to be paired with a read from the API that gives you the starting state:
- Subscribe to the channel first, so nothing lands between your read and your subscription.
- Read the snapshot —
getOrderBook,listOrderSubmissions,listOrderExecutions. - Apply pushes on top. Book deltas carry a
seqto align against the snapshot'ssequence; fill payloads carry aversion; order events carryupdatedAt.
Channels keep a short history (roughly five minutes for public streams, ten for account streams), so a brief disconnect recovers automatically through the SDK. After anything longer, or on any RESYNC marker, re-read the snapshot.
Delivery is at-most-once from the server's point of view — a rolled-back write emits nothing, and a client that is offline misses ephemeral pushes. Reconcile from the API whenever you reconnect.