Private Channels
Private channels stream account-scoped events for one 20-byte engine account. Issue a short-lived session token over HTTP, then present it in the first WebSocket frame. Never send owner-wallet signatures or HMAC secrets over the socket.
Channels
| Channel | Purpose | Required credential scope |
|---|---|---|
account | Balances, holds, releases, positions and the nonce window (nonceFloor, nonceWindow, nonceUsedInWindow, nonceHoles) | read |
userEvents | Account lifecycle events (settlement, funding credits, admin actions) | read |
orderUpdates | Order acknowledgements, working state, amendments, cancels, terminal states | read |
userFills (alias fills) | Executions with price, quantity, fee (feeMicro, fee ppm) and liquidity role | read |
dropCopy | Drop-copy execution stream, identical in content to FIX drop-copy and the BSL execution stream | drop_copy |
A subscription on a channel whose scope the token's credential lacks returns
error 1002.
1. Issue a token
POST /api/v1/ws/token
Content-Type: application/json
SC-Auth-Version: 2
SC-Key: <apiKeyId>
SC-Nonce: <monotonic>
SC-Timestamp: <unix ms>
SC-Passphrase: <apiPassphrase>
SC-Signature: <hmac>
{ "account": "0x2222222222222222222222222222222222222222", "ttlMs": 180000, "clientId": "mm-quote-engine-1", "scope": "read" }
scope is read (default) or drop_copy. The request can also be
wallet-signed (agentId, nonce, signature together) instead of using
SC-* headers. Response:
{ "ok": true, "account": "0x2222…", "scope": "private_ws:read", "tokenType": "Bearer",
"token": "spws1.…", "issuedAtMs": 1788558400818, "expiresAtMs": 1788558580818, "ttlMs": 180000, "ipBound": false, "clientId": "mm-quote-engine-1" }
Token issuance is rate-limited on its own; on 429 respect Retry-After and
reuse the token you already hold.
2. Authenticate on the socket
The token goes into the first frame, not into a header or query string:
{ "id": "auth-1", "method": "auth", "authorization": "Bearer spws1.…" }
{ "type": "ack", "channel": "auth", "data": { "ok": true, "account": "0x2222…", "scope": "read", "tokenType": "ws_session_token", "agentId": "agt_…", "credentialId": "…" }, "id": "auth-1" }
Authenticate within authDeadlineMs from the session frame; otherwise the
server closes with 4401. Subscribing before auth returns error 1001.
3. Subscribe with a cursor
{ "id": "sub-1", "method": "subscribe", "subscription": { "type": "orderUpdates", "account": "0x2222…", "fromSeq": 4810 } }
{ "id": "sub-2", "method": "subscribe", "subscription": { "type": "userFills", "account": "0x2222…", "fromSeq": 913 } }
account, when present, must equal the account in the URL. With fromSeq
the server replays retained events after that sequence, then sends
snapshot_end on the channel; without it you receive live events only.
Persist the last processed seq per channel and pass it on reconnect.
Sequence scope
Private streams are sequenced per account and channel. Deduplicate by
(channel, seq): a replay after a process crash can legitimately redeliver
the last events you already handled.
Delivery model
Private events are delivered ring-first: reads are served from an
in-memory, per-account replay buffer before falling back to SQL. The buffer is
bounded by an event count and a retention window, so it covers the most recent
events per account. When a requested range is not fully covered, the server
backfills from SQL and resumes from the ring; sequence numbers and ordering
are identical either way because every event is committed and numbered by the
database before it enters the buffer. If your cursor is older than the
retention window you receive resume_required instead of a silent gap.
Recovery frames
| Frame | Meaning | Client action |
|---|---|---|
snapshot_end | Replay for that channel is complete | Switch to live processing |
resume_required / gap_fill | Cursor outside retention; data carries oldestAvailableSeq and newestAvailableSeq | Rebuild from REST (/api/v1/accounts/{account}/orders, /fills, /api/v1/bsl/accounts/{account}/executions?cursor=), then resubscribe from oldestAvailableSeq or later |
rotate | Planned disconnect ahead; includes the resubscribe journal | Reconnect and replay the journal |
warning with reason: slow_consumer | Your consumer is behind | Drain faster; the server closes with 4408 if it persists |
SDK
@sentico-labs/sdk ships ReliablePrivateStream, which implements token
refresh, auth, cursor persistence, replay dedup, rotation and reconnect:
const stream = client.ws.reliablePrivate({
account,
tokenProvider: () => client.trading.issuePrivateWsToken(account, { ttlMs: 180_000 }),
subscriptions: [{ type: "orderUpdates" }, { type: "userFills" }],
onEvent: (frame, ctx) => strategy.apply(frame, ctx), // idempotent by (channel, seq)
onResyncRequired: (details) => strategy.rebuildFromRest(details),
});
await stream.start();
Security notes
- Keep tokens short-lived; the server clamps
ttlMsto its configured range. - Separate read-only stream credentials from trading credentials where possible.
- Treat a private stream disconnect as an operational alert for unattended trading.