wss://api.testnet.arcus.xyz/v1/ws (mainnet: wss://api.arcus.xyz/v1/ws). The connection multiplexes channel subscriptions and request/response (RPC) calls.
Recommended client resources. For a client consuming the real-time feeds, we recommend at least 2 CPU cores and 4 GB of RAM so message parsing and local book maintenance keep up under load. High-throughput clients (many markets or deep order books) should provision more.
Message envelopes
Subscribe / unsubscribe:{ "type": "subscribed", ... } whose contents is the initial snapshot for channels that deliver one — there is no separate bare acknowledgement frame. (A connection-health check should therefore count channels that delivered contents, not empty acks.) It then streams { "type": "channel_data", ... } updates. Each update carries the channel, the subscription id (for per-market channels), and a contents payload. Timestamps are inside contents (e.g. timestamp, epoch, markEpochNanos); there is no top-level publish timestamp.
Optional subscribe fields:
Request / response: every
post and get message must include a numeric id chosen by the client. The server echoes the same id in its response. IDs do not need to increase — uniqueness among in-flight calls on the connection is sufficient.
What authorizes a subscription
Nothing. Subscribing is never authenticated — not even on the account-scoped channels (account, positions, orders, userFills). A subscribe frame carries no signature, the socket is not bound to an API key, and there is no authenticate step to miss: any connection may subscribe to any address and will receive that address’s balances, positions, open orders, and fills.
This is deliberate, not an oversight. These channels are the streaming form of the account-scoped REST reads (GET /v1/fills, /v1/orders, /v1/positions, /v1/account), which are themselves public and take ?address= without an API key. Account state on Arcus is readable by anyone who knows the address; the API key authorizes writes only.
The WebSocket authenticate method exists, but it is not a prerequisite for subscribing or for signed order methods (each order carries its own signature). See Authentication.
Placing orders
Order methods are asynchronous. The server returns202 with status: "ACK" (or CANCEL_ACKNOWLEDGED); subscribe to the orders or userFills channels to observe the lifecycle.
Dead man’s switch (scheduleCancel)
scheduleCancel is a server-side dead man’s switch: you arm a deadline, and if you don’t refresh it before it elapses, the gateway automatically cancels your resting orders. Use it so a crashed or disconnected trading process doesn’t leave stale quotes on the book.
It’s a signed post method — same envelope and signing rules as other order methods (see Authentication) — and is also available over REST as POST /v1/scheduleCancel.
- Arm or refresh — send
time, an absolute epoch-microsecond deadline between 5 seconds and 5 minutes in the future. Keep the switch alive by sending a new, latertimebefore the current one elapses (typicallynow + 60son a short refresh interval); reusing a pasttimedoes not extend it. - Disarm — omit
time(or sendnull).
marketId for an account-wide switch (cancels every open order across all markets when it fires), or set it to scope the switch to a single market. Per-market switches are independent of the account-wide switch and of each other on the same subaccount — each keeps its own deadline and cancels only its own market — so you can run one process per market, each arming its own switch. marketId selects which switch a given arm/refresh/disarm targets.
"marketId": 1 to the payload to scope the switch to a single market.
Limits. Two quotas bound the switch, on top of the per-subaccount cancel pool that every arm/refresh draws from (see Rate limits):
- Auto-fires are capped at 10 per UTC day per subaccount, shared across all of that subaccount’s switches.
- A wallet may hold at most 50 switches armed at once, across all its subaccounts and markets. Arming a new switch beyond that returns
429; refreshing an already-armed switch is always allowed.
orders channel like any other cancel.
Channels
See the Channels reference for the full list and per-channel payload schemas.Sequence numbers
Streamed messages carry sequence numbers so you can order events, detect dropped messages, and resync after a reconnect. Three scopes exist:- Global — one monotonic counter across the whole exchange, incremented for every event the matching engine processes. Use it to order events across markets and to tell whether you’ve missed anything.
- Per-market — a counter local to a single market.
- Per-account — a counter local to a single account.
The
lastSequenceId on account and positions snapshots is the per-account sequence at snapshot time — the same counter as AccountUpdate.sequenceNumber and Order.sequenceNumber. Streaming positions deltas carry this same per-account counter: each PositionUpdate envelope stamps lastSequenceId and each position row carries sequenceNumber, so a delta can be ordered directly against account updates and used to discard updates already contained in a snapshot. For account state, treat each snapshot as the reconciliation point: the account channel re-snapshots every 5 seconds, and a re-subscribe fetches a fresh snapshot on any other account-scoped channel.
Resyncing the order book
Forl2Orderbook, the snapshot’s lastSequenceId is the per-market sequence of the last update it reflects. Seed from the snapshot, then apply every l2OrderbookUpdates delta whose lastSequenceId is greater than the snapshot’s. The snapshot is a periodic generation that lags slightly behind the live delta head, so the first delta may be a few sequences ahead of the snapshot’s lastSequenceId — this boundary gap is expected and self-heals, so do not re-subscribe on it. Once you are applying deltas, sequences are contiguous: a gap that appears mid-stream means you missed a delta, and only then should you re-subscribe for a fresh snapshot. Use globalSequenceId to order order-book events against other markets.
Order-book sequence contiguity
The order-book channels share the same per-marketlastSequenceId, but only l2OrderbookUpdates (and the l2Orderbook snapshot it baselines against) is contiguous — the only stream where a gap is meaningful. Treat each field by its guarantee:
bbo is a snapshot channel: every frame carries the full current top of book and self-replaces the previous one, so a dropped frame self-heals on the next — there is no in-band gap detection and none is needed. If you need a contiguous, gap-detectable, replayable order-book stream, use l2Orderbook / l2OrderbookUpdates.
The per-market lastSequenceId does not reset on a reconnect, but don’t assume continuity across the reconnect gap. On every (re)subscribe, take the fresh snapshot’s lastSequenceId as the new baseline and resume gap-checking from there.
Errors
Responses include an HTTP-likestatus and either a result or an error object with type, message, and optional field details.