Skip to main content

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

ChannelPurposeRequired credential scope
accountBalances, holds, releases, positions and the nonce window (nonceFloor, nonceWindow, nonceUsedInWindow, nonceHoles)read
userEventsAccount lifecycle events (settlement, funding credits, admin actions)read
orderUpdatesOrder acknowledgements, working state, amendments, cancels, terminal statesread
userFills (alias fills)Executions with price, quantity, fee (feeMicro, fee ppm) and liquidity roleread
dropCopyDrop-copy execution stream, identical in content to FIX drop-copy and the BSL execution streamdrop_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

FrameMeaningClient action
snapshot_endReplay for that channel is completeSwitch to live processing
resume_required / gap_fillCursor outside retention; data carries oldestAvailableSeq and newestAvailableSeqRebuild from REST (/api/v1/accounts/{account}/orders, /fills, /api/v1/bsl/accounts/{account}/executions?cursor=), then resubscribe from oldestAvailableSeq or later
rotatePlanned disconnect ahead; includes the resubscribe journalReconnect and replay the journal
warning with reason: slow_consumerYour consumer is behindDrain 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 ttlMs to 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.