Skip to main content

Overview

markets-service exposes a realtime feed at GET /v1/ws. Connect once and multiplex any number of channel subscriptions over the same socket. Every stream starts with a snapshot and then delivers incremental updates, so you never poll GET /v1/book or GET /v1/trades in a loop.
  • Endpointwss://api.numofx.com/v1/ws
  • Framing — JSON text frames
  • Model — snapshot, then a strictly increasing stream of deltas you can resume after a reconnect
  • Delivery — event-driven push on every state change, with no publish interval (see Update delivery)
Every server frame that belongs to a market carries the seq it reflects. The snapshot frame is taken at a specific seq; each following update is the next seq. A gap in the seq you receive means you missed an event — resume (see Resume after reconnect).

Update delivery

Updates are event-driven, not interval-based. The server pushes an update frame the moment the underlying state changes — an order placed, cancelled, expired, or filled. There is no publish tick, no fixed cadence, and nothing for you to poll. That has a few consequences worth designing for:
  • One frame per state change. Updates are not batched, conflated, or rate-limited. A price level that changes five times produces five update frames, each with its own seq.
  • No cadence to rely on. A quiet market can go minutes without a frame; a single match can deliver a burst of frames in the same millisecond. Drive your UI off arriving frames rather than a timer, and buffer client-side if you need a fixed-cadence render.
  • Silence is not a stalled connection. The only periodic traffic the server sends is a WebSocket ping every 15 seconds. If the market is idle, you receive nothing but pings.
  • Related deltas arrive together. A fill emits its trades, book, and orders deltas as one contiguous block of seq values, so you never see a trade without the matching book and order updates.
If an internal notification is ever lost, a background reconciliation delivers the affected events within a few seconds, in normal seq order. No client action is needed — you see a slightly later frame, never a gap.

Channels

The orders channel is scoped to the authenticated owner address. You must send a valid auth frame before subscribing to it, and you only ever receive your own order events.

Client frames

Send these as JSON text frames. The op field selects the operation.

Subscribe

string
required
One of subscribe, unsubscribe, auth, ping.
string
book, trades, or orders.
string
Canonical market symbol such as USDCcNGN-SPOT, or an asset_address:sub_id pair. Required for book and trades. Optional for orders, where it scopes the stream to a single market.
integer
Optional. The last seq you processed. When present, the server replays events after that point instead of sending a fresh snapshot. See Resume after reconnect.
Subscriptions are idempotent — subscribing again to the same (channel, market) is a no-op.

Unsubscribe

The server replies with an ack frame.

Ping

The server replies with { "type": "pong" }. The server also sends protocol-level WebSocket pings every 15 seconds and drops the connection on missed pongs, so most clients do not need to send ping themselves.

Auth

Required before subscribing to the private orders channel. See Authentication.

Server frames

The type field identifies the frame. Frame shape:

Update payloads

The data on an update frame is a compact delta, not the whole book.
The snapshot payload mirrors the matching REST response: book snapshots use the same shape as GET /v1/book, and trades snapshots use the trade list from GET /v1/trades.

Subscribe and snapshot consistency

On subscribe, the server:
1

Registers your subscription and buffers live deltas

Nothing is dropped between the read and the first frame.
2

Reads the snapshot at a consistent boundary seq

The snapshot data and its seq come from a single consistent read.
3

Sends the snapshot, then flushes deltas after the boundary

Buffered deltas with seq <= boundary are dropped, so you never double-apply an event already reflected in the snapshot.

Resume after reconnect

Track the highest seq you have applied per (channel, market). On reconnect, subscribe with since_seq set to that value:
  • If the events after since_seq are still retained, the server replays them and goes live — no snapshot, no gap.
  • If since_seq is older than the retention horizon, the server sends error with code resume_too_old, followed by a fresh snapshot. Discard your local state and rebuild from the snapshot.

Authentication

The orders channel requires a signed auth frame. Authentication uses an EIP-191 personal_sign signature over a canonical message, recovered to your owner address. Build and sign this exact message (unix seconds, no trailing newline):
Then send the fields in an auth frame:
string
required
Your owner address, 0x + 40 hex characters. Recovery must match this address.
string
required
The personal_sign signature over the canonical message.
string
required
A unique value per auth frame.
integer
required
Unix seconds when the frame was signed. Rejected if far in the future.
integer
required
Unix seconds when the frame stops being valid. Keep the window short; the server enforces a maximum TTL.
On success the server replies { "type": "ack", "message": "authenticated" } and the connection is authorized for that address until it closes. On failure it replies with an error frame, code auth_failed. The REST order history endpoint takes the same frame fields but signs a different message, so a frame signed for one is never accepted by the other.

Error codes

error frames carry a code and a human-readable message.
The server enforces backpressure. If your client cannot keep up with the send rate, the connection is closed with slow_consumer. Reconnect and resume with since_seq rather than holding a slow socket open.