Overview
The Monaco Protocol WebSocket API provides real-time streaming of multiple data types. Unlike REST API endpoints that require polling, WebSocket connections give you instant updates as events occur.Available Channels
Balances vs. Movements
- Use
balanceswhen you need your UI or system to reflect the current balance state for each asset, updated whenever a balance changes (e.g., lock/unlock, deposit, withdrawal, trade). - Use
movementswhen you need a stream of individual balance-affecting events (ledger-style entries) for reconciliation or auditing — deposits, withdrawals, funding settlements, and the balance a cancelled or expired order unlocks. A fill’s ledger entries are not streamed here: read them from movement history (GET /api/v1/accounts/movements), and take the live signal for a fill offbalances.
Key Features
- Automatic reconnection - Full-jitter exponential backoff, unlimited retries by default, with an
onResynchook after each reconnect - Heartbeat/keepalive - Runs at two layers, both handled automatically — see Heartbeat and keepalive
- Session-key authentication - Authenticated channels are secured by ed25519 session-key signing, handled automatically by the SDK
- Functional API - Clean subscription pattern with unsubscribe functions
- TypeScript support - Fully typed event interfaces
Connection Setup
SDK Initialization
The WebSocket URL is automatically resolved from thenetwork parameter — no manual configuration needed:
Connecting
The WebSocket auto-connects when the SDK is initialized. You can also manually connect:Default Settings
- Auto-reconnect: Enabled
- Reconnect backoff: Full-jitter exponential backoff, base 1s, capped at 30s
- Max reconnect attempts: Unlimited by default (set
maxReconnectAttemptsfor a finite cap) - Connection timeout: 10 seconds (
connectionTimeoutMs) - Heartbeat interval: 15 seconds (client-sent JSON
Ping; the server currently sends a transport-level ping about every 25 seconds — see Heartbeat and keepalive)
Reconnection and resync
sdk.ws auto-reconnects with full-jitter exponential backoff (base 1s, capped at 30s) and, by default, retries indefinitely. onStatusChange reports "reconnecting" while backing off, and heartbeats are handled automatically (see Heartbeat and keepalive).
After every automatic reconnect, onResync fires after the SDK sends authentication/subscription requests. It does not wait for their acknowledgements or subscribe snapshots, and performs no REST refetches automatically. The application owns reconciliation, including channels without subscribe snapshots. A receiver gap owned by any of the connection’s subscriptions closes the whole connection with 1013 (see Slow-client disconnect); slowClient is true for that close code.
Merge orders by (id, version), not updatedAt. Persisted snapshot absence does not prove a locally resting order terminated: persistence can lag a newer event. Retain the row and resolve it with getOrder, then apply the version rule. An absent version is unknown, not zero; use buffer-and-replay recovery for those rows. See the SDK lifecycle guidance and market-maker recovery recipe for version ties, delayed events, and missed executions. Available history has pagination/retention limits; reconnect does not guarantee complete reconstruction of a gap.
Error frames reach no subscription handler. The onError callback is how they reach your app. A per-channel failure (INVALID_SUBSCRIPTION, AUTH_REQUIRED, SUBSCRIPTION_LIMIT, SNAPSHOT_UNAVAILABLE) also carries channel, the channel string exactly as the client sent it (a string longer than 128 bytes, never a valid channel, is echoed as its first 128 UTF-8 bytes, cut at a character boundary): Subscribed / Unsubscribed acknowledgements list only the channels a request changed, so a rejected channel is left out of the ack and reported by its own Error frame, and channel pairs the two. Connection-wide failures carry no channel. Branch on code — refetch that channel’s REST snapshot on SNAPSHOT_UNAVAILABLE (the subscription is live and diffs flow, but the subscribe-time baseline is missing), re-authenticate on SESSION_INVALID (the SDK stops reconnecting after it, or after AUTH_FAILED, and calls onReauthenticationRequired — see session loss), and slow down on MESSAGE_RATE_LIMIT (a per-connection inbound cap was exceeded — the message rate, or the byte budget that refuses a burst of very large frames — and the connection closes with 1008; batch subscriptions, keep frames small, and back off before reconnecting). AUTH_REQUIRED needs one step more: the server rejected that subscribe without registering the channel, and the client still holds your handler, so it treats the channel as subscribed and sends no new Subscribe — re-subscribe after authenticating (release the last handler for the channel, then subscribe again). A code this SDK version does not recognize is delivered as-is rather than dropped, so log the default branch instead of ignoring it.
Pass these options to the SDK as the ws config — the MonacoSDK constructor now forwards them to the WebSocket client it builds internally — or to the standalone createMonacoWebSocket(baseUrl, options) factory, which additionally accepts the session keypair:
createMonacoWebSocket(baseUrl, options) factory takes the same options (plus the session keypair) when you build a client directly:
Heartbeat and keepalive
Connection liveness runs at two independent layers. The SDK handles both for you — you never send or answer a ping yourself — but the distinction matters when building a client directly against the raw protocol:- Transport-level ping (server → client). The server sends a WebSocket ping control frame about every 25 seconds so proxies do not close an idle connection. Your WebSocket implementation (browser,
ws, etc.) replies with a pong frame automatically; this never surfaces as an application message and there is nothing to handle in your code. - Application-level
Ping/Pong(client → server). The SDK sends a JSON{ "type": "Ping" }every 15 seconds and the server replies{ "type": "Pong" }, which the SDK consumes silently. This is a client-initiated check. - Security note.
Ping/Pongis a liveness signal only — its payloads are unsigned and non-transactional. It does not authenticate a user action, extend or refresh a session, or provide replay protection. User authentication is the signedAuthenticatehandshake, and it is not renewed by heartbeat traffic.
Ping only travels client → server, and Pong only travels server → client. The server does not send a JSON { "type": "Ping" } — server-initiated liveness is the transport-level ping frame above. If you build your own raw client, send { "type": "Ping" } on your own timer and treat { "type": "Pong" } as the reply; do not wait for a server-sent JSON ping.
WebSocket Types
Order Events
Order events require authentication. Callsdk.login(clientId) before subscribing. Once a session is active, the SDK authenticates the WebSocket for you — it sends a signed Authenticate handshake on connect and re-authenticates automatically after any reconnect. Public channels (orderbook, candlesticks, trades) need no authentication.
Subscribe to Orders
Subscribe to All User Orders
UseuserOrders() to receive events for ALL your orders across all trading pairs:
Use
sdk.ws.orders(pairId, mode, handler) for pair-specific subscriptions, or sdk.ws.userOrders(handler) for all orders.Event Types
Taker vs Maker Events
Order events contain different fields depending on whether you’re the taker (initiating the trade) or maker (resting order on the book):Illustrative Event Structure
This combined field overview uses the SDK’s camelCase names; raw wire payloads use snake_case. It is not an exact per-variant type: required fields and nullability differ by event and role. See the typed order-event variants, especiallyOrderMatched and OrderExpired, before treating a key as optional or non-null.
data.version?: number is an opaque, compare-only revision shared with REST and subscription snapshots. Compare only the same order; gaps are normal, and absent means unknown rather than zero. A snapshot wins when snapshot.version >= local.version, including ties, because several live events can share one step. Known value aliases below allow unfamiliar strings and need a default branch; concrete event variants retain their closed discriminants.
terminalReason SELF_TRADE_PREVENTION, reason self_trade_prevention, and stpCounterpartyOrderId. The three always travel together, so the presence of stpCounterpartyOrderId is the whole signal; absent means this was not an STP cancellation. It names the arriving order on a swept resting order, and the first own resting order on a cancelled taker remainder. Such a cancellation is a real terminal CANCELLED even with zero fills. An accepted IOC/FOK that filled nothing for want of liquidity is REJECTED instead, arriving as OrderRejected; a plain zero-fill MARKET submit is refused before acceptance and emits nothing at all, so on this channel the contrast that matters is OrderCancelled versus OrderRejected.
Example: Handle Order Events
Orderbook Updates
Subscribe to real-time orderbook depth updates. No authentication required.Subscribe to Orderbook
Orderbook Event Structure
OHLCV Candlestick Data
Subscribe to real-time candlestick updates. No authentication required.Subscribe to OHLCV
OHLCV Event Structure
Trade Events
Subscribe to real-time trade executions. No authentication required.Subscribe to Trades
Trade Event Structure
Movement Events
New in v0.5.4 - Real-time balance movement tracking
Subscribe to Movements
Movement Event Structure
Fills do not arrive on this channel. Live
movement frames cover deposits, withdrawals, funding settlements, and the unlocks a cancelled or expired order releases. A fill’s ledger entries are written durably and read back through movement history (GET /api/v1/accounts/movements); the live signal for a fill is a balance_update on the balances channel — a versioned spot row for a spot-owned leg, and for a margin fill an unversioned frame carrying the margin account’s post-fill collateral. Fee attribution on that path is conditional: the taker also gets a fee-reason frame only when a taker or application fee was actually charged, and an impacted margin maker gets a rebate-reason frame only when its maker fee is negative — a positive maker fee produces no separate frame, so do not wait for one.In that history, a populated balance snapshot describes a spot user_balances row, and only that. Two kinds of entry carry no spot row and report 0 in balanceBefore, balanceAfter, lockedBefore, and lockedAfter (and their raw forms). The first is any margin fill leg — a margin taker’s own debit and credit legs, and a margin maker’s — because that collateral settles through the risk engine rather than a spot row; a genuine spot maker matched by a margin taker is the exception and still reports its real snapshot. The second is a FUNDING row, which is derived at read time from the funding ledger and is a collateral delta rather than a token transfer. In both cases the zeros mean “no spot snapshot”, never “a spot balance of zero”. Read margin account state for collateral, and do not derive a position’s collateral change from these fields.Important: Use
decimals field to convert between human-readable and raw amounts:amount: “1.5” (decimal, for display)amountRaw: “1500000000” (raw, withdecimals: 9)- Conversion:
rawAmount = decimalAmount * 10**decimals
Movement Types
entryType and transactionType are free-form strings and are not normalized to one casing. The deposit, withdrawal and funding producers emit both fields lowercase (entryType credit / debit). The order-unlock producer emits entryType as uppercase CREDIT and transactionType in PascalCase (OrderCancelled, OrderPartiallyCancelled, OrderExpired). Normalize both fields before comparing. The values a live frame can carry:
Entry Types:
credit/CREDIT- Funds added to available balancedebit- Funds removed from available balance
deposit- On-chain deposit confirmed (spot or margin-routed)withdrawal- Withdrawal processedfunding- Funding payment settled against margin collateral (reconciles with thebalance_updatewhosereasonisfunding)OrderCancelled/OrderPartiallyCancelled/OrderExpired- Balance unlocked by a cancelled, partially cancelled, or expired order
GET /api/v1/accounts/movements), never on this channel.
Example: Track Deposits
Example: Monitor Order Unlocks
balances instead — a settlement’s live signal is a balance_update with reason: "trade", and its ledger entry is read back from movement history.
React Hook
The SDK provides a dedicated React hook for movements with automatic deduplication:Balance Updates
Subscribe to real-time balance changes for your account. Requires authentication. Balance updates are triggered by:- Deposits to the vault
- Withdrawals from the vault
- Orders locking balance
- Orders releasing locked balance
- Trade executions affecting balance
Subscribe to Balance Updates
Event Structure
Update Reasons
Balance updates include areason field indicating why the balance changed:
For a genuine spot
user_balances row, version is the durable producer-owned revision scoped to the authenticated (user, application, token). REST, the subscribe-time snapshot, and a live event for identical state carry the same value. Only a POSITIVE version is rankable: apply a higher version, treat equal as an idempotent duplicate, and ignore a lower version for the whole row. 0 is the legacy sentinel and means exactly what an absent field means — unranked — so normalize the two together and fall back to a full field comparison. A snapshot can carry a persisted 0; ranking it as the oldest revision would suppress the unranked frames a rolling deploy produces. Never use updatedAt, arrival order, or receiver backlog to establish ordering.A collateral transfer that moves the spot wallet row emits a genuine versioned spot event for that leg. Margin/perpetual collateral frames (funding, liquidation, adl, margin fills, parent to risk-bucket allocations, and margin-routed deposit notifications) are not spot rows and intentionally omit version. Their wire identity has no margin-account ID, and some carry a notification amount rather than authoritative whole-row state. They therefore cannot be suppressed by a spot snapshot watermark. Handle an unversioned frame fail-open and refresh authoritative margin-account state; durable margin-collateral reconciliation remains separate work.Example: Track Balance Changes by Reason
React Hook
The SDK provides a dedicated React hook for real-time balances:Conditional Order Updates (Perp TP/SL)
Stream lifecycle events for take-profit and stop-loss conditional orders. Requires authentication.Subscribe
Event payload
version is present on REST reads and subscribe-time snapshots only; live conditional events never carry it. It is an opaque, compare-only revision for the same conditional order, with absent meaning unknown rather than zero. Seed from REST or the subscription snapshot and compare versioned snapshots with snapshot.version >= local.version; apply live events as state updates without version ranking. Do not compare versions across different orders.
@0xmonaco/core parses this frame leniently: an unrecognized orderType, timeInForce, state, side, positionSide, conditionType, triggerSource, associationType, or reason is passed through as-is rather than rejected, because throwing inside the message handler would drop the whole update. Only eventType stays strict. Match these fields with a default branch: reason is free-form on the wire (canonical live values created, activated, cancelled, parent_cancelled, triggered, failed, oco_cancelled, plus armed and ratcheted on a TRAILING_STOP row — tracking began, or the trigger moved, each ratcheted frame carrying the new triggerPrice and watermarkPrice — plus snapshot on the subscribe-time baseline), and timeInForce is the full time-in-force enum (GTC, IOC, FOK, GTD) rather than the GTC/IOC subset the creation paths validate. See Conditional Orders Channel.
When a conditional triggers, the underlying order it submits flows through the regular orders / userOrders channels — subscribe to both to track a TP/SL all the way through to fill. Neither side of the REST two-way link reaches you over this socket at all. triggeredOrder is never on this channel. triggeredOrderId is declared on the payload but no current frame carries it: the live reason: "triggered" update is emitted from the conditional’s pre-trigger model, so it announces the transition without the id that transition produced, and the subscribe-time snapshot is no fallback because it lists only the non-terminal states — PENDING_PARENT, ACTIVE, TRIGGERING — and never a TRIGGERED row. A reconnect does not recover the id either. And the close’s own frames on orders / userOrders never carry conditionalOrderId. Read both from REST — GET /api/v1/orders/conditional/{conditionalOrderId} for the close’s outcome, GET /api/v1/orders or GET /api/v1/orders/{orderId} for the back-pointer. See Trigger Price Is Not Fill Price for why the trigger price and the fill price are independent numbers.
For higher-level usage, see Order Management.
Connection Management
Check Connection Status
Disconnect
Complete Example
React Integration
Best Practices
1. Always Use Trading Pair IDs
2. Clean Up Subscriptions
3. Handle Reconnection
sdk.ws reconnects automatically (unlimited retries by default, full-jitter backoff). To observe status on the built-in client, read sdk.ws.getStatus() — it returns "reconnecting" while backing off:
onResync callback (refetch REST snapshots and reconcile after each reconnect) and an onStatusChange handler, construct the client with createMonacoWebSocket(baseUrl, options) — see Reconnection and resync.
4. Authenticate for Orders Channel
WebSocket URLs
The SDK resolves the WebSocket URL from the
network preset, so you normally don’t set it by hand. The staging and development presets now point at dedicated WebSocket ingresses (wss://ws-staging.apimonaco.xyz/ws, wss://ws-develop.apimonaco.xyz/ws); the previous wss://staging.apimonaco.xyz/ws and wss://develop.apimonaco.xyz/ws paths keep working during the migration window, so custom wsUrl overrides are unaffected.
Next Steps
- OHLCV Data Streaming - Detailed candlestick data guide
- Order Management - Create orders that trigger events
- Trades API - REST API for trade history

