# Claude Code Source: https://docs.arcus.xyz/ai-tools/claude-code Add the Arcus MCP server to Claude Code Give [Claude Code](https://claude.com/claude-code) access to the Arcus docs so it can answer questions and generate code grounded in the live REST and WebSocket reference. ## Prerequisites * Claude Code installed (`npm i -g @anthropic-ai/claude-code`) * An Anthropic account (Claude Pro, Max, Team, or API access) ## Setup In your terminal, run: ```bash theme={null} claude mcp add --transport http arcus-docs https://docs.arcus.xyz/mcp ``` Optional scope: * `-s user` — available across every project on this machine * `-s project` — saved to `.mcp.json` in the repo (shared with teammates) * default (no flag) — only this project, only you Start a Claude Code session and run: ``` /mcp ``` Select **arcus-docs**, then **Authenticate**. A browser window opens, you sign in, and the OAuth token is stored locally. You only do this once. ```bash theme={null} claude mcp list ``` You should see `arcus-docs ✓ Connected`. Inside a session, `/mcp` will also show the tools the server exposes (search, fetch, etc.). Ask something that exercises the spec: > *Which endpoint accepts take-profit and stop-loss parameters — `placeOrder` or `batchPlaceOrders`? Show me the request shape.* Claude Code will call the MCP server, pull the relevant page, and answer from the actual spec instead of guessing. ## Removing the server ```bash theme={null} claude mcp remove arcus-docs ``` ## Notes * The first request after authentication may take a moment while the client establishes the streaming HTTP session. * If the `Authenticate` step never opens a browser, update Claude Code and re-add the server — HTTP-transport OAuth support is recent. * For the underlying server details and OAuth metadata, see the [MCP server overview](/ai-tools/mcp). # Cursor Source: https://docs.arcus.xyz/ai-tools/cursor Add the Arcus MCP server to Cursor Connect [Cursor](https://cursor.com) to the Arcus docs MCP server so its agent and chat panes can answer questions and write code grounded in the live spec. ## Prerequisites * Cursor 0.45 or newer (MCP support required) ## Setup In Cursor, open **Settings → Cursor Settings → MCP** (or run the command palette: *MCP: Add new server*). Add an entry with the following configuration: ```json theme={null} { "mcpServers": { "arcus-docs": { "url": "https://docs.arcus.xyz/mcp" } } } ``` Save the file. Cursor stores user-level MCP servers in `~/.cursor/mcp.json` and project-level ones in `.cursor/mcp.json` at the repo root — pick whichever scope fits. Cursor will prompt you to authenticate the first time it talks to the server. Click **Sign in**, complete the OAuth flow in your browser, and return to Cursor. Open the agent or chat pane and ask: > *Show me how to subscribe to the orderbook WebSocket channel for `BTC-USD`.* Cursor will call the MCP server and ground its answer in the live Arcus reference. ## Notes * If the OAuth popup never appears, upgrade Cursor — remote MCP with OAuth landed in recent versions. * For the underlying server details, see the [MCP server overview](/ai-tools/mcp). # Arcus MCP server Source: https://docs.arcus.xyz/ai-tools/mcp Connect your AI assistant to the Arcus docs over the Model Context Protocol Arcus exposes its documentation as a remote [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server. Once connected, your AI assistant can search the REST and WebSocket reference, surface examples, and answer questions like *"where do I set TP/SL?"* without leaving your editor. ## Server details | Field | Value | | - | - | | URL | `https://docs.arcus.xyz/mcp` | | Transport | HTTP (streamable) | | Authentication | OAuth 2.0 with dynamic client registration | | Discovery | `/.well-known/oauth-protected-resource` | Browsing the MCP URL directly in a browser returns `401 invalid_token` — that is the standard MCP-spec advertisement of the OAuth flow ([RFC 9728](https://www.rfc-editor.org/rfc/rfc9728)). MCP clients follow the `WWW-Authenticate` header to complete authentication automatically; you do not need a personal access token. ## What you can ask After connecting, your assistant can answer questions grounded in the live spec: *"Which endpoint takes TP/SL — `placeOrder` or `batchPlaceOrders`?"* *"Show me how to sign a request with my Ed25519 API key."* *"What channels stream order book updates and what do their payloads look like?"* *"Give me a TypeScript snippet that cancels all open orders for one market."* ## Connect your editor Remote MCP via `claude mcp add`. Add through Cursor MCP settings. Configure in Cascade's MCP panel. ## Troubleshooting Expected. The endpoint is an OAuth-protected MCP resource; browsers do not run the MCP OAuth flow. Use a real MCP client (Claude Code, Cursor, Windsurf, Claude Desktop) — it will redirect you through a one-time authentication and then connect. Update your client to the latest version — MCP HTTP-transport OAuth support is recent. For Claude Code: `npm i -g @anthropic-ai/claude-code`. Then remove and re-add the server. The MCP server reflects the current published spec. If something looks wrong, open an issue and link to the page — we resync from upstream on every merge. # Windsurf Source: https://docs.arcus.xyz/ai-tools/windsurf Add the Arcus MCP server to Windsurf Connect [Windsurf](https://windsurf.com) and its Cascade agent to the Arcus docs MCP server. ## Prerequisites * Windsurf with MCP support (recent versions) ## Setup In Windsurf, open **Cascade → Plugins / MCP servers** (the entry point is in the Cascade sidebar). Add a new server with the following configuration: ```json theme={null} { "mcpServers": { "arcus-docs": { "serverUrl": "https://docs.arcus.xyz/mcp" } } } ``` Windsurf reads MCP servers from `~/.codeium/windsurf/mcp_config.json`. Save and reload Cascade. On the first request to the server, Windsurf opens a browser tab for OAuth sign-in. Complete the flow and return to Cascade — the connection becomes active. Ask Cascade: > *What does the response to `GET /v1/positions?address={address}` look like? Include the field types.* Cascade will call the MCP server, fetch the relevant page, and ground its answer in the live spec. ## Notes * If you don't see an authentication prompt, your Windsurf build may predate remote-MCP OAuth support — update and try again. * For the underlying server details, see the [MCP server overview](/ai-tools/mcp). # Account Source: https://docs.arcus.xyz/api-reference/account/account > **Endpoint:** `wss://api.arcus.xyz/v1/ws` — one socket multiplexes every channel and RPC request. Click **Connect**, then send the example subscribe message below to start receiving data. Account state snapshot and streaming updates for a single sub-account. **No authentication required at subscribe time** — pass the account's Ethereum address as `id`. Snapshot on subscribe includes open positions, balances, and margin info. Periodic re-snapshots are sent every 5 seconds. Update `type` values: `PLACED`, `CANCELED`, `MARGIN_CANCELED`, `REJECTED`, `TPSL_PLACED`, `TPSL_CANCELED`, `TPSL_TRIGGERED`, `LIQUIDATION`, `ADL`. `FILL` events are routed to the `userFills` channel only and do not appear here. `LIQUIDATION` and `ADL` are fanned out to both `account` and `userFills`. # Account attribute updates Source: https://docs.arcus.xyz/api-reference/account/accountattributeupdates > **Endpoint:** `wss://api.arcus.xyz/v1/ws` — one socket multiplexes every channel and RPC request. Click **Connect**, then send the example subscribe message below to start receiving data. Per-account attribute state, discriminated by entry `type`. `type: "leverage"` entries carry effective leverage and margin mode per market. `isolated` and `marginMode` are always present — in the snapshot they reflect the current persisted state; in streaming frames they reflect the account's margin mode after the triggering event, defaulting to CROSS if the margin mode lookup fails. `type: "feeTier"` entries carry account-level fee tier state. `type: "leverageReject"` entries (streaming only) report that a setLeverage request was refused by the engine — this is the `REJECTED` half of the setLeverage lifecycle; an applied change arrives as a `leverage` entry. When `isolated` was also supplied on the request, both the leverage and margin mode outcomes are folded into the same entry via the optional `marginModeStatus` and `marginModeRejectReason` fields. Snapshot returns one leverage entry per market (carrying both leverage and margin mode state) plus, when available, one fee tier entry. Streaming delivers one entry for each changed attribute. # Account transfer updates Source: https://docs.arcus.xyz/api-reference/account/accounttransferupdates > **Endpoint:** `wss://api.arcus.xyz/v1/ws` — one socket multiplexes every channel and RPC request. Click **Connect**, then send the example subscribe message below to start receiving data. Account transfer events (deposits, withdrawals, transfers, referral claims) for an account. Snapshot on subscribe contains the most recent events newest-first (`isSnapshot: true`); each streaming update carries a single event (`isSnapshot: false`) as it is applied by the engine. Amounts and balances are human-readable decimal strings in quote currency. `globalSequenceId` gives strict ordering across all engine events. The equivalent REST read is `GET /v1/accountTransferUpdates`. # Funding Source: https://docs.arcus.xyz/api-reference/account/funding > **Endpoint:** `wss://api.arcus.xyz/v1/ws` — one socket multiplexes every channel and RPC request. Click **Connect**, then send the example subscribe message below to start receiving data. Funding payment stream for an account. Snapshot on subscribe contains up to 100 most recent funding payments from the last 30 days; for older history query REST `GET /v1/funding` with an explicit `from`. Live updates deliver one event per (market, account) at each funding payment time. Sign convention on `payment`: positive = received, negative = paid. `time` is the Unix-microsecond funding payment time. # Orders Source: https://docs.arcus.xyz/api-reference/account/orders > **Endpoint:** `wss://api.arcus.xyz/v1/ws` — one socket multiplexes every channel and RPC request. Click **Connect**, then send the example subscribe message below to start receiving data. Open and recently closed order updates for an account. Snapshot on subscribe includes all open orders and up to 100 recently closed orders. Streaming updates use a unified shape for both status changes and fill events. Snapshot is sent once on connect only. Subscribe to this channel alongside `userFills` to observe the full order lifecycle after placing an order via the `orderOperations` channel. Optionally scoped to one market: pass `market` on subscribe (ticker or numeric id) and the snapshot, the live frames, and the post-snapshot catch-up all carry only that market. The filter is part of subscription identity, so one connection can hold several market-scoped views of the same account; `unsubscribe` must repeat the same `market`. # Positions Source: https://docs.arcus.xyz/api-reference/account/positions > **Endpoint:** `wss://api.arcus.xyz/v1/ws` — one socket multiplexes every channel and RPC request. Click **Connect**, then send the example subscribe message below to start receiving data. Open positions for a single sub-account. Snapshot contents use `PositionsSnapshot`: the REST `GET /positions` positions map plus `isSnapshot: true`, `lastSequenceId`, `address`, and `accountIndex`. Updates carry one `PositionUpdate` event per frame. `cumulativeFunding` is absent from streaming events — use the snapshot for funding horizons. On close (size drops to 0), the row is emitted in `FLAT` form with zeroed financials. `leverage` reflects the user's per-(account, market) override (set via REST `POST /v1/setLeverage`) when present, otherwise the market default. A `PositionUpdate` is also emitted when the user adjusts leverage on a market they already hold a position in, and when they add or remove isolated margin via `POST /v1/adjustIsolatedMargin` (that frame carries `requestId` so clients can correlate it with the 202 ACK, and `marginMode: ISOLATED`). Every perp position row — snapshot and streaming — includes `marginMode` (`CROSS` or `ISOLATED`) so clients can tell the collateral mode without a REST round-trip. Isolated rows set `marginUsed` to the posted isolated-leg pool (not IMR at mark); the full isolated-leg cash balance is derivable as `marginUsed + borrowedCapital`. Optionally scoped to one market: pass `market` on subscribe (ticker or numeric id) and the snapshot, the live frames, and the post-snapshot catch-up all carry only that market. The filter is part of subscription identity, so one connection can hold several market-scoped views of the same account; `unsubscribe` must repeat the same `market`. # User fills Source: https://docs.arcus.xyz/api-reference/account/userfills > **Endpoint:** `wss://api.arcus.xyz/v1/ws` — one socket multiplexes every channel and RPC request. Click **Connect**, then send the example subscribe message below to start receiving data. Per-user fill stream. Snapshot on subscribe contains recent fills; streaming updates deliver each new fill as orders are matched. Snapshot is sent once on connect only. `LIQUIDATION` and `ADL` fills also appear here carrying the `liquidation` marker (`method` + `liquidatedUser`) — on the liquidated account's leg for `LIQUIDATION` and the deleveraged counterparty's leg for `ADL` — so subscribers tracking realized closures can observe forced fills on either `account` or `userFills`. The marker is persisted, so it is present in snapshots as well as live updates. Optionally scoped to one market: pass `market` on subscribe (ticker or numeric id) and the snapshot, the live frames, and the post-snapshot catch-up all carry only that market. The filter is part of subscription identity, so one connection can hold several market-scoped views of the same account; `unsubscribe` must repeat the same `market`. # Authentication Source: https://docs.arcus.xyz/api-reference/authentication Ed25519 API keys and request signing Arcus is non-custodial. The server holds only the public half of your API key, and registration is gated by an ECDSA signature from the master Ethereum address that owns the key. ## Generate an Ed25519 key pair The public key becomes your `apiKey`. The [Arcus web app](https://testnet.arcus.xyz/api-keys) can generate and register a key for you in a few clicks — the easiest path, and where the [trading guides](/guides/rest-trading#step-2-get-an-api-key) start; the manual steps below are for managing keys yourself. ```bash theme={null} openssl genpkey -algorithm ed25519 -out private.pem API_KEY=$(openssl pkey -in private.pem -pubout -outform DER | tail -c 32 | xxd -p -c 32) ``` ## Register the key (REST) The registration request must be ECDSA-signed by the Ethereum address that owns the key. ```bash theme={null} curl -X POST https://api.testnet.arcus.xyz/v1/createApiKey \ -H "Content-Type: application/json" \ -d "{ \"address\": \"0x...\", \"publicKey\": \"$API_KEY\", \"apiWalletName\": \"Arcus\", \"validUntil\": 1777479871997, \"signature\": { \"r\": \"0x...\", \"s\": \"0x...\", \"v\": \"0x1b\" } }" ``` The `signature` is the `(r, s, v)` triple from the wallet's **EIP-712 typed-data signature** (`eth_signTypedData_v4`) — see the [`POST /v1/createApiKey`](/api-reference/onboarding/create-api-key) endpoint reference for the domain, types, and complete signing examples. `createApiKey` is REST-only. The WebSocket server returns `501 NotImplemented` for it. ## Sign protected requests Every mutating request is authenticated with your Ed25519 API key. You always send the **same three values** — only *what you put in the signature* changes per operation: | Value | REST | WebSocket | | - | - | - | | API key (the public key) | `X-API-Key` header | `apiKey` envelope field | | Unix-**nanosecond** timestamp | `X-Timestamp` header | `timestamp` envelope field | | Ed25519 signature (128 hex chars) | `X-Signature` header | `signature` envelope field | There are **two signing schemes**. Pick by operation: | Operation | Scheme | | - | - | | `placeOrder`, `cancelOrder`, `modifyOrder` | **Scheme 1 — typed payload** | | `batchPlaceOrders`, `batchCancelOrders` | **Scheme 1**, applied once per element (see [Batches](#batches)) | | `cancelAllOrders`, `setLeverage`, WebSocket `authenticate` | **Scheme 2 — legacy message** | | Referral writes (`/v1/affiliate/*` POSTs) and signed referral reads (`inviteCodes`, `info`, `code`, `referees`, `commissions`) | **Scheme 2** (a read has no body, so it signs `timestamp + action`) | Batches are **not** a third scheme — they are Scheme 1 signed separately for each order. Both schemes produce an Ed25519 signature; they differ only in which bytes you sign. ### Scheme 1 — typed payload (orders) For `placeOrder`, `cancelOrder`, and `modifyOrder`, the signed message **is the request payload itself** — a compact, key-sorted JSON object built from engine-native integer values. There is no timestamp or action prefix: ``` signature = ed25519(canonical_payload) ``` The timestamp lives *inside* the payload as `ct` (it must equal the `X-Timestamp` you send). Keys are in fixed alphabetical order with no whitespace. The address (`ad`) is the **only** case-folded field — it is lowercased before signing. Every other string is signed **exactly as you send it**: the client id (`c`) and the order id (`id`) are byte-for-byte verbatim, so a mixed-case `clientId` must be signed in its original case and sent in that same case, or verification fails. | Operation | `op` | Canonical payload | | - | - | - | | `placeOrder` | `1` | `{"ad":"0x…","ai":N,"c":"…","ct":N,"g":N,"m":N,"op":1,"p":N,"q":N,"r":0,"s":N,"t":N,"v":1}` | | `cancelOrder` | `2` | `{"ad":"0x…","ai":N,"c":"…","ct":N,"id":"…","m":N,"op":2,"v":1}` | | `modifyOrder` | `3` | `{"ad":"0x…","ai":N,"c":"…","ct":N,"g":N,"id":"…","m":N,"op":3,"p":N,"q":N,"r":0,"s":N,"t":N,"v":1}` | Fields: * `ad` — master Ethereum address, lowercase `0x`-hex * `ai` — account (subaccount) index * `c` — client id; **omitted entirely when empty** * `ct` — client timestamp in Unix **nanoseconds**; equals the `X-Timestamp` you send * `g` — `goodTilTime` in **nanoseconds** (the request's microsecond `goodTilTime` × 1000; required on every order — see below) — `placeOrder` and `modifyOrder` * `id` — server `orderId`. **Required** for `modifyOrder`; for `cancelOrder` it is **omitted when canceling by client id** * `m` — market id * `op` — operation: `1` place, `2` cancel, `3` modify (`4` = untriggered TPSL — same fields as place) * `p` — price in **integer ticks** = `price ÷ market tickSize` * `q` — quantity in **integer quantums** = `size ÷ market stepSize` * `r` — reduce-only, `0` or `1` (integer, not a boolean) — `placeOrder` and `modifyOrder` * `s` — side: `0` buy, `1` sell — `placeOrder` and `modifyOrder` * `t` — time-in-force: `0` GTT, `1` FOK, `2` IOC, `3` ALO — `placeOrder` and `modifyOrder` * `v` — payload version (currently `1`) For `cancelOrder`, provide exactly one of `id` (server order id) or `c` (client id). For `modifyOrder`, `id` is **always required**; include `c` only if the resting order was placed with a client id (it echoes that original value, and the engine rejects a mismatch). The `g`, `r`, `s`, and `t` fields in a `modifyOrder` **echo the resting order's immutable attributes** so the validator can verify the signature without fetching the original — passing a new `g` performs a cancel-replace with a fresh expiry. Because `p` and `q` are integers, convert the human-readable decimal price and size to ticks and quantums using the market's `tickSize` and `stepSize` from [`GET /v1/markets`](/api-reference/public/get-markets) — the conversion must be exact (`price ÷ tickSize` with no remainder). The divisor is **always the market's top-level `tickSize`**, at every price — a market's `tickTiers` ladder never changes the signed integer. Tiers only constrain which prices are *accepted*: a limit price in a coarser band must also be a multiple of that band's tick or the order is rejected (`… is not a multiple of tick size … for its price tier`); it is never re-interpreted under a different denominator, so code that signs with `tickSize` stays correct even after a market's price crosses a tier boundary. **Every order requires `goodTilTime` / `g`** — including `1` FOK and `2` IOC — and it must be **at least one month in the future** when the API handler processes the order; nearer or missing values are rejected. FOK and IOC orders never rest, but the field still acts as a mandatory replay-protection timestamp on them. #### Batches A batch (`batchPlaceOrders` / `batchCancelOrders` / `batchModifyOrders`) is just Scheme 1 repeated: sign **each element** as its own typed payload, with every element sharing the one `X-Timestamp` you send as its `ct`. Each element carries its own `signature` field, and the shared `X-API-Key` + `X-Timestamp` authenticate the request. On REST the `X-Signature` header must still be **present** — set it to any one element's signature; the batch is verified per element, not against this header (omitting it rejects every element with `invalid order signature`). A top-level `grouping` (TPSL) field is not signed. ### Scheme 2 — legacy message (everything else) `cancelAllOrders`, `setLeverage`, WebSocket `authenticate`, and the [referral endpoints](/api-reference/referral/get-affiliate-info) sign the timestamp, then the action, then the canonical JSON body — concatenated with no delimiters: ``` signature = ed25519(timestamp + action + canonical_json(body)) ``` `action` is the camelCase final path segment (`/v1/cancelAllOrders` → `cancelAllOrders`); over WebSocket it's the request `type`. A signed `GET` has no body, so its message is `timestamp + action` (e.g. `/v1/affiliate/inviteCodes` → `inviteCodes`), and its `address` query parameter must be the API key's master Ethereum address. The HTTP method is not part of the message. `canonical_json` serializes the body with sorted keys and no whitespace. You do not need to call `authenticate` first for signed requests. ## Your testnet account A new account starts **empty**. `POST /v1/createApiKey` registers your key but credits no balance, so [`GET /v1/account`](/api-reference/public/get-account) returns `404 "this account has no activity yet"` until your first deposit. (Testnet previously auto-credited \$5,000 of paper-trading collateral; that has been removed.) Most testnet users never fund manually: in the Arcus testnet web app, connect your wallet and click **Testnet Deposit** to instantly credit **\~\$1,000 USDC** of paper-trading collateral. If you trade via the API and need a larger balance — or want to fund programmatically — deposit collateral on-chain instead; see [Fund a testnet account](/guides/fund-testnet-account). Once funded, the deposit appears as a `DEPOSIT` entry in [`GET /v1/accountTransferUpdates`](/api-reference/public/get-account-transfer-updates) and your balance shows up on `GET /v1/account`: ```bash theme={null} curl "https://api.testnet.arcus.xyz/v1/account?address=0x..." \ -H "X-API-Key: $API_KEY" # → { "equity": "...", "netDeposits": "...", "freeCollateral": "...", ... } ``` Testnet trades against a paper-trading universe of perpetuals across crypto, equities, commodities, and indices — all quoted against USD. The full list lives at [`GET /v1/markets`](/api-reference/public/get-markets); subscribe to the [`markets`](/api-reference/channels#markets) WebSocket channel for live updates. The universe is the same for every API key. # Add or remove margin on an isolated-mode position Source: https://docs.arcus.xyz/api-reference/exchange/add-or-remove-margin-on-an-isolated-mode-position /api-reference/openapi.yml post /v1/adjustIsolatedMargin Move margin between the cross bucket and an isolated-mode position's leg. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Move margin between the cross bucket and an isolated-mode position's leg. Requires `address` query parameter or JSON `address` field (must match `X-API-Key`'s master if both are present). `accountIndex` is required in the signed JSON body (default `0`); it may also be sent as `?accountIndex=`. If both are present they must match. The signed body always includes `accountIndex` so a signature cannot be replayed against a different subaccount. `amount` is dollars as a decimal string (e.g. `"100"` for \$100) — do not send quote quantums; the gateway converts dollars to quantums for the engine. Positive adds margin to the leg (cross → isolated), negative removes margin from the leg (isolated → cross). ### Response behavior **Asynchronous.** The endpoint returns `200 OK` (engine confirmed the move), `422 Unprocessable Entity` (engine rejected — see `rejectReason`), or `202 Accepted` (request forwarded but the confirmation didn't arrive within the timeout; the engine may still apply the move — observe the next `positions` WebSocket frame to confirm). Adding margin fails with `UNDERCOLLATERALIZED` if the cross bucket doesn't have enough free collateral to fund the transfer. An already-underwater isolated leg may still be topped up: the add is accepted as long as it does not make either domain's free collateral worse. Removing margin fails with `UNDERCOLLATERALIZED` if it would leave the isolated leg's equity below its initial margin requirement, or deepen an already-undercollateralized leg. The engine rolls back atomically on either path. An amount that would overflow int64 quote balances is rejected as `INVALID_AMOUNT`. # Batch cancel orders Source: https://docs.arcus.xyz/api-reference/exchange/batch-cancel-orders /api-reference/openapi.yml post /v1/batchCancelOrders Cancel up to 100 orders in a single request. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Cancel up to 100 orders in a single request. Requires `address` query parameter matching the master Ethereum address for `X-API-Key`. **Signed per order.** This endpoint requires the `X-Signature` header to be **present** (non-empty); its value is not verified — set it to any one element's signature. Omitting it rejects every element with `invalid order signature`. Each element of `cancels` carries its own `signature` over the **ordersign typed canonical payload** for that cancel (`op` 2) — the same bytes a standalone `cancelOrder` signs; the shared `X-API-Key` + `X-Timestamp` headers authenticate the batch. ### Response behavior **Asynchronous.** The endpoint returns either `202 Accepted` (the common case — the batch cancel was forwarded to the matching engine and per-order rows do not carry terminal state) or `200 OK` (the gateway already had definitive state by the time it responded; per-order `status` reflects that state). In both cases each row echoes the targeted `orderId` / `clientId` so subscribers can correlate WebSocket events with the request. To observe each order's resulting state (`CANCELED` / `FILLED`) clients **must** subscribe to the `orders` WebSocket channel. # Batch modify orders Source: https://docs.arcus.xyz/api-reference/exchange/batch-modify-orders /api-reference/openapi.yml post /v1/batchModifyOrders Modify up to 100 open orders in a single request. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Modify up to 100 open orders in a single request. Every row must share the same `(address, accountIndex)` — batch modifies are scoped to one subaccount. Requires `address` query parameter (or a uniform per-row `address`) matching the master Ethereum address for `X-API-Key`. **Signed per modify.** This endpoint requires the `X-Signature` header to be **present** (non-empty); its value is not verified — set it to any one element's signature. Omitting it rejects every element with `invalid order signature`. Each element of `modifies` carries its own `signature` over the same typed canonical modify payload as a standalone `modifyOrder`, so the signed bytes are identical whether the modify is submitted alone or in a batch; the shared `X-API-Key` + `X-Timestamp` headers authenticate the batch. **Failures are per-item.** A row that fails validation or signature verification is returned with `status: ERROR` and an `error` message; the remaining rows still reach the matching engine. The whole batch is rejected only for envelope-level problems (empty/oversized array, mixed `(address, accountIndex)`, bad auth). **Speed-bump semantics match `batchPlaceOrders`:** the batch is forwarded to the engine as one unit. If every modify targets a post-only (ALO) resting order the batch skips the taker speed bump entirely; if ANY modify targets a non-post-only order the whole batch is subject to it. **All-ALO batches are prioritized like cancels.** An all-ALO batch can only shrink or reprice maker orders — it can never take liquidity — so it is routed on the high-priority path ahead of order placements, the same as cancels. Like a cancel, it may therefore be processed ahead of earlier not-yet-sequenced placements (including its own targets, which would then reject as `ORDER_NOT_FOUND_FOR_MODIFY`). Under cancel-replace modify semantics such a rejected modify is also buffered briefly and applied when the target placement arrives, mirroring buffered-cancel behavior — see `POST /v1/modifyOrder`. ### Response behavior **Asynchronous.** Returns either `202 Accepted` (the common case) or `200 OK` (the gateway already had definitive per-row state). Each row echoes `orderId` / `clientId` so clients can correlate `orders` WebSocket channel events. # Cancel all open orders Source: https://docs.arcus.xyz/api-reference/exchange/cancel-all-open-orders /api-reference/openapi.yml post /v1/cancelAllOrders Cancel every open order for `(address, accountIndex)`. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Cancel every open order for `(address, accountIndex)`. Pass `marketId` to restrict the cancel to a single market; omit it to cancel every open order across every market on the account. Untriggered TPSL orders within the scope are canceled too. ### Response behavior **Asynchronous.** Returns `202 Accepted` with `status = CANCEL_ALL_ACKNOWLEDGED` once the request is forwarded to the matching engine. Per-order `CANCELED` events flow over the `orders` WebSocket channel as the engine processes each affected order — clients **must** subscribe to that channel to observe the resulting state. Rate-limit cost is one unit per request, identical to `cancelOrder` and `batchCancelOrders`, regardless of how many orders end up being canceled. # Cancel order Source: https://docs.arcus.xyz/api-reference/exchange/cancel-order /api-reference/openapi.yml post /v1/cancelOrder Cancel an open order by ID. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Cancel an open order by ID. Requires `address` query parameter matching the master Ethereum address for `X-API-Key`. ### Response behavior **Asynchronous.** The endpoint returns either `202 Accepted` (the common case — the signed cancel was forwarded to the matching engine and the body does not carry terminal state) or `200 OK` (the gateway already had definitive state for the cancel by the time it responded; `status` reflects that state). In both cases the body echoes the targeted `orderId` / `clientId` so subscribers can correlate WebSocket events with the request. To observe the resulting state (`CANCELED`, or `FILLED` if the order completed before cancel was processed) clients **must** subscribe to the `orders` WebSocket channel. # Modify order Source: https://docs.arcus.xyz/api-reference/exchange/modify-order /api-reference/openapi.yml post /v1/modifyOrder Modify the price and/or quantity of an open order. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Modify the price and/or quantity of an open order. A modify is applied **in place** — preserving queue priority — only when it keeps the same price and reduces size. Any other change (a price move, or a size increase) is handled as an atomic **cancel + replace**, which loses queue priority. Requires `address` query parameter matching the master Ethereum address for `X-API-Key`. Identify the order to modify by **exactly one** of `orderId` or `clientId` — setting both, or neither, is rejected with a validation error. **ALO modifies are prioritized like cancels.** A modify on a post-only (ALO) order can only shrink or reprice a maker order — it can never take liquidity — so it is routed on the high-priority path ahead of order placements, the same as cancels (and skips the taker speed bump). Modifies with any other `timeInForce` ride the normal path and are subject to the speed bump. Like a cancel, a prioritized modify may be processed ahead of an earlier not-yet-sequenced placement of its own target, which would then reject as `ORDER_NOT_FOUND_FOR_MODIFY` (and, under cancel-replace modify semantics, be buffered briefly and applied when the placement arrives). No separate cancel + place event pair is emitted. On the `orders` WebSocket channel the client observes: a non-crossing modify → a single `orders` update; a crossing modify → a fill followed by a `PLACED` (or `REJECTED`). Correlate by `orderId` / `clientId`. **Cancel-replace semantics (feature-flagged rollout).** When the `modify_cancel_place` engine flag is active, every modify unconditionally cancels the original order and re-places it under the **same `orderId`**: the `orders` channel emits a `CANCELED` update with `cancelReason: MODIFY_CANCELED` immediately followed by the replacement outcome (`PLACED`, fills, or `REJECTED`). Under these semantics: * A modify whose target has not yet reached the book is rejected with `ORDER_NOT_FOUND_FOR_MODIFY` but also **buffered briefly** — if the target placement arrives shortly after, it is canceled (`MODIFY_CANCELED`) and replaced with the modify's parameters, mirroring buffered-cancel behavior. * Untriggered TPSL orders cannot be modified: the modify is rejected with `MODIFY_TPSL_NOT_SUPPORTED` and a live TPSL survives unchanged. If a buffered modify meets an arriving TPSL placement, the TPSL is canceled as a precaution. * A pending cancel always dominates a modify for the same order; the modify is rejected with `MODIFY_SUPERSEDED_BY_CANCEL` and the order's final state is canceled. ### Response behavior **Asynchronous.** Returns either `202 Accepted` (common case — forwarded to the matching engine, body carries the `orderId` for correlation) or `200 OK` (gateway already has definitive state for the order). Subscribe to the `orders` WebSocket channel to observe the order's lifecycle. # Place batch orders Source: https://docs.arcus.xyz/api-reference/exchange/place-batch-orders /api-reference/openapi.yml post /v1/batchPlaceOrders Place up to 100 orders in a single request. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Place up to 100 orders in a single request. Requires `address` query parameter matching the master Ethereum address for `X-API-Key` (same as single `POST /placeOrder`). **Signed per order.** This endpoint requires the `X-Signature` header to be **present** (non-empty); its value is not verified — set it to any one element's signature. Omitting it rejects every element with `invalid order signature`. Each element of `orders` carries its own `signature` over the **ordersign typed canonical payload** for that order (`op` 1) — the same bytes a standalone `placeOrder` signs; the shared `X-API-Key` + `X-Timestamp` headers authenticate the batch. The whole batch consumes a single replay slot. The top-level `grouping` is not individually signed. ### Response behavior **Asynchronous.** The endpoint returns either `202 Accepted` (the common case — the batch was forwarded to the matching engine and per-order rows do not carry terminal state) or `200 OK` (the gateway already had definitive state for the batch by the time it responded; per-order `status` reflects that state). In both cases each row echoes the accepted `orderId` / `clientId` so subscribers can correlate WebSocket events with the request. To observe each order's full lifecycle (`OPEN`, `FILLED`, `CANCELED`, `REJECTED`, fills, etc.) clients **must** subscribe to the `orders` WebSocket channel (and `userFills` for trade-level events). Per-order validation failures are still returned synchronously as `status: ERROR` with an `error` message in the corresponding `responses[]` entry; valid rows are forwarded to the matching engine (partial success). ### TPSL groupings Set the top-level `grouping` field to submit a take-profit / stop-loss bundle instead of plain orders. The three grouping modes are: #### partialTpsl Place 1–2 TPSL legs sized to a user-specified quantity. Every order must set `tpslType` (`STOP_LOSS` or `TAKE_PROFIT`) and `stopPrice`. Two legs must be one of each trigger type. ```json { "grouping": "partialTpsl", "orders": [ { "tpslType": "TAKE_PROFIT", "stopPrice": "70000.0", "reduceOnly": true, ... }, { "tpslType": "STOP_LOSS", "stopPrice": "55000.0", "reduceOnly": true, ... } ] } ``` #### positionTpsl Same shape as `partialTpsl` but the engine resizes each leg to close the account's **full open position** at trigger time (`isPositionTPSL` is implicitly true). Use `quantity: "0"` on each leg; the engine replaces it at trigger. At most one TP and one SL may be active per account+market — a second placement is rejected with `POSITION_TPSL_ALREADY_EXISTS`. ```json { "grouping": "positionTpsl", "orders": [ { "tpslType": "TAKE_PROFIT", "stopPrice": "70000.0", "quantity": "0", "reduceOnly": true, ... }, { "tpslType": "STOP_LOSS", "stopPrice": "55000.0", "quantity": "0", "reduceOnly": true, ... } ] } ``` #### entryTpsl Submit an entry order together with 1–2 bound TPSL children in a single atomic bundle (2–3 orders total). The entry leg has no `tpslType`; each child leg sets `tpslType`. When the entry is canceled, all child TPSL legs are automatically cascade-canceled. ```json { "grouping": "entryTpsl", "orders": [ { "orderType": "LIMIT", "price": "60000.0", "quantity": "0.1", ... }, { "tpslType": "TAKE_PROFIT", "stopPrice": "70000.0", "quantity": "0.1", "reduceOnly": true, ... }, { "tpslType": "STOP_LOSS", "stopPrice": "55000.0", "quantity": "0.1", "reduceOnly": true, ... } ] } ``` # Place order Source: https://docs.arcus.xyz/api-reference/exchange/place-order /api-reference/openapi.yml post /v1/placeOrder Place a single order. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Place a single order. Requires `address` query parameter matching the master Ethereum address for `X-API-Key` (same as `POST /createApiKey` response). ### Response behavior **Asynchronous.** The endpoint returns either `202 Accepted` (the common case — the signed request was forwarded to the matching engine and the body does not carry terminal state) or `200 OK` (the gateway already had definitive state for the order by the time it responded; `status` reflects that state). In both cases the body echoes the accepted `orderId` / `clientId` so subscribers can correlate WebSocket events with the request. To observe the full order lifecycle (`OPEN`, `FILLED`, `CANCELED`, `REJECTED`, fills, rejection reasons, etc.) clients **must** subscribe to the `orders` WebSocket channel (and `userFills` for trade-level events). # Schedule cancel-all (dead man's switch) Source: https://docs.arcus.xyz/api-reference/exchange/schedule-cancel-all-dead-mans-switch /api-reference/openapi.yml post /v1/scheduleCancel Arm, refresh, or disarm a per-subaccount dead man's switch. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Arm, refresh, or disarm a per-subaccount dead man's switch. If the deadline passes without a refresh, the gateway fires `cancelAllOrders` for the subaccount (account-wide). Keep the switch armed by posting a **new** `time` before the current deadline: each refresh is a full re-arm with an updated absolute epoch **microseconds** timestamp (typically `now + 60s`). Reusing a past deadline, or letting `time` elapse, does not extend the previous arm. Pass `time` to arm or refresh. Omit `time` (or send null) to disarm. Minimum lead is 5 seconds; maximum lead is 5 minutes. Successful auto-fires are capped at 10 per UTC day per subaccount (429 when exceeded). # Set leverage for a market Source: https://docs.arcus.xyz/api-reference/exchange/set-leverage-for-a-market /api-reference/openapi.yml post /v1/setLeverage Set per-(account, market) leverage. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Set per-(account, market) leverage. Requires `address` query parameter or JSON `address` field (must match `X-API-Key`'s master if both are present). `leverage` is an integer in `[1, market max]`. ### Response behavior **Asynchronous.** The endpoint returns `200 OK` (engine confirmed the change), `422 Unprocessable Entity` (engine rejected — see `rejectReason`), or `202 Accepted` (request forwarded but the confirmation didn't arrive within the timeout; the engine may still apply the change — observe the next `accounts` / `positions` WebSocket frame to confirm). Lowering leverage on an account with open positions can fail with `UNDERCOLLATERALIZED` if the higher initial margin requirement would push equity below required margin; the engine rolls back atomically on this path. Maintenance margin and liquidation price are unaffected by leverage changes — they depend on the market's `maintenanceMarginFraction`, not the per-account leverage cap. # Submit internal transfer Source: https://docs.arcus.xyz/api-reference/exchange/submit-internal-transfer /api-reference/openapi.yml post /v1/transfer Move collateral between two account indexes of the **same wallet**. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Move collateral between two account indexes of the **same wallet**. The request is self-authenticated by a secp256k1 **EIP-712** typed-data signature in the body — it does **not** require the `X-API-Key` / `X-Signature` headers. In v0 transfers are same-wallet only: both `fromAccountIndex` and `toAccountIndex` belong to `ethereumAddress`, so there is no cross-wallet recipient field. Funds never leave custody, so nothing settles on-chain. ### Response behavior **Asynchronous.** A `202 Accepted` means the transfer passed validation and was queued to the matching engine with `status: PENDING`. The definitive outcome (applied, or rejected for insufficient free collateral) is delivered asynchronously — poll `GET /v1/accountTransferUpdates` or subscribe to the account transfer update WebSocket channel and correlate on `transferId`. ## Signing the request Transfers use **EIP-712 typed data** (`eth_signTypedData_v4`). Sign this typed data with the wallet that owns `ethereumAddress`, then split the 65-byte result into `{r, s, v}` for the `signature` field. The gateway recovers the signer and rejects mismatches with HTTP 401. **Domain** ``` { "name": "Arcus Transfer", "version": "1", "chainId": , "verifyingContract": "" } ``` **Types** ``` Transfer(address ethereumAddress,uint8 fromAccountIndex,uint8 toAccountIndex,uint256 amount,string nonce) ``` **Message** ``` { "ethereumAddress": "", "fromAccountIndex": , "toAccountIndex": , "amount": "", "nonce": "" } ``` `amount` is the integer collateral quote-quantum amount (same value as the `amount` request field). **Per-environment domain parameters.** `chainId` and `verifyingContract` are identical to the withdraw domain — see the per-environment table under `POST /v1/withdraw`. Only the domain `name` differs (`"Arcus Transfer"` vs `"Arcus Withdraw"`). **Complete `eth_signTypedData_v4` call (ethers v6 / viem)** ```js // ethers v6 — chainId / verifyingContract per the /v1/withdraw table const domain = { name: "Arcus Transfer", version: "1", chainId, // e.g. 4663 on production (see /v1/withdraw) verifyingContract, // BridgeVault for that chain (see /v1/withdraw) }; const types = { Transfer: [ { name: "ethereumAddress", type: "address" }, { name: "fromAccountIndex", type: "uint8" }, { name: "toAccountIndex", type: "uint8" }, { name: "amount", type: "uint256" }, { name: "nonce", type: "string" } ] }; const message = { ethereumAddress: "0xYourAddress", fromAccountIndex: 0, toAccountIndex: 1, amount: BigInt("100000000000"), // quote quantums nonce: "1730000000000000000" }; const sig = await signer.signTypedData(domain, types, message); const r = sig.slice(0, 66); const s = "0x" + sig.slice(66, 130); const v = "0x" + sig.slice(130, 132); ``` ```ts // viem const sig = await walletClient.signTypedData({ domain, types, primaryType: "Transfer", message }); ``` The `v` component must be `0x1b` (27) or `0x1c` (28); wallets that return `0` / `1` should have 27 added before sending. # Submit withdrawal Source: https://docs.arcus.xyz/api-reference/exchange/submit-withdrawal /api-reference/openapi.yml post /v1/withdraw Submit a withdrawal of collateral to the chain. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Submit a withdrawal of collateral to the chain. `amount` is in quote quantums. Two mutually exclusive authentication modes are supported, selected by the presence of the `X-API-Key` header: * **Wallet-signed (default):** the request is self-authenticated by a secp256k1 **EIP-712** typed-data signature in the body's `signature` field — no `X-API-Key` / `X-Signature` headers. This is the original v0 mode, unchanged. * **API-key-signed:** the request carries the `X-API-Key` / `X-Timestamp` / `X-Signature` header triple with an Ed25519 signature over the **ordersign `WithdrawV1` typed canonical payload** (see below); the body `signature` field is ignored. The key must be bound to exactly the requested `(ethereumAddress, accountIndex)` and must carry the opt-in `withdraw` permission (grantable only through the operator's provisioning flow — keys from the public `POST /createApiKey` are always trade-only and are rejected with HTTP 403). This mode exists for contract-owned accounts (e.g. vaults), which cannot produce wallet signatures. In v0 only withdraw-to-self is supported on **both** modes: the recipient is always the wallet that owns the account (`ethereumAddress`), so there is no `to` field — an API key can only move funds to the account owner's own address. ### Response behavior **Asynchronous.** A `202 Accepted` means the withdrawal passed validation and was queued to the matching engine with `status: PENDING`. The definitive outcome (applied, or rejected for insufficient free collateral) is delivered asynchronously — poll `GET /v1/accountTransferUpdates` or subscribe to the account transfer update WebSocket channel and correlate on `withdrawalId`. ## Signing the request Withdrawals use **EIP-712 typed data** (`eth_signTypedData_v4`), so wallets render each field and institutional / MPC signers can whitelist on the domain. Sign this typed data with the wallet that owns `ethereumAddress`, then split the 65-byte result into `{r, s, v}` for the `signature` field. The gateway recovers the signer and rejects mismatches with HTTP 401. **Domain** ``` { "name": "Arcus Withdraw", "version": "1", "chainId": , "verifyingContract": "" } ``` **Types** ``` Withdraw(address ethereumAddress,uint8 accountIndex,uint256 amount,string nonce) ``` **Message** ``` { "ethereumAddress": "", "accountIndex": , "amount": "", "nonce": "" } ``` `amount` is the integer collateral quote-quantum amount (same value as the `amount` request field). **Per-environment domain parameters** | Environment | `chainId` | `verifyingContract` (`BridgeVault`) | | - | - | - | | testnet | `46630` | `0x9a6d3499149fea853efe775a3577701539054eaf` | | production | `4663` | `0x14b107cf534239c59571b066cb6497a321da897c` | **Complete `eth_signTypedData_v4` call (ethers v6 / viem)** ```js // ethers v6 const domain = { name: "Arcus Withdraw", version: "1", chainId: 4663, // production verifyingContract: "0x14b107cf534239c59571b066cb6497a321da897c" }; const types = { Withdraw: [ { name: "ethereumAddress", type: "address" }, { name: "accountIndex", type: "uint8" }, { name: "amount", type: "uint256" }, { name: "nonce", type: "string" } ] }; const message = { ethereumAddress: "0xYourAddress", accountIndex: 0, amount: BigInt("5000000000000"), // quote quantums nonce: "b1c2d3e4-5f60-7182-93a4-b5c6d7e8f901" }; const sig = await signer.signTypedData(domain, types, message); // Split into r / s / v for the request body: const r = sig.slice(0, 66); const s = "0x" + sig.slice(66, 130); const v = "0x" + sig.slice(130, 132); ``` ```ts // viem const sig = await walletClient.signTypedData({ domain, types, primaryType: "Withdraw", message }); ``` The `v` component must be `0x1b` (27) or `0x1c` (28); wallets that return `0` / `1` should have 27 added before sending. ## Signing the request (API-key mode) The `X-Signature` header carries a lowercase-hex Ed25519 signature over the **ordersign `WithdrawV1` typed canonical payload** — the same compact key-sorted-JSON convention as `placeOrder`. The payload is built from parsed request values (never raw body bytes): ``` {"ad":"","ai":,"ct":,"n":"","op":5,"q":,"v":1} ``` Keys are strictly alphabetical (`ad`, `ai`, `ct`, `n`, `op`, `q`, `v`) with no whitespace. `ct` must equal the `X-Timestamp` header value (nanosecond epoch), binding the signature to the drift window; `op` is fixed at `5` (withdraw — an order signature can never authorize a withdrawal); `q` is the integer quote-quantum amount (same value as the `amount` field, unquoted); `n` is the body `nonce` signed verbatim. **Example (payload → signature):** ``` payload = {"ad":"0x1111111111111111111111111111111111111111","ai":0,"ct":1718644999000000000,"n":"1718644999000","op":5,"q":2000000000000,"v":1} X-Signature = hex(ed25519_sign(privateKey, payload)) ``` Replay protection is two-layered on this mode: the `X-Timestamp` drift window bounds how long a captured signature stays valid, and the `nonce` is single-use per `(ethereumAddress, accountIndex)` exactly as on the wallet-signed mode (409 on reuse). # Introduction Source: https://docs.arcus.xyz/api-reference/introduction REST and WebSocket APIs for Arcus Arcus exposes a REST API for account management, perpetuals trading, and historical data, plus a WebSocket API for real-time market data and order routing. These APIs cover Arcus **perpetuals**. Both are non-custodial: you sign with an Ed25519 API key registered against your Ethereum address, and the server never sees a private key. **Fund your account before trading.** A newly-registered API key starts with **no balance** — `GET /v1/account` returns `404` until you make a deposit. Use the **Testnet Deposit** button in the testnet web app for a quick \~\$1,000, or deposit on-chain via the bridge. See [Your testnet account](/api-reference/authentication#your-testnet-account). ## Base URLs | Environment | REST | WebSocket | | - | - | - | | Mainnet | `https://api.arcus.xyz` | `wss://api.arcus.xyz/v1/ws` | | Testnet | `https://api.testnet.arcus.xyz` | `wss://api.testnet.arcus.xyz/v1/ws` | Both environments share identical paths, payloads, and signing — only the host differs, selectable from the server dropdown on each endpoint page. The code samples and API playground throughout these docs default to **mainnet**. New API features ship to testnet first, so the two environments can differ for a while: the [changelog](/changelog) tracks what is live in production, and the [testnet changelog](/changelog/testnet) tracks spec changes as they land on testnet ahead of it. ## Where to next Generate an Ed25519 key, register it, and sign requests. Connect, send subscribe / post / get envelopes, and place orders. Per-channel reference: market data, order book, account, candles. Auto-generated from the OpenAPI spec — see the **Endpoints** group in the sidebar. Per-IP request weights, per-subaccount trading pools, and WebSocket caps. # Bbo Source: https://docs.arcus.xyz/api-reference/market-data/bbo > **Endpoint:** `wss://api.arcus.xyz/v1/ws` — one socket multiplexes every channel and RPC request. Click **Connect**, then send the example subscribe message below to start receiving data. Best bid and offer (top-of-book) for a single market. Lower bandwidth than `l2Orderbook` when only the inside quote is needed. # Candles Source: https://docs.arcus.xyz/api-reference/market-data/candles > **Endpoint:** `wss://api.arcus.xyz/v1/ws` — one socket multiplexes every channel and RPC request. Click **Connect**, then send the example subscribe message below to start receiving data. OHLCV candlestick stream. Subscribe `id` is `"MARKET/TIMEFRAME"`, e.g. `"BTC-USD/1m"`. On subscribe: snapshot of the most recent candles (up to 200). Then: real-time updates as each bar changes. Open-candle updates are coalesced and sent at most about every 250 ms per market/timeframe. `isFinal: true` updates are sent immediately when the interval closes and the bar will not be updated again. Available timeframes: `1m`, `3m`, `5m`, `15m`, `30m`, `1h`, `2h`, `4h`, `8h`, `12h`, `1d`, `3d`, `1w`. # Exchange attribute updates Source: https://docs.arcus.xyz/api-reference/market-data/exchangeattributeupdates > **Endpoint:** `wss://api.arcus.xyz/v1/ws` — one socket multiplexes every channel and RPC request. Click **Connect**, then send the example subscribe message below to start receiving data. Exchange-wide attribute state, discriminated by entry `type`: `"feeTierConfig"` carries the full fee tier table whenever the fee tier table is updated. `"marketAttributes"` (DEPRECATED — superseded by the dedicated `marketAttributes` channel, whose entries carry the market's full attribute state; this variant keeps streaming for existing consumers and will be removed after a deprecation window) carries per-market Regular Trading Hours and off-hours trading-bound events: RTH boundary crossings (`isOutsideRth`, with `boundEvent`: `exitRth` / `enterRth`), VWAP settlement seals (`sealedVwapSettlementPrice`), and off-hours expansion-zone events (`boundEvent`: `expansionZoneEntered` when a side's impact-price EWMA crosses within 90% of its trading boundary, starting the 1-hour expansion clock; `expansionZoneExited` when the EWMA relaxes out of the zone and the clock resets; `tradingBoundExpanded` when the clock completes and that side's band widens one level on the 0.5×→1×→2×→4× ladder). **Global channel — no `id` required.** All subscribers receive the same stream. On subscribe: snapshot of the current fee tier table (one `feeTierConfig` entry, `isSnapshot: true`). Streaming: fee tier table replacements and marketAttributes edge events (`isSnapshot: false`). The equivalent REST reads are `GET /v1/feetiers` and `GET /v1/markets` (`isOutsideRth`, `isUpperInExpansionZone` / `isLowerInExpansionZone`, `upperTradingBound` / `lowerTradingBound`). # L2 orderbook Source: https://docs.arcus.xyz/api-reference/market-data/l2orderbook > **Endpoint:** `wss://api.arcus.xyz/v1/ws` — one socket multiplexes every channel and RPC request. Click **Connect**, then send the example subscribe message below to start receiving data. Periodic full L2 orderbook snapshots for a single market. On subscribe, the server sends the current book up to `nLevels` price levels per side, then continues to broadcast a fresh full snapshot every ~200ms. No incremental updates — each message replaces the prior book state. Use `l2OrderbookUpdates` if you need to maintain your own local book. A connection may subscribe more than once to the same market when each subscribe uses a distinct (`sigFigs`, `roundStep`) pair. `subscribed`, `channel_data`, and `unsubscribed` frames echo those fields (omitted for full precision) so the client can demux views. Unsubscribe must include the same pair to drop one view without affecting the others. # L2 orderbook updates Source: https://docs.arcus.xyz/api-reference/market-data/l2orderbookupdates > **Endpoint:** `wss://api.arcus.xyz/v1/ws` — one socket multiplexes every channel and RPC request. Click **Connect**, then send the example subscribe message below to start receiving data. Full L2 orderbook snapshot followed by event-driven incremental deltas for a single market. On subscribe: full snapshot up to `nLevels` per side, then delta updates as orders are placed, modified, or cancelled. Each delta carries a monotonically increasing per-market `lastSequenceId`. Reconciliation (splice rule): the snapshot is a periodic generation that lags slightly behind the live delta head, so the first delta after the snapshot may be a few sequences ahead of the snapshot's `lastSequenceId` — this small boundary gap is expected and self-heals. Seed from the snapshot, then apply every delta whose `lastSequenceId` exceeds the snapshot's; do NOT require the first delta to equal `snapshotSeq + 1`, and do NOT drop or re-subscribe on the initial boundary gap. Once you are applying deltas, sequences are contiguous; a gap that appears mid-stream indicates a genuine problem — resubscribe then. A single delta frame may list the same price more than once; apply entries in array order, last-write-wins per price. # Market attributes Source: https://docs.arcus.xyz/api-reference/market-data/marketattributes > **Endpoint:** `wss://api.arcus.xyz/v1/ws` — one socket multiplexes every channel and RPC request. Click **Connect**, then send the example subscribe message below to start receiving data. Per-market Regular Trading Hours / off-hours attribute state for every online market. 24/7 markets (crypto) are included with `isOutsideRth: false` — they never leave regular trading hours — and every other off-hours field `null`; they never produce streaming updates. Supersedes the `marketAttributes` entry variant on `exchangeAttributeUpdates`. Every entry — snapshot and streaming alike — is the market's COMPLETE current attribute block: `marketId`, `marketDisplayName`, `offHoursInitialMarginFraction`, `isOutsideRth`, `currentSettlementPrice`, both trading-band boundaries and next-level boundaries, and per-side expansion-zone state. Fields that do not currently apply are `null`, never omitted. Field semantics match `GET /v1/markets`. **Global channel — no `id` required.** All subscribers receive the same stream. On subscribe: one full block per online market (`isSnapshot: true`) — a complete bootstrap, no REST read needed. Streaming: a single-entry envelope with the affected market's full post-event block on every RTH boundary crossing, VWAP seal, and off-hours bound event (`isSnapshot: false`). Because each update carries the full block — both sides' bounds, not just the side an event fired on — clients replace their local entry for `marketId` wholesale; `boundEvent` annotates what fired — always present on delta entries (`exitRth` / `enterRth` on RTH boundary crossings, a bound-event name otherwise); `boundSide` is also always present on delta entries (null on RTH-transition / non-bound-event deltas). # Markets Source: https://docs.arcus.xyz/api-reference/market-data/markets > **Endpoint:** `wss://api.arcus.xyz/v1/ws` — one socket multiplexes every channel and RPC request. Click **Connect**, then send the example subscribe message below to start receiving data. Global channel — no `id` required. Streams full per-market metadata for all listed markets. Snapshot-only channel: every message is a full map of all markets keyed by `marketId` (same shape on subscribe and on every periodic re-emission, no partial diffs). Clients replace their entire markets view on each message. **Cadence:** ~5s periodic snapshot, plus an immediate snapshot when a new funding rate is applied (~hourly) so the just-applied `fundingRate` propagates without waiting for the next periodic snapshot. **Field coverage today:** `marketDisplayName`, `marketId`, `baseAsset`, `quoteAsset`, `status` (currently always `ONLINE`), static config (tick/step/min/max sizes, margin fractions, fees), `oraclePrice`, `fundingRate`, `nextFundingRate`, and `nextFundingAt` are populated. `priceChange24h` is the fractional change in the **mark price** over the trailing 24h (not the oracle price). `volume24h`, `high24h`, `low24h`, and `trades24h` are rolling 24h aggregates, refreshed periodically. High/low use only closed candles with at least one trade (gap-fill buckets are excluded). `openInterest` is the total size of all open long positions per market (equals total short), in base asset units, refreshed from a periodic snapshot (default ~60s) so it may lag real time. `openInterestCapNotional` (`null` when no cap is active) is the operator-set maximum open-interest USD notional, refreshed on the same cadence. # Oracle prices Source: https://docs.arcus.xyz/api-reference/market-data/oracleprices > **Endpoint:** `wss://api.arcus.xyz/v1/ws` — one socket multiplexes every channel and RPC request. Click **Connect**, then send the example subscribe message below to start receiving data. Per-market oracle price updates sourced from the on-chain slinky aggregator. Pushes a new price on each oracle update. # Predicted funding Source: https://docs.arcus.xyz/api-reference/market-data/predictedfunding > **Endpoint:** `wss://api.arcus.xyz/v1/ws` — one socket multiplexes every channel and RPC request. Click **Connect**, then send the example subscribe message below to start receiving data. Predicted next-hour funding rate for a single market. Subscribe `id` is the market display name (e.g. `BTC-USD`). On subscribe the initial response carries the latest cached prediction for the market; `contents` is empty when no prediction has been published yet. Streaming updates deliver a new `{market, rate1h}` payload each time the funder recomputes the prediction. Unlike most channels, payloads are not wrapped in an `isSnapshot` envelope. # Trades Source: https://docs.arcus.xyz/api-reference/market-data/trades > **Endpoint:** `wss://api.arcus.xyz/v1/ws` — one socket multiplexes every channel and RPC request. Click **Connect**, then send the example subscribe message below to start receiving data. Public trade stream for a single market. No snapshot on subscribe — only live updates from the moment of subscription. Each message is an array of fills from a single taker order match. All fills in one array share the same `takerOrderId`, `takerAccountId`, `timestamp`, and `sequenceNumber`. # Create API key Source: https://docs.arcus.xyz/api-reference/onboarding/create-api-key /api-reference/openapi.yml post /v1/createApiKey Create an API key for the ethereum address. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Create an API key for the ethereum address. No address or account parameter is required; all keys are associated with the master account. ## Signing the request The request is authenticated by a secp256k1 **EIP-712** typed-data signature (`eth_signTypedData_v4`) produced by the wallet that owns `address`, so wallets render each field and institutional / MPC signers can whitelist on the domain. Sign the typed data below, then split the 65-byte result into `{r, s, v}` for the `signature` field. The gateway recovers the signer and rejects the request with HTTP 401 if the recovered address does not equal `address`. **Domain** API keys are pure off-chain authentication, so the domain has **no `verifyingContract`** (the field is optional per EIP-712): ``` { "name": "Arcus API Key", "version": "1", "chainId": } ``` **Types** To scope the key to a subaccount, sign the accountIndex-bearing type. Use `0`–`9` for a single subaccount, or `255` to scope the key to **all** of the address's subaccounts (the default for web/mobile trading keys): ``` CreateApiKey(string apiWalletName,string apiWalletPublicKey,uint256 validUntil,uint8 accountIndex) ``` For `accountIndex` 0 you may instead sign the original index-less type (backward compatibility, no deadline); a **non-zero** `accountIndex` (including the `255` all-subaccounts sentinel) MUST use the type above: ``` CreateApiKey(string apiWalletName,string apiWalletPublicKey,uint256 validUntil) ``` **Replay protection (recommended)** When including a `nonce` in the request body, sign the combined type that binds both replay protection and subaccount scope: ``` CreateApiKey(string apiWalletName,string apiWalletPublicKey,uint256 validUntil,string nonce,uint8 accountIndex) ``` The `nonce` and `accountIndex` in the message must match the request body. For `accountIndex` 0 the server also accepts the nonce-only type without `accountIndex` in the signed payload (backward compatibility). **Message** ``` { "apiWalletName": "", "apiWalletPublicKey": "", "validUntil": , "accountIndex": } ``` `apiWalletPublicKey` is the request's `publicKey` field (64 hex chars, no `0x`); `validUntil` is epoch ms, the same value as the request field. Always send `validUntil` explicitly and sign that value — if the body omits it, the server verifies against its own default (now + 14 days), which will not match what you signed. `accountIndex` is the key's subaccount scope — `0`–`9` for a single subaccount, or `255` for all subaccounts — and must match the request field; omit it from the message only when signing the index-less type for index 0. The legacy EIP-191 `personal_sign` fallback is index-0 only. **Per-environment domain parameters** | Environment | `chainId` | | - | - | | testnet | `46630` | | production | `4663` | **Complete `eth_signTypedData_v4` call (ethers v6 / viem)** ```js // ethers v6 const domain = { name: "Arcus API Key", version: "1", chainId: 4663 // production }; const types = { CreateApiKey: [ { name: "apiWalletName", type: "string" }, { name: "apiWalletPublicKey", type: "string" }, { name: "validUntil", type: "uint256" } ] }; const message = { apiWalletName: "Arcus", apiWalletPublicKey: pubKeyHex, // 64 hex chars, no 0x validUntil: validUntil // epoch ms }; const sig = await signer.signTypedData(domain, types, message); // Split into r / s / v for the request body: const r = sig.slice(0, 66); const s = "0x" + sig.slice(66, 130); const v = "0x" + sig.slice(130, 132); ``` ```ts // viem const sig = await walletClient.signTypedData({ domain, types, primaryType: "CreateApiKey", message }); ``` ### Legacy EIP-191 signatures (deprecated) This endpoint previously accepted an EIP-191 `personal_sign` signature over the canonical JSON message ``` {"apiWalletName":"","apiWalletPublicKey":"","validUntil":} ``` (keys in this exact order, no whitespace, no trailing newline). During the migration window the server accepts **either** scheme: it verifies the EIP-712 signature first and falls back to legacy EIP-191. EIP-191 is deprecated and will be **rejected once the migration window closes** — migrate existing integrations to EIP-712 typed data; new integrations must use EIP-712 only. ## Public-key ownership A public key is a globally unique credential: auth resolves the owning account from `api_keys_by_key`. `createApiKey` rejects registering a public key already owned by a **different** account with HTTP 409 — including keys that have expired but were never revoked. Re-registering a key the caller already owns (renew / rewrite `validUntil`) is allowed. # Get API keys Source: https://docs.arcus.xyz/api-reference/onboarding/get-api-keys /api-reference/openapi.yml get /v1/apiKeys Returns all API keys associated with the given Ethereum address. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns all API keys associated with the given Ethereum address. No authentication required — API keys are Ed25519 public keys and are not secret. Use this to bootstrap: check whether a locally stored key is still active before attempting authenticated requests, and trigger key creation if not. With no `accountIndex`, the listing spans every subaccount of the address (plus any all-subaccounts keys). A single-subaccount key entry is tagged with the `accountIndex` it belongs to; an all-subaccounts key entry omits `accountIndex` and sets `allSubaccounts: true` — use that flag as the scope discriminator. Pass `accountIndex` to scope the listing to one single-subaccount partition. Because the response is all-or-nothing across subaccounts, a `200` is safe to treat as the complete set for the requested window — an unreadable subaccount surfaces as a `500` rather than a short list that would look like a revoked key. Results are ordered newest-first by `createdAt` and capped at `limit` (default and max 1000). Narrow the set with `limit` / `from` / `to`; `createdAt` is epoch milliseconds, the same unit the bounds take, so a value read from one page is a valid bound for the next. # Revoke API key Source: https://docs.arcus.xyz/api-reference/onboarding/revoke-api-key /api-reference/openapi.yml post /v1/revokeApiKey Revoke (remove) an existing API key. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Revoke (remove) an existing API key. The request is authenticated by a secp256k1 **EIP-712** typed-data signature produced with the wallet that owns `address` (the same wallet that originally signed the matching `createApiKey`). The gateway recovers the signer and rejects mismatches with HTTP 401. This endpoint does **not** require an `X-API-Key` header so the owner can still revoke a key that has been lost or compromised. Revocation is scoped to `(address, publicKey)`: only the account that owns the key may revoke it. A revoke for a key not registered to the caller's account returns HTTP 404 and is a no-op — no state is changed. ## Signing the request Identical to `createApiKey` — same `Arcus API Key` domain, same per-environment `chainId` values, no `verifyingContract` — except the primary type is `RevokeApiKey` and the message **does not include `validUntil`** (revoke applies regardless of the key's remaining lifetime). **Domain** ``` { "name": "Arcus API Key", "version": "1", "chainId": } ``` **Types** Same accountIndex rule as `createApiKey`: sign the accountIndex-bearing type, and for a non-zero index — including the `255` all-subaccounts sentinel — it is mandatory. The value must match the scope the key was created against. Index 0 may still use the original index-less type. ``` RevokeApiKey(string apiWalletName,string apiWalletPublicKey,uint8 accountIndex) ``` ``` RevokeApiKey(string apiWalletName,string apiWalletPublicKey) ``` **Replay protection (recommended)** When including a `nonce` in the request body, sign the combined type that binds both replay protection and subaccount scope: ``` RevokeApiKey(string apiWalletName,string apiWalletPublicKey,string nonce,uint8 accountIndex) ``` The `nonce` and `accountIndex` in the message must match the request body. For `accountIndex` 0 the server also accepts the nonce-only type without `accountIndex` in the signed payload (backward compatibility). **Message** ``` { "apiWalletName": "", "apiWalletPublicKey": "", "accountIndex": } ``` **Complete `eth_signTypedData_v4` call (ethers v6 / viem)** ```js // ethers v6 const domain = { name: "Arcus API Key", version: "1", chainId: 4663 // production }; const types = { RevokeApiKey: [ { name: "apiWalletName", type: "string" }, { name: "apiWalletPublicKey", type: "string" } ] }; const message = { apiWalletName: "Arcus", apiWalletPublicKey: pubKeyHex // 64 hex chars, no 0x }; const sig = await signer.signTypedData(domain, types, message); // Split into r / s / v for the request body: const r = sig.slice(0, 66); const s = "0x" + sig.slice(66, 130); const v = "0x" + sig.slice(130, 132); ``` ```ts // viem const sig = await walletClient.signTypedData({ domain, types, primaryType: "RevokeApiKey", message }); ``` Revokes signed by any wallet other than the original creator are rejected. ### Legacy EIP-191 signatures (deprecated) This endpoint previously accepted an EIP-191 `personal_sign` signature over the canonical JSON message ``` {"apiWalletName":"","apiWalletPublicKey":""} ``` (keys in this exact order, no whitespace, no trailing newline). During the migration window the server accepts **either** scheme: it verifies the EIP-712 signature first and falls back to legacy EIP-191. EIP-191 is deprecated and will be **rejected once the migration window closes** — migrate existing integrations to EIP-712 typed data; new integrations must use EIP-712 only. # Get a single trade by ID Source: https://docs.arcus.xyz/api-reference/public/get-a-single-trade-by-id /api-reference/openapi.yml get /v1/trade/{tradeId} Returns one trade row. The `market` query parameter is required: `trades_by_market` is partitioned by `market_id` first, so without it the lookup would scan every market's parts. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns one trade row. The `market` query parameter is required: `trades_by_market` is partitioned by `market_id` first, so without it the lookup would scan every market's parts. # Get account Source: https://docs.arcus.xyz/api-reference/public/get-account /api-reference/openapi.yml get /v1/account Returns account information for the requested address, including balances, collateral, and open positions. No authentication header is required. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns account information for the requested address, including balances, collateral, and open positions. No authentication header is required. # Get account stats Source: https://docs.arcus.xyz/api-reference/public/get-account-stats /api-reference/openapi.yml get /v1/account/stats Returns trading statistics for any Ethereum address, in selectable sections so clients fetch only what they need: - `feeTier` — the current trading fee tier (level + BPS). **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns trading statistics for any Ethereum address, in selectable sections so clients fetch only what they need: * `feeTier` — the current trading fee tier (level + BPS). Cheap; the right shape for frequent polling of fee rates. * `volumes` — notional fill volume and fees paid over the fee-tier rolling window (30 days) and all-time. Backed by analytics storage; fetch on demand (e.g. a stats page). Select sections with the `include` query parameter (e.g. `include=feeTier`); omitting it returns both. Fixed 24h / 7d / 30d windows are additionally available via the `windows` query parameter (e.g. `windows=24h,7d,30d`) and are returned under `windowedStats`. This endpoint always returns `200` — a brand-new address with no activity yields `rollingVolume: 0` and `tradingFeeTier` at level 0 (Base). No authentication is required. # Get account transfer updates Source: https://docs.arcus.xyz/api-reference/public/get-account-transfer-updates /api-reference/openapi.yml get /v1/accountTransferUpdates Returns deposits, withdrawals, and transfers for the requesting account, newest-first. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns deposits, withdrawals, and transfers for the requesting account, newest-first. Each entry is an `AccountTransferUpdate` object. The matching engine rejects a withdrawal or transfer before it is persisted when either of the following conditions holds: * **Non-positive amount** — `amount` must be strictly greater than zero. * **Insufficient free collateral** — `amount` exceeds `accountEquity − requiredMargin`. The engine does not perform partial pulls; the entire request is rejected and account state is left unchanged. No authentication header is required. # Get all mid prices Source: https://docs.arcus.xyz/api-reference/public/get-all-mid-prices /api-reference/openapi.yml get /v1/mids Returns precomputed mids for every market. Markets with no bid, no ask, or no top-of-book return an empty string for that key. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns precomputed mids for every market. Markets with no bid, no ask, or no top-of-book return an empty string for that key. # Get Best Bid Offer (BBO) Source: https://docs.arcus.xyz/api-reference/public/get-best-bid-offer-bbo /api-reference/openapi.yml get /v1/bbo/{market} Returns the best bid and best ask for a market, derived from the orderbook snapshot. Returns null for bestBid/bestAsk when no data is available. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns the best bid and best ask for a market, derived from the orderbook snapshot. Returns null for bestBid/bestAsk when no data is available. # Get current rate-limit usage Source: https://docs.arcus.xyz/api-reference/public/get-current-rate-limit-usage /api-reference/openapi.yml get /v1/rateLimit Returns the rate-limit state of both per-subaccount pools (order and cancel) so callers can self-pace and inspect which throttle is constraining them. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns the rate-limit state of both per-subaccount pools (order and cancel) so callers can self-pace and inspect which throttle is constraining them. The pools have independent consumed counters and different BaseSeeds (20K order, 40K cancel) — an HFT cancel-heavy pattern can saturate one while the other sits idle. Each pool reports `{used, cap, nextAvailableMs}`: * `cap` = BaseSeed + lifetime\_filled\_usd / 0.10 (10 pool units per USD of lifetime realized notional). Lifetime volume is shared across both pools, so the two caps grow in lockstep. * `nextAvailableMs` is 0 while `used < cap`. Past the cap, it carries the millisecond-until-next-drip-token (1 action per 10 seconds in production). No authentication header is required. # Get current server time Source: https://docs.arcus.xyz/api-reference/public/get-current-server-time /api-reference/openapi.yml get /v1/time Returns the current server wall-clock time as Unix nanoseconds. Rate-limited to 1 token per call. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns the current server wall-clock time as Unix nanoseconds. Rate-limited to 1 token per call. # Get fee tier table Source: https://docs.arcus.xyz/api-reference/public/get-fee-tier-table /api-reference/openapi.yml get /v1/feetiers Returns the complete exchange-wide fee tier table, sorted ascending by level. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns the complete exchange-wide fee tier table, sorted ascending by level. The response reflects the in-memory cache on the serving gateway instance, which is refreshed sub-second whenever an admin fee tier update is confirmed by the matching engine. No authentication required. # Get fill by ID Source: https://docs.arcus.xyz/api-reference/public/get-fill-by-id /api-reference/openapi.yml get /v1/fill/{tradeId} Returns the single fill belonging to the requested account for the given trade ID. Requires the `address` query parameter (optional `accountIndex` selects a subaccount, default 0): fill data is partitioned by account, so the lookup needs the owning account. A trade can produce one fill per participant that share a `tradeId`; this returns the leg owned by the requested account. No authentication header is required. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns the single fill belonging to the requested account for the given trade ID. Requires the `address` query parameter (optional `accountIndex` selects a subaccount, default 0): fill data is partitioned by account, so the lookup needs the owning account. A trade can produce one fill per participant that share a `tradeId`; this returns the leg owned by the requested account. No authentication header is required. # Get fills Source: https://docs.arcus.xyz/api-reference/public/get-fills /api-reference/openapi.yml get /v1/fills Returns fill and trade history for the requested account address, newest-first. Optional `from`/`to` (epoch **microseconds**, the unit of the `createdAt` field in the response) bound the time window. Optional `accountIndex` selects a subaccount (default 0); optional `market` restricts results to a single market; optional `role` restricts results to a single liquidity side (maker or taker); optional `side` restricts results to a single trade direction (buy or sell). No authentication header is required. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns fill and trade history for the requested account address, newest-first. Optional `from`/`to` (epoch **microseconds**, the unit of the `createdAt` field in the response) bound the time window. Optional `accountIndex` selects a subaccount (default 0); optional `market` restricts results to a single market; optional `role` restricts results to a single liquidity side (maker or taker); optional `side` restricts results to a single trade direction (buy or sell). No authentication header is required. # Get funding payments Source: https://docs.arcus.xyz/api-reference/public/get-funding-payments /api-reference/openapi.yml get /v1/funding Returns funding payment history for the requested account address, newest-first, optionally restricted to a single market via `market`. Optional `from`/`to` (epoch **microseconds**, the unit of the `time` field in the response) bound the time window. When `to` is omitted it defaults to now; when `from` is omitted it defaults to 30 days before `to`. An explicit `from` is honored as-is, so a wider range (e.g. a full year for tax) is returned in full subject to data retention. No authentication header is required. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns funding payment history for the requested account address, newest-first, optionally restricted to a single market via `market`. Optional `from`/`to` (epoch **microseconds**, the unit of the `time` field in the response) bound the time window. When `to` is omitted it defaults to now; when `from` is omitted it defaults to 30 days before `to`. An explicit `from` is honored as-is, so a wider range (e.g. a full year for tax) is returned in full subject to data retention. No authentication header is required. # Get L2 orderbook snapshot Source: https://docs.arcus.xyz/api-reference/public/get-l2-orderbook-snapshot /api-reference/openapi.yml get /v1/l2OrderBook/{market} Returns the current L2 orderbook snapshot for a market, truncated to the requested number of levels per side. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns the current L2 orderbook snapshot for a market, truncated to the requested number of levels per side. Optionally aggregates price levels to fewer significant figures via `sigFigs` (and `roundStep`). When aggregating, bids round down and asks round up to the bucket boundary so the displayed spread is never tighter than the true book, and the sizes of all merged levels are summed. Aggregation is applied before `nLevels` truncation. # Get leverage and margin mode for an account across all markets Source: https://docs.arcus.xyz/api-reference/public/get-leverage-and-margin-mode-for-an-account-across-all-markets /api-reference/openapi.yml get /v1/leverages Returns the effective leverage and margin mode (CROSS/ISOLATED) for `(address, accountIndex)` across every market, sorted by ascending `marketId`. Each entry reflects the user's leverage override (set via `POST /v1/setLeverage`) when one exists, otherwise the market default, and the current margin mode (`isolated` / `marginMode`). **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns the effective leverage and margin mode (CROSS/ISOLATED) for `(address, accountIndex)` across every market, sorted by ascending `marketId`. Each entry reflects the user's leverage override (set via `POST /v1/setLeverage`) when one exists, otherwise the market default, and the current margin mode (`isolated` / `marginMode`). # Get live prices for all markets Source: https://docs.arcus.xyz/api-reference/public/get-live-prices-for-all-markets /api-reference/openapi.yml get /v1/prices Returns the latest oracle / mark price and per-market sequence number for every configured market, keyed by numeric `marketId` (as a string). **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns the latest oracle / mark price and per-market sequence number for every configured market, keyed by numeric `marketId` (as a string). Data is served entirely from in-memory caches on the gateway and refreshed in-process from the oracle feed, so this endpoint is safe to poll at high frequency. Notes: * `markPrice` currently mirrors `oraclePrice`. The field is kept separate so a future mark-price feed can be introduced without changing the response shape. * `sequencer` only advances from engine-sourced updates. Prices seeded via the HTTP ingestion path (which has no engine sequence yet) leave `sequencer` at its previous value (or `0` if none). # Get market funding rates Source: https://docs.arcus.xyz/api-reference/public/get-market-funding-rates /api-reference/openapi.yml get /v1/fundingRates Returns historical funding rates for a market, newest-first. Optional `from`/`to` (epoch **microseconds**, the unit of the `time` field in the response) bound the time window. No authentication header is required. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns historical funding rates for a market, newest-first. Optional `from`/`to` (epoch **microseconds**, the unit of the `time` field in the response) bound the time window. No authentication header is required. # Get markets Source: https://docs.arcus.xyz/api-reference/public/get-markets /api-reference/openapi.yml get /v1/markets Returns all markets with details, sorted by ascending `marketId`. Optionally filter to a single market. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns all markets with details, sorted by ascending `marketId`. Optionally filter to a single market. # Get OHLCV candles Source: https://docs.arcus.xyz/api-reference/public/get-ohlcv-candles /api-reference/openapi.yml get /v1/candles Returns OHLCV candlestick bars for a market and timeframe. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns OHLCV candlestick bars for a market and timeframe. Results include closed bars (gap-filled with O=H=L=C=prev\_close, vol=0 for empty buckets) plus the current open bar. Bars are returned oldest-first; the last entry is the in-progress bar with `isFinal: false`. Bar prices (`open`/`high`/`low`/`close`) are derived from the market's oracle price feed, not from executed trades. `volume`, `takerBuyVolume`, the notional volumes, and `tradeCount` remain trade-derived statistics for the bucket. Buckets in which the oracle price did not move are flat at the previous oracle close. # Get open orders Source: https://docs.arcus.xyz/api-reference/public/get-open-orders /api-reference/openapi.yml get /v1/openOrders Returns the open orders for the requested account address, newest-first by placement time (`createdAt`). **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns the open orders for the requested account address, newest-first by placement time (`createdAt`). Optional `from`/`to` (epoch microseconds) bound the window on `createdAt`. At most 1000 orders are returned per response, which is also the default. The matching engine allows up to 10,000 live clientIds per account (`TOO_MANY_CLIENT_IDS`), so an account can hold more open orders than fit in one page. To page through them, walk backwards through `createdAt` windows: 1. Request the first page with no `to`. You get the newest 1000 orders. 2. Take the **oldest** `createdAt` in the response and send it back verbatim as `to` on the next request. Bounds are microseconds, the same unit `createdAt` is reported in, so no conversion is needed. 3. Repeat until a response contains fewer than 1000 orders. **Deduplicate by `orderId` as you go.** `to` is inclusive and closes over the whole microsecond it names, so the last order(s) of one page reappear at the top of the next. That overlap is deliberate: orders placed in a single batch can share a `createdAt`, and a bound that excluded them would drop the ones you had not yet seen instead of repeating the ones you had. The window keys off `createdAt` rather than the last-update time so that paging is stable — an order's `createdAt` does not move when it partially fills, so each order stays in the same window as you walk. Optional `market` restricts results to a single market, and optional `status` separates orders resting on the book (`OPEN`) from TPSLs parked on the untriggered book (`UNTRIGGERED`). Both are applied before the row cap, so a filtered page still carries up to `limit` orders. No authentication header is required. # Get order history Source: https://docs.arcus.xyz/api-reference/public/get-order-history /api-reference/openapi.yml get /v1/orders Returns the order history (open, filled, canceled, rejected) for the requested account address, newest-first by event time. Optional `from`/`to` (epoch **microseconds**, the unit of the `updatedAt` field in the response) bound the time window for follow-on pagination. Optional `market`, `side`, and `status` narrow the results; `status` takes one or more values and matches each order's current status. No authentication header is required. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns the order history (open, filled, canceled, rejected) for the requested account address, newest-first by event time. Optional `from`/`to` (epoch **microseconds**, the unit of the `updatedAt` field in the response) bound the time window for follow-on pagination. Optional `market`, `side`, and `status` narrow the results; `status` takes one or more values and matches each order's current status. No authentication header is required. # Get order status Source: https://docs.arcus.xyz/api-reference/public/get-order-status /api-reference/openapi.yml get /v1/order/{orderId} Returns the status of a specific order by order ID. Requires `address` query parameter for the owning account because order data is partitioned by account, so the lookup needs the owning account. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns the status of a specific order by order ID. Requires `address` query parameter for the owning account because order data is partitioned by account, so the lookup needs the owning account. # Get portfolio history Source: https://docs.arcus.xyz/api-reference/public/get-portfolio-history /api-reference/openapi.yml get /v1/portfolio Returns bundled account-equity and PnL time series for the requested address across preset ranges (24h, 7d, 30d, all-time) and per product (total/perp). **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns bundled account-equity and PnL time series for the requested address across preset ranges (24h, 7d, 30d, all-time) and per product (total/perp). The response carries every range in one payload so the frontend can switch tabs without an extra round-trip. Each range exposes its bucket resolution ("1h" or "1d") and two parallel point series: `accountEquityHistory` (absolute equity) and `pnlHistory` (period profit and loss, rebased so each timeframe starts at 0). Data points are `[timestamp_us, "decimal_value"]` tuples. Values are stringified to preserve precision; clients should treat them as decimals, not floats. No authentication header is required. # Get positions Source: https://docs.arcus.xyz/api-reference/public/get-positions /api-reference/openapi.yml get /v1/positions Returns all open positions for the requested account address. No authentication header is required. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns all open positions for the requested account address. No authentication header is required. # Get recent public trades Source: https://docs.arcus.xyz/api-reference/public/get-recent-public-trades /api-reference/openapi.yml get /v1/trades Returns recent public trades for a market, newest-first. One row per maker/taker pairing — a market sweep that hit 3 makers produces 3 rows. Same shape as the `trades` WebSocket channel. No authentication header is required. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns recent public trades for a market, newest-first. One row per maker/taker pairing — a market sweep that hit 3 makers produces 3 rows. Same shape as the `trades` WebSocket channel. No authentication header is required. # Get referral commission rate schedule Source: https://docs.arcus.xyz/api-reference/public/get-referral-commission-rate-schedule /api-reference/openapi.yml get /v1/commissionrates Returns the referral commission rate schedule used by the referral service to compute affiliate payouts on each fill. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns the referral commission rate schedule used by the referral service to compute affiliate payouts on each fill. T1 (depth 1) varies by the referrer's tier and is listed per row; T2 (depth 2) and T3 (depth 3) are flat rates that apply regardless of tier. These rates are fixed constants with no admin mutation path today, so the response is stable until it is next changed. Effective T1 paid to a referrer on a given fill is `max(t1_rate_bps - referee_discount_bps_at_binding, 0)` where the discount is locked at the referee's registration time. No authentication required. # Get the all-traders leaderboard Source: https://docs.arcus.xyz/api-reference/public/get-the-all-traders-leaderboard /api-reference/openapi.yml get /v1/leaderboard Returns the top-N traders ranked by `sortBy` (`volume`, `pnl`, or `fees`; default `volume`) over the requested rolling window (`all`, `30d`, or `24h`; default `30d`). **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns the top-N traders ranked by `sortBy` (`volume`, `pnl`, or `fees`; default `volume`) over the requested rolling window (`all`, `30d`, or `24h`; default `30d`). Each row carries volume, fees paid, realized PnL, and its `rank` on the active `sortBy` column, all computed in a single query so the columns are always mutually consistent for a row. `pnl` is realized only (sum of per-fill `closed_pnl`); open-position (unrealized) MTM is NOT included. Served from the hourly rollup, which lags real-time by up to the current partial hour. Pass `address` to fetch just that one trader's row — its figures and its rank on the `sortBy` column, wherever it sits on the board (not only within the top-N). `limit` is ignored in that mode, and an address that did not trade in the window returns an empty `entries` list. Pagination: `limit` max is 100 and there is no cursor — fetch up to 100 rows in one call and paginate client-side. # Health check Source: https://docs.arcus.xyz/api-reference/public/health-check /api-reference/openapi.yml get /health **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. # Service info Source: https://docs.arcus.xyz/api-reference/public/service-info /api-reference/openapi.yml get / Returns service metadata and the list of available endpoints. Use `GET /health` for a liveness/readiness check. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns service metadata and the list of available endpoints. Use `GET /health` for a liveness/readiness check. # Rate limits Source: https://docs.arcus.xyz/api-reference/rate-limits How requests are weighted and throttled across REST, WebSocket, and the trading path Arcus throttles traffic in two independent layers. Both can reject the same request, and a request must pass both to succeed: * **Per-IP weight limits** — every REST request costs a *weight*. Each source IP gets a token budget that refills over time; expensive endpoints drain it faster. WebSocket messages do not draw from this budget; they have their own [per-IP limits](#websocket-limits). * **Per-subaccount trading limits** — order placement and cancellation draw from two pools keyed to your subaccount, independent of IP. These scale with your realized trading volume. Limits are enforced globally across all gateway nodes (state lives in a shared store), so spreading requests across connections or hitting different nodes does not raise your effective budget. The read budget is keyed on the **source IP**, not on the address or API key: every account queried from one IP shares that IP's bucket, and REST requests from different IPs draw from separate buckets. ## Per-IP weight limits Every IP gets a token bucket holding **1,500 weight**, refilling continuously at **1,500 weight per minute** (25 weight/second). Each request deducts its weight before the handler runs. When the bucket can't cover a request's weight, the request is rejected. ### Rejection contract A throttled request returns: ```http theme={null} HTTP/1.1 429 Too Many Requests Retry-After: 2 Content-Type: application/json {"error":"rate limited"} ``` `Retry-After` is the whole number of seconds to wait before the bucket holds enough tokens to retry (minimum `1`). Arcus does **not** return `X-RateLimit-*` headers — the budget and per-endpoint weights are published here instead, so you can account for cost client-side rather than discovering it per response. ### Which limit did I hit? The response **body tells you which bucket rejected the request**, so you never have to guess which layer to tune: | Body | Bucket that rejected you | What to change | | - | - | - | | `{"error":"rate limited"}` — no `reason` field | **Per-IP weight**, on a read endpoint | Lower read frequency, move to a WebSocket subscription, or use a cheaper endpoint | | `{"error":"rate limited","reason":"ip",...}` | **Per-IP weight**, on an order write | Reduce non-write traffic from this IP — the write itself is free, so something else drained the bucket | | `{"error":"rate limited","reason":"account_empty",...}` | **Per-subaccount pool** is exhausted; you are on the drip | Slow the order/cancel loop, or build lifetime volume to raise the cap | | `{"error":"rate limited","reason":"account_partial",...}` | **Per-subaccount pool** can't cover this charge in full (typically an oversized batch) | Send a smaller batch | Order-write rejections carry two extra fields: `retryAfterMs`, the precise millisecond wait (the `Retry-After` header is the same value rounded up to whole seconds, so it is never shorter), and the `clientId` / `clientIds` you submitted, echoed back so a rejection can be correlated to a specific order: ```json theme={null} { "error": "rate limited", "reason": "account_empty", "retryAfterMs": 850, "clientId": "my-order-1" } ``` There is **no maker-specific or taker-specific throttle**. The only limits that exist are the per-IP weight bucket, the two per-subaccount trading pools, and the [WebSocket connection limits](#websocket-limits) below. If a `429` is not attributable to one of those, it is not a rate limit. ### Working out which limit binds first Because the IP bucket refills at **25 weight/second**, the sustainable request rate depends entirely on endpoint weight: | Endpoint weight | Sustained rate from one IP | Example endpoints | | - | - | - | | 2 | **12.5 req/s** | `bbo`, `mids`, `account`, `positions`, `order`, `feeTiers` | | 20 | **1.25 req/s** | `prices`, `markets`, `orders`, `fills`, `openOrders`, `portfolio`, `candles` | | 125 | **0.2 req/s** | `setLeverage`, `withdraw`, `transfer` | | 0 | unbounded on this layer | order writes (pool-governed), `health` | A polling loop at, say, 4 req/s against weight-20 endpoints costs 80 weight/s against a 25/s refill — it drains the 1,500 bucket in roughly 25 seconds and then throttles continuously, **even though the order path is completely idle**. Order writes cost 0 IP weight, so a `429` seen while placing orders at a modest rate is usually the read traffic sharing that IP, not the trading path. Check the `reason` field before rate-limiting your own order flow. ### Weight tiers Endpoints fall into a handful of tiers by how much work they cost the backend. With a 1,500/min budget that's roughly **750 cheap reads**, **75 standard list calls**, or **12 `setLeverage`/`withdraw` calls** per minute from a single IP. | Weight | Endpoints | | - | - | | **0** | `health` — never touches the limiter (use it for liveness probes). **Order writes** — `placeOrder`, `modifyOrder`, `cancelOrder`, and `cancelAllOrders` — are also **free on the IP layer**; they're governed entirely by the per-subaccount trading pools below (`cancelAllOrders` charges the cancel pool a flat 1,000). | | **1** | `/` (root), `time` | | **2** | Cheap single-object reads — `bbo`, `mids`, `account`, `positions`, `order`, `feeTiers`, `leverages`, `accountStats`, `rateLimit` | | **20** | Standard data, lists, and management — `prices`, `markets`, `trade`, `trades`, `candles`, `portfolio`, `openOrders`, `orders`, `fills`, `funding`, `fundingRates`, `accountTransferUpdates`, `apiKeys`, `createApiKey`, and `revokeApiKey` | | **125** | Heavyweight writes — `setLeverage`, `withdraw`, `transfer` | **Batch order writes** (`batchPlaceOrders`, `batchCancelOrders`) are also free on the IP base; they incur only a post-flight per-item charge of `floor(N/40)` weight for a batch of `N` — so a batch of up to 39 orders costs **0** IP weight. As with single writes, the real limit is the per-subaccount trading pool (each element charges the pool 1). **List endpoints add a per-item charge** on top of their base weight, computed from the rows actually returned: `weight = base + floor(items / N)`, where `N` is `20` for most lists — `60` for `candles` and `50` for `openOrders`. Page sizes are capped at **1,000 rows** for most lists — `1,500` for `candles` — so the worst-case add-on is bounded: a full 1,000-row page of `fills` costs `20 + 50 = 70`, and a full 1,500-bar `candles` page costs `20 + 25 = 45`. `l2OrderBook` works the same way on order-book depth, charging on the depth you request rather than the rows returned: `2 + floor(nLevels/20)`, maxing at weight 7 for a 100-level book. Because the row count isn't known until the query runs, this charge is applied after the response is sent, so a single large page can briefly drive your bucket negative; requests arriving before it refills are rejected with `429` and a `Retry-After` hint, as above. ## Per-subaccount trading limits Order placement and cancellation draw from two pools scoped to your **subaccount** (address + account index), enforced on both the REST and WebSocket order paths. This is the address-based analogue to the per-IP layer: it bounds how fast a single account can act, regardless of how many IPs or connections it spreads across. | Pool | Starting cap | Charged by | | - | - | - | | **Order** | 20,000 | `placeOrder` / `modifyOrder` — 1 per order (N for a batch of N) | | **Cancel** | 40,000 | `cancelOrder` — 1 per cancel (N for a batch of N); `cancelAllOrders` — flat **1,000** | The pools are independent: a cancel-heavy strategy can drain the cancel pool while the order pool sits nearly full, or vice versa. Both pools are keyed **strictly** on `(address, accountIndex)` — there is no per-market component. Orders across every market share one order pool, so spreading flow over more markets does not buy extra headroom; moving it to another **subaccount** does, since each index carries its own pools (and its own volume credit). ### Volume-based replenishment Each pool's effective cap grows with your **lifetime realized notional** — the more you trade, the more headroom you get: ``` effective cap = starting cap + lifetime_notional_usd / 0.10 ``` Every **\$0.10** of realized fill notional adds **1** unit of pool headroom (so \$1 of volume buys **10**). Roughly **\$100K** of cumulative volume buys **+1,000,000** headroom on each pool, and high-volume market makers (≥ \$1M lifetime) are effectively unconstrained. The counter is cumulative and never decays, so caps only ever rise. ### When a pool is empty Past the effective cap, a slow **drip** takes over: **1 action per 10 seconds** per pool. This keeps a depleted account alive at a trickle rather than hard-failing, while still throttling abuse. Charges that exceed the remaining pool are rejected the same way as the IP layer (with a retry hint). Charges are refunded automatically if an accepted order fails to publish downstream (a transient internal error). Engine rejections do **not** refund — the matching-engine work was already consumed. ### Checking your remaining budget `GET /v1/rateLimit?address=<0x...>&accountIndex=` returns a live snapshot of both pools. It covers only the per-subaccount trading pools: there is no endpoint that reports how much of your [per-IP weight budget](#per-ip-weight-limits) is left, so track that client-side from the published weights. The call itself costs weight 2. Example response: ```json theme={null} { "address": "0x...", "accountIndex": 0, "order": { "used": 1200, "cap": 21200, "nextAvailableMs": 0 }, "cancel": { "used": 0, "cap": 41200, "nextAvailableMs": 0 } } ``` * `used` — units consumed since your last reseed. * `cap` — current effective cap (starting cap plus volume replenishment). * `nextAvailableMs` — `0` when you have headroom; otherwise the milliseconds until the next drip token frees up. The subaccount parameter is spelled `accountIndex` (camelCase), the same as everywhere else in the API. An **unrecognised** parameter name is not an error — it is ignored, and the request silently resolves to index `0`. Because the response echoes back the `accountIndex` it actually served, compare that field against the index you asked for to catch a misspelling. An out-of-range `accountIndex` (above `9`) returns `400`. ### Dead man's switch quotas The [dead man's switch](/api-reference/websocket#dead-mans-switch-schedulecancel) (`scheduleCancel`) draws from the **cancel** pool on every arm/refresh like other cancels, and adds two quotas of its own: * **Auto-fires** — capped at **10 per UTC day per subaccount**, shared across all of that subaccount's switches (account-wide and per-market). * **Live switches** — a wallet may hold at most **50 switches armed at once**, across all its subaccounts and markets. Arming a new switch past the cap returns `429`; refreshing an already-armed switch is exempt. ## WebSocket limits WebSocket traffic is bounded by its own per-IP limits. WebSocket messages do **not** draw from the REST weight bucket above: | Limit | Value | Scope | | - | - | - | | Simultaneous connections | 50 | per IP | | New connections | 50 / min | per IP | | Subscriptions per connection | 100 | per socket | | Subscriptions | 1,000 | per IP (across all connections) | | Outbound messages | 1,000 / min | per IP | | In-flight `post` messages | 50 | per connection | | Connection lifetime | 24 hours | per connection (auto-closed after) | Only **client→server** `subscribe` / `unsubscribe` messages, JSON `ping` messages, and malformed frames count against the outbound-message rate. Well-formed `post` / `get` RPCs are **exempt** — they're bounded by the per-subaccount trading pools and the in-flight `post` cap instead. Server-pushed `channel_data` updates are free. A rejected new connection still consumes a new-connection token, so a client hammering the connection cap burns its own budget faster. ## Handling rate limits * **Read `Retry-After`** on a `429` and back off for at least that long — or `retryAfterMs` on an order write, which is precise to the millisecond. A blind retry loop will keep losing. * **Check the `reason` field** before you throttle yourself. It names [which bucket rejected you](#which-limit-did-i-hit); tuning the wrong layer costs throughput for nothing. * **Prefer WebSocket subscriptions over REST polling** for anything that changes frequently (order book, fills, account state). One subscription replaces a stream of weighted REST calls. * **Batch order operations.** A batch of `N` costs only `floor(N/40)` IP weight (free on the IP base, like single writes) — far cheaper than `N` separate calls — though it still charges per item against the trading pool. You also save the per-request network round trips. * **Poll `GET /v1/rateLimit`** if you run an aggressive order/cancel loop, and slow down as `used` approaches `cap`. * **Request smaller pages** on list endpoints — a large page adds per-item weight after the fact. If your use case legitimately needs higher limits (e.g. a high-volume market maker), reach out about IP allowlisting for your egress addresses. # Check whether a referral code is available Source: https://docs.arcus.xyz/api-reference/referral/check-whether-a-referral-code-is-available /api-reference/openapi.yml get /v1/affiliate/codeAvailable Returns `{ code, available }` for a single referral code string. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns `{ code, available }` for a single referral code string. `available: true` means no row exists in the codes directory and the code is currently free for `POST /v1/affiliate/createCode`. **Advisory only.** Two callers can both observe `available: true` and race on createCode; only one will end up owning the code. Use this endpoint for live form-validation feedback in the UI, but treat the createCode response as the authoritative result. Public — no `X-API-Key`, no signature. # Claim accrued commission Source: https://docs.arcus.xyz/api-reference/referral/claim-accrued-commission /api-reference/openapi.yml post /v1/affiliate/claim Submits a claim for accrued affiliate commission. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Submits a claim for accrued affiliate commission. The server reads the pending balance and cutoff from the datastore and computes a deterministic `transferId`, making retries idempotent. `cutoffFillId` and `amountQuantums` are optional — when supplied they are validated against the server-computed values (optimistic UX check). A mismatch returns 400 so the client can prompt a refresh. # Create referral code Source: https://docs.arcus.xyz/api-reference/referral/create-referral-code /api-reference/openapi.yml post /v1/affiliate/createCode Creates a new referral code for the authenticated affiliate. Requires Ed25519 signature. The `address` in the body must match the API key's master Ethereum address. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Creates a new referral code for the authenticated affiliate. Requires Ed25519 signature. The `address` in the body must match the API key's master Ethereum address. # Get affiliate claim history Source: https://docs.arcus.xyz/api-reference/referral/get-affiliate-claim-history /api-reference/openapi.yml get /v1/affiliate/claims Returns one page of an affiliate's claim history, newest-first by `timestampMs`. Optional `from`/`to` (epoch ms) bound the time window for follow-on pagination. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns one page of an affiliate's claim history, newest-first by `timestampMs`. Optional `from`/`to` (epoch ms) bound the time window for follow-on pagination. # Get affiliate commission history Source: https://docs.arcus.xyz/api-reference/referral/get-affiliate-commission-history /api-reference/openapi.yml get /v1/affiliate/commissions Returns one page of the caller's own per-fill commission events, ordered by `fillId DESC, tier ASC, referee ASC`. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns one page of the caller's own per-fill commission events, ordered by `fillId DESC, tier ASC, referee ASC`. Each row carries both the per-event delta (`amountQuantums`) and the post-event snapshot (`postTotalCommissionQuantums`, `postPendingCommissionQuantums`) stamped by the referral service at emission time, so clients never have to re-aggregate. **Signed, and scoped to the caller.** `address` must be the API key's master Ethereum address; reading another affiliate's commission log is `403`. The Ed25519 request signature is required because `X-API-Key` alone proves nothing (API keys are Ed25519 public keys, listable by anyone via `GET /v1/apiKeys`). With no request body the signing message is `timestamp + action` (action `commissions` — the path segment verbatim, casing included). # Get affiliate info Source: https://docs.arcus.xyz/api-reference/referral/get-affiliate-info /api-reference/openapi.yml get /v1/affiliate/info Returns the caller's own affiliate information including code, tier, commission totals, and referee count. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns the caller's own affiliate information including code, tier, commission totals, and referee count. **Signed, and scoped to the caller.** `address` must be the API key's master Ethereum address; reading another wallet's affiliate info is `403`. The Ed25519 request signature is required because `X-API-Key` alone proves nothing (API keys are Ed25519 public keys, listable by anyone via `GET /v1/apiKeys`). With no request body the signing message is `timestamp + action` (action `info` — the path segment verbatim, casing included). # Get affiliate leaderboard Source: https://docs.arcus.xyz/api-reference/referral/get-affiliate-leaderboard /api-reference/openapi.yml get /v1/affiliate/leaderboard Returns the top-N affiliates by lifetime commission, all-time. The result is cached 60s. Cardinality is bounded by the affiliate count, not the commission count. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns the top-N affiliates by lifetime commission, all-time. The result is cached 60s. Cardinality is bounded by the affiliate count, not the commission count. # Get my referrer Source: https://docs.arcus.xyz/api-reference/referral/get-my-referrer /api-reference/openapi.yml get /v1/affiliate/myReferrer Returns the referral binding for the given address, if any — the `code` it was bound under and the `discountBps` it earns. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns the referral binding for the given address, if any — the `code` it was bound under and the `discountBps` it earns. Does **not** return the referrer's address. # Get referees Source: https://docs.arcus.xyz/api-reference/referral/get-referees /api-reference/openapi.yml get /v1/affiliate/referees Returns one page of the caller's T1/T2/T3 downline, newest-first by each row's `boundAt`. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns one page of the caller's T1/T2/T3 downline, newest-first by each row's `boundAt`. `limit` defaults to 1000 and is clamped there. Pass `parent` to page the part of that same downline under one referee: a T1 parent yields their T2 and T3, a T2 parent yields their T3, and a T3 parent has none left inside the three-level book. The header totals (trader count, referred volume, fees earned) are on `GET /v1/affiliate/info`, not a sum of this page. They cover the same T1/T2/T3 downline as these rows, so across every page `rollingVolume` sums to `totalReferredVolume30d`, `volumeAllTime` to `totalReferredVolumeAllTime`, and `feesEarned` to `totalCommission` (up to rollup lag, and less any commission from referees who have since moved out of the downline). `traderCount` is the full downline. All figures are perps only. Optional `from`/`to` (epoch ms) bound `boundAt`. Use `nextCursor` for the next page of the same `parent`. A cursor from one parent is not valid for another. **Signed, and scoped to the caller.** `address` must be the API key's master Ethereum address; listing another affiliate's referees is `403`. The Ed25519 request signature is required because `X-API-Key` alone proves nothing (API keys are Ed25519 public keys, listable by anyone via `GET /v1/apiKeys`). With no request body the signing message is `timestamp + action` (action `referees` — the path segment verbatim, casing included). # Get the caller's referral code Source: https://docs.arcus.xyz/api-reference/referral/get-the-callers-referral-code /api-reference/openapi.yml get /v1/affiliate/code Lightweight code-only read: returns `{ address, code, isAffiliate }` from a single database point read — none of the volume, commission, or fee-tier enrichment that `GET /v1/affiliate/info` performs. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Lightweight code-only read: returns `{ address, code, isAffiliate }` from a single database point read — none of the volume, commission, or fee-tier enrichment that `GET /v1/affiliate/info` performs. Intended for UI surfaces that only need the code (e.g. an "Invite Friends" / copy-referral-link control) and may poll on every session. Unlike `/info`, "not an affiliate" is a valid empty state served as 200 with `code: ""` and `isAffiliate: false` — never a 404 — so callers don't have to overload the 404 status. **Signed, and scoped to the caller.** `address` must be the API key's master Ethereum address; looking up another wallet's code is `403`. The Ed25519 request signature is required because `X-API-Key` alone proves nothing (API keys are Ed25519 public keys, listable by anyone via `GET /v1/apiKeys`). With no request body the signing message is `timestamp + action` (action `code` — the path segment verbatim, casing included). # List the caller's invite codes Source: https://docs.arcus.xyz/api-reference/referral/list-the-callers-invite-codes /api-reference/openapi.yml get /v1/affiliate/inviteCodes Returns the caller's earned invite codes with redemption status. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns the caller's earned invite codes with redemption status. One invite code is earned per `$1M` of the caller's own lifetime perps trading volume plus the all-time volume of their direct referees. Volume from deeper levels of the referral tree does not count. Each code is tagged to a referrer address, and to the owner's referral code when they have one. The server stores both and links at redemption; neither is returned. Codes appear here as volume milestones are crossed. **Signed.** An invite code is a bearer secret that grants perps trading access, and `X-API-Key` alone proves nothing (API keys are Ed25519 public keys, listable by anyone via `GET /v1/apiKeys`). The Ed25519 request signature proves possession of the private key, and the key must be registered to `address`. With no request body the signing message is `timestamp + action` (action `inviteCodes` — the path segment verbatim, casing included). The response is returned with `Cache-Control: no-store`. # Look up a single claim Source: https://docs.arcus.xyz/api-reference/referral/look-up-a-single-claim /api-reference/openapi.yml get /v1/affiliate/claimStatus Returns a single claim row identified by `(address, id)`. Both parameters are required — claims are partitioned by affiliate in the datastore, so a global `id`-only lookup is not supported. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Returns a single claim row identified by `(address, id)`. Both parameters are required — claims are partitioned by affiliate in the datastore, so a global `id`-only lookup is not supported. # Redeem an invite code Source: https://docs.arcus.xyz/api-reference/referral/redeem-an-invite-code /api-reference/openapi.yml post /v1/affiliate/redeemInvite Redeems a single-use invite code. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Redeems a single-use invite code. On success the caller is: * linked in the referral tree to the invite code's owner by address. A self-redemption (see below) grants access and does not bind, and * granted perps trading access (effective typically within \~30 seconds). The invite code is marked used atomically — if two callers race on the same code, exactly one wins and the other receives `409`. **The response does not name the code's owner.** The redeemer never learns the address of the trader who invited them. The redemption publishes a referral binding from the caller to the code owner's address. A referral code is not required. The standard registration volume gate applies. **An invite code is only for gaining trading access.** A caller who is already whitelisted (on the API access whitelist, or already granted access by an earlier invite) is refused with `409` (`account is already whitelisted — invite code was not consumed`) and the code is not spent. | Caller can already trade | Caller already referred | Outcome | | - | - | - | | no | no | access granted, bound to the code's owner | | no | yes | access granted, existing referrer kept | | yes | either | `409` — already whitelisted, the code is not spent | An invite code never moves an existing referral binding. **Self-redemption** (redeeming your own code) grants access and burns the code, but creates no referral binding (A referring A is refused). The owner-code and volume gates do not apply to it. **Retry-safe:** every step is idempotent. If the response is a `502` ("… retry to complete"), the code is claimed by the caller and retrying the identical request completes the redemption. **Guessing guard.** Invite codes are short, so this endpoint enforces a tight budget on attempts that name a code which does not exist, on top of the standard rate limit. A caller who submits several unknown codes in quick succession receives `429` with `Retry-After`; the budget is not consumed by successful redemptions. # Register as referee Source: https://docs.arcus.xyz/api-reference/referral/register-as-referee /api-reference/openapi.yml post /v1/affiliate/registerAffiliate Binds the authenticated user to a referral code, registering them as a referee of the code's owner. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Binds the authenticated user to a referral code, registering them as a referee of the code's owner. When the registration volume gate is enabled, the referee's rolling 30-day volume must be strictly below \$1M. A successful registration links the caller to the code owner's address in the referral tree. A caller who cannot trade perps yet can only register by spending one of the code owner's invite codes: the first unused one is redeemed on the caller, which grants trading access, and shows as used in the owner's invite list. The owner must allow this. A caller who can already trade spends no code and is only linked. # Rename referral code Source: https://docs.arcus.xyz/api-reference/referral/rename-referral-code /api-reference/openapi.yml post /v1/affiliate/modifyCode Renames the authenticated affiliate's referral code to a new string in place. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Renames the authenticated affiliate's referral code to a new string in place. Existing referees keep earning the affiliate commission (the referrer↔referee link is by account, not by the code string), and the code's kickback is preserved. The **old string is retired permanently** — it can never be re-created or reclaimed by anyone, so links already shared under it stop resolving rather than later pointing at a different affiliate. Requires the affiliate to already hold a code (use `createCode` otherwise). The new string must be free: not currently owned and not a previously-retired (tombstoned) string. # Revoke referral code Source: https://docs.arcus.xyz/api-reference/referral/revoke-referral-code /api-reference/openapi.yml post /v1/affiliate/revokeCode Revokes the authenticated affiliate's referral code. Existing bindings remain but no new referees can register. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Revokes the authenticated affiliate's referral code. Existing bindings remain but no new referees can register. # Update kickback rate Source: https://docs.arcus.xyz/api-reference/referral/update-kickback-rate /api-reference/openapi.yml post /v1/affiliate/kickback Updates the kickback rate (BPS) for the authenticated affiliate's referral code. **The response body shown here is a static example, not live data.** After you click **Send**, your live result appears in a separate panel headed **200 OK**. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use **Send**, or call the endpoint, for current values. Updates the kickback rate (BPS) for the authenticated affiliate's referral code. # Order operations Source: https://docs.arcus.xyz/api-reference/trading/orderoperations > **Endpoint:** `wss://api.arcus.xyz/v1/ws` — one socket multiplexes every channel and RPC request. Click **Connect**, then send the example subscribe message below to start receiving data. RPC-style trading operations over WebSocket. Use `type: post` for order mutations and `type: get` for read-only snapshots. Every request carries a client-chosen numeric `id` that is echoed in the response, allowing you to correlate replies with requests. Use a monotonically increasing counter per connection. **Order mutations are asynchronous** — they reply with `status: 202` (accepted, not terminal). Subscribe to `orders` and `userFills` to observe the full order lifecycle. **Supported `post` methods:** `placeOrder`, `cancelOrder`, `cancelAllOrders`, `scheduleCancel`, `modifyOrder`, `batchPlaceOrders`, `batchCancelOrders`, `batchModifyOrders`, `setLeverage`, `adjustIsolatedMargin`. **Supported `get` methods:** `l2orderbook`, `bbo`, `mids`, `account`, `fills`, `orders`, `markets`, `prices`, `positions`, `leverages`, `ratelimit`. # WebSocket Source: https://docs.arcus.xyz/api-reference/websocket Real-time market data and order routing Connect to `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:** ```json theme={null} { "type": "subscribe", "channel": "", "id": "" } { "type": "unsubscribe", "channel": "", "id": "" } ``` The server replies with `{ "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: | Field | Applies to | Meaning | | - | - | - | | `snapshot` | channels that stream deltas | `false` suppresses the initial snapshot so only live updates flow. Default `true`. Ignored (always `true`) on snapshot-only channels such as `markets`. | | `nLevels` | `l2Orderbook`, `l2OrderbookUpdates` | Price levels per side, 1–100 (default 20). | | `nRecentClosed` | `orders` | Closed orders included in the snapshot (default 100). | | `nFills` | `userFills` | Fills included in the snapshot (default 500, max 500). | | `market` | `orders`, `positions`, `userFills` | Restricts the snapshot and the live stream to one market (ticker such as `BTC-USD`, or numeric market ID). Omit for all markets. An unknown market, or `market` on any other channel, is rejected with an `error` frame rather than ignored. A filtered and an unfiltered subscription to the same address can coexist on one connection; pass the same `market` on `unsubscribe` to remove a filtered one. | **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 must be **strictly increasing per connection**: a `get` or `post` whose `id` is not greater than the previous one on that socket is rejected with an `error` frame. That `error` frame does **not** echo the `id`, so a client that matches replies by `id` will see the request as unanswered rather than failed. A counter that starts at 1 for each new connection satisfies this. ```json theme={null} { "type": "get", "id": 2, "request": { "type": "l2orderbook", "payload": { "market": "BTC-USD" } } } ``` See [Authentication](/api-reference/authentication) for the signing rules that apply to order methods. ## 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. Your positions and fills are observable in real time by anyone who knows your address. If a strategy depends on that activity not being attributable, separate it by address — the API offers no per-address read restriction. 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](/api-reference/authentication). ## Placing orders Order methods are asynchronous. The server returns `202` with `status: "ACK"` (or `CANCEL_ACKNOWLEDGED`); subscribe to the [`orders`](/api-reference/channels#orders) or [`userFills`](/api-reference/channels#userfills) channels to observe the lifecycle. ```json theme={null} { "type": "post", "id": 1, "request": { "type": "placeOrder", "payload": { "address": "0x...", "accountIndex": 0, "marketId": 1, "orderSide": "BUY", "orderType": "LIMIT", "timeInForce": "GTT", "goodTilTime": "4102444800000000", "quantity": "0.01", "price": "50000" }, "apiKey": "<64-hex-public-key>", "timestamp": "1712345678000000000", "signature": "<128-hex-signature>" } } ``` Server response: ```json theme={null} { "method": "placeOrder", "id": 1, "status": 202, "result": { "orderId": "...", "status": "ACK" } } ``` **No cancel-on-disconnect.** Dropping a WebSocket connection tears down your subscriptions but does **not** cancel your resting orders. An order rests until it fills, you explicitly cancel it, or its `goodTilTime` expires (an expired resting order is removed when matching reaches it and reported on the `orders` channel as `REJECTED` / `EXPIRED`), and `goodTilTime` must be at least \~1 month out (enforced on every order, including `IOC`/`FOK`). If you need orders pulled when your process dies, arm the [dead man's switch](#dead-mans-switch-schedulecancel) or run your own external kill-switch that cancels on disconnect. ## 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](/api-reference/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, later `time` before the current one elapses (typically `now + 60s` on a short refresh interval); reusing a past `time` does not extend it. * **Disarm** — omit `time` (or send `null`). **Scope.** Omit `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. ```json theme={null} { "type": "post", "id": 5, "request": { "type": "scheduleCancel", "payload": { "address": "0x...", "accountIndex": 0, "time": 1734567890123000 }, "apiKey": "<64-hex-public-key>", "timestamp": "1712345678000000000", "signature": "<128-hex-signature>" } } ``` Add `"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](/api-reference/rate-limits#dead-mans-switch-quotas)): * **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. When a switch fires, the resulting cancellations stream over the [`orders`](/api-reference/channels#orders) channel like any other cancel. ## Channels See the [Channels](/api-reference/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. Which field carries the sequence depends on the message: | Scope | Field | Appears on | | - | - | - | | Global | `globalSequenceId` | `l2Orderbook`, `l2OrderbookUpdates`, `bbo` | | Global | `sequenceNumber` | `trades` | | Per-market (order book) | `lastSequenceId` | `l2Orderbook`, `l2OrderbookUpdates`, `bbo` | | Per-market (market attributes) | `marketSequenceNum` | `exchangeAttributeUpdates` (`marketAttributes` entries) | | Per-account | `sequenceNumber` | `AccountUpdate` (`AccountTransferEvent` exposes the same value as `accountSequenceNum`) | | Per-account | `lastSequenceId` | `account` and `positions` snapshots | 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 For `l2Orderbook`, 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-market `lastSequenceId`, 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: | Field / channel | Contiguous per subscription? | Handling | | - | - | - | | `lastSequenceId` on `l2Orderbook` / `l2OrderbookUpdates` | Contiguous once you are applying deltas; the first delta after a snapshot may be a few sequences ahead (expected boundary gap) | Seed from the snapshot and apply deltas whose `lastSequenceId` exceeds it; do **not** re-subscribe on the initial boundary gap. A gap that appears **mid-stream** means a missed delta — re-subscribe for a fresh snapshot then. | | `lastSequenceId` on `bbo` | No — advances past suppressed updates | `bbo` emits a frame only when the top of book changes, so the sequence legitimately **jumps forward** over depth-only updates. A forward jump is normal — do **not** treat it as a dropped frame or re-subscribe. | | `globalSequenceId` (any channel) | No — cross-market counter | Other markets consume values, so it skips within a single market's stream. It's an ordering key only; never gap-check it. | `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-like `status` and either a `result` or an `error` object with `type`, `message`, and optional `field`, `errorType` / `errorSource` (machine-readable code and the layer that raised it; `errorType: Transmission` means the order never reached the matching engine and is safe to resend unchanged) and `retryAfterMs` (on `429`). # Changelog API (Mainnet) Source: https://docs.arcus.xyz/changelog Release notes for the Arcus production API **Mainnet runs `production-v0.7.0`.** This page tracks what is live in production, anchored to the production release that shipped each change; verify the running version at any time with `GET /` on `https://api.arcus.xyz`. Changes reach testnet first — see the [testnet changelog](/changelog/testnet) for what is queued ahead of production. ## Referrals * An invite code is only for gaining trading access: [`POST /v1/affiliate/redeemInvite`](/api-reference/referral/redeem-an-invite-code) by a caller who can already trade returns `409` and the code is not spent. Redemption links you to the code's owner by address (the owner no longer needs a referral code) and never moves an existing referral. * A caller who cannot trade yet can register on [`POST /v1/affiliate/registerAffiliate`](/api-reference/referral/register-as-referee) by spending one of the code owner's unused invite codes, which grants trading access; `403` or `409` when the owner does not allow it or has none left, and `502` (nothing registered, safe to retry) when the access grant fails. * [`GET /v1/affiliate/info`](/api-reference/referral/get-affiliate-info) adds `traderCount`, and its referred-volume and commission totals now cover your whole T1/T2/T3 downline, perps only. [`GET /v1/affiliate/referees`](/api-reference/referral/get-referees) rows add `volumeAllTime`, and a `parent` filter pages one referee's sub-tree. * [`POST /v1/affiliate/createCode`](/api-reference/referral/create-referral-code) and [`modifyCode`](/api-reference/referral/rename-referral-code) reject a string that is already an invite code with `409` ("not a valid referral code"). * Spec correction: invite codes are earned on your own lifetime perps volume plus the all-time volume of your direct referees; deeper levels do not count. ## Trading * Accounts blocked under the Terms of Use are close-only: they can cancel orders, place reduce-only `IOC` or `FOK` orders to reduce or close a position, and withdraw. Every other trading request returns `403`. ## Referrals * Redeeming your own invite code on [`POST /v1/affiliate/redeemInvite`](/api-reference/referral/redeem-an-invite-code) grants you trading access if you don't have it yet; the code is spent and no referral binding is created. ## Stability * Fixed: a market's leverage change now shows on [`GET /v1/markets`](/api-reference/public/get-markets) as soon as the engine applies it, and markets listed after launch appear on the unfiltered list. * Fixed: [`GET /v1/leaderboard`](/api-reference/public/get-the-all-traders-leaderboard) no longer reports an overflowed `feesPaid` for traders whose net fees are negative (maker rebates). ## API changes * Markets listed after launch are served like the rest: they appear on prices, mids, candles, [`GET /v1/leverages`](/api-reference/public/get-leverage-and-margin-mode-for-an-account-across-all-markets) and the WebSocket channels, and their order rows show the market ticker. * REST list responses carry `total`, the number of items in the returned page (not the size of the full result set), on [`GET /v1/trades`](/api-reference/public/get-recent-public-trades), `/v1/candles`, `/v1/orders`, `/v1/openOrders`, `/v1/fills`, `/v1/positions`, `/v1/funding`, `/v1/fundingRates` and `/v1/accountTransferUpdates`. * [`GET /v1/apiKeys`](/api-reference/onboarding/get-api-keys) is bounded and paginated: keys come back newest first, capped by `limit` (default and maximum 1000), and `from` / `to` filter on `createdAt` in epoch milliseconds. An invalid `limit`, `from` or `to` returns `400`. ## Stability * Fixed: the WLFI-USD market's price-increment tiers now use the correct anchor price. * Fixed: withdrawals submitted in quick succession no longer stall before being sent on-chain. ## Referrals * The referral program is in the API reference: create, rename and revoke referral codes, register as a referee, and read referees, commissions and claims under [`/v1/affiliate/*`](/api-reference/referral/get-affiliate-info), plus the [commission rate schedule](/api-reference/public/get-referral-commission-rate-schedule). * Invite codes are live: [`GET /v1/affiliate/inviteCodes`](/api-reference/referral/list-the-callers-invite-codes) lists the codes you have earned (one per \$1M of lifetime perps volume), and [`POST /v1/affiliate/redeemInvite`](/api-reference/referral/redeem-an-invite-code) redeems one, granting trading access and binding you as a referee of the code's owner. * New [`GET /v1/leaderboard`](/api-reference/public/get-the-all-traders-leaderboard) ranks traders by volume, realized PnL or fees over a rolling window. * Referral payouts appear as `REFERRAL_CLAIM` transfers on [`GET /v1/accountTransferUpdates`](/api-reference/public/get-account-transfer-updates) and the [`accountTransferUpdates`](/api-reference/channels#accounttransferupdates) channel. ## API changes * New [`GET /v1/fill/{tradeId}`](/api-reference/public/get-fill-by-id): look up the single fill an account owns for a given `tradeId`. * [`GET /v1/leaderboard`](/api-reference/public/get-the-all-traders-leaderboard) accepts an `address` to return just that trader's row, and every row carries its `rank` on the active `sortBy` column. * [`GET /v1/affiliate/info`](/api-reference/referral/get-affiliate-info) adds `totalReferredVolumeAllTime`, the lifetime notional volume of all the affiliate's referees. * Spec correction: the [`GET /v1/affiliate/info`](/api-reference/referral/get-affiliate-info) spec now names the affiliate's trailing-30-day volume field `volume30d`, matching what the API already returned (it was documented as `rollingVolume`). No client change is needed. * New [`POST /v1/affiliate/modifyCode`](/api-reference/referral/rename-referral-code): rename your referral code in place; existing referees stay attached. * API keys can be scoped to every subaccount of an address: [`POST /v1/createApiKey`](/api-reference/onboarding/create-api-key) and [`POST /v1/revokeApiKey`](/api-reference/onboarding/revoke-api-key) accept `accountIndex: 255`, and key rows on [`GET /v1/apiKeys`](/api-reference/onboarding/get-api-keys) carry an `allSubaccounts` flag. ## Order management * [`POST /v1/scheduleCancel`](/api-reference/exchange/schedule-cancel-all-dead-mans-switch) accepts an optional `marketId` to arm a per-market dead man's switch, independent of the account-wide switch. A wallet may hold a bounded number of armed switches; arming beyond it returns `429`, and refreshing an armed switch is always allowed. ## WebSocket * Equity and free collateral streamed over WebSocket now net funding that has accrued but not yet settled, matching the values the margin engine uses. ## Orders and fills * **Partially filled orders that are then canceled now report `avgFillPrice`.** A `CANCELED` order with `filledSize` above zero on [`GET /v1/orders`](/api-reference/public/get-order-history) carries `avgFillPrice`, the volume-weighted price of its fills; until now it was omitted. * **`closedPnl` on closing fills is now computed from the cost basis the fill releases.** On [`GET /v1/fills`](/api-reference/public/get-fills), a fill that reduces or closes a position reports its notional against the cost basis it releases, minus the fee, instead of using a truncated per-unit average entry price. Opening and add legs still report `−fee`. * **Liquidation fills now report `closedPnl`.** The liquidated account's leg of a forced closure (`liquidation.method` `LIQUIDATION`) on [`GET /v1/fills`](/api-reference/public/get-fills) carries the realized PnL of the close, net of taker trading fees (the liquidation penalty included in `fee` is not deducted); until now it read `"0"`. Earlier liquidation fills are not backfilled. ## WebSocket * WebSocket connections now receive a `1001` (Going Away) close frame before the server drains them on restart, instead of being closed without a close frame. ## Liquidations and risk * Liquidation orders closed on the book carry the limit price the engine computed for the close, or `0` when that limit is not a positive price. * Liquidation updates report the full fee charged, including the liquidated account's taker trading fees, and forced-close order rows (liquidation and ADL) report `avgFillPrice`. * Orders, modifies, and fills that would take a position past the engine's maximum position notional, one fixed bound for all markets (about \$922M), are rejected with `POSITION_SIZE_CAP_EXCEEDED`. * A triggered TP/SL order keeps its trigger metadata on its terminal cancel, so [`GET /v1/order/{orderId}`](/api-reference/public/get-order-status) still shows the stop after an IOC partial fill. ## Margin * **Per-account isolated margin is now enabled in production.** Set a market to isolated mode with the optional `isolated` flag on [`POST /v1/setLeverage`](/api-reference/exchange/set-leverage-for-a-market), move collateral onto or off an isolated position's leg with [`POST /v1/adjustIsolatedMargin`](/api-reference/exchange/add-or-remove-margin-on-an-isolated-mode-position), and read the mode from `marginMode`/`isolated` on position rows, [`GET /v1/leverages`](/api-reference/public/get-leverage-and-margin-mode-for-an-account-across-all-markets), and the [`accountAttributeUpdates`](/api-reference/channels#accountattributeupdates) channel. The API surface shipped in `production-v0.1.10.0` and read `CROSS`/`false` for every account until now; isolated behaviour is active for all accounts as of this update. Testnet has carried the full mechanics since `testnet-v1.8.0` — see the [testnet changelog](/changelog/testnet) for the detailed `marginUsed`, rejection-reason, and streaming semantics. ## WebSocket / Order book * Subaccounts across the account-scoped channels — [`account`](/api-reference/channels#account), [`positions`](/api-reference/channels#positions), [`orders`](/api-reference/channels#orders), [`userFills`](/api-reference/channels#userfills), [`funding`](/api-reference/channels#funding), [`accountTransferUpdates`](/api-reference/channels#accounttransferupdates), and [`accountAttributeUpdates`](/api-reference/channels#accountattributeupdates): an optional `accountIndex` (0–9), part of subscription identity and echoed on every frame. Omit it for subaccount 0. * Position and order rows carry a per-account `sequenceNumber` (the same counter as `AccountUpdate.sequenceNumber`); the [`orders`](/api-reference/channels#orders) snapshot carries `lastSequenceId`, matching [`positions`](/api-reference/channels#positions). ## API changes * Referral codes are non-resolvable to wallets: [`GET /v1/affiliate/myReferrer`](/api-reference/referral/get-my-referrer) redacts the referrer address. ## Markets * 13 new crypto markets enabled and tradeable: DOGE, ENA, PUMP, NEAR, PENGU, FARTCOIN, SUI, WLFI, LINK, BNB, TAO, AAVE, UNI. ## Breaking changes * `from`/`to` on [`GET /v1/funding`](/api-reference/public/get-funding-payments) and [`GET /v1/fundingRates`](/api-reference/public/get-market-funding-rates) are epoch **microseconds**, matching the unit each response reports. Millisecond values are now rejected. ## API changes * New `market` filter (ticker or numeric id) on [`GET /v1/prices`](/api-reference/public/get-live-prices-for-all-markets), [`GET /v1/mids`](/api-reference/public/get-all-mid-prices), [`GET /v1/positions`](/api-reference/public/get-positions), [`GET /v1/leverages`](/api-reference/public/get-leverage-and-margin-mode-for-an-account-across-all-markets), and [`GET /v1/funding`](/api-reference/public/get-funding-payments); an unresolvable value returns 400. * New `status` filter on [`GET /v1/orders`](/api-reference/public/get-order-history) and [`GET /v1/openOrders`](/api-reference/public/get-open-orders): `OPEN`, `UNTRIGGERED`, `FILLED`, `CANCELED`, `REJECTED`, `LIQUIDATED`, `ADL`. * New `side` filter (`BUY`/`SELL`) on [`GET /v1/orders`](/api-reference/public/get-order-history) and [`GET /v1/fills`](/api-reference/public/get-fills). The `role` filter on `GET /v1/fills` now matches correctly. * `netDeposits` on [`GET /v1/account`](/api-reference/public/get-account) and the [`accounts`](/api-reference/channels#accounts) channel reports real lifetime net deposits. ## Order management * New [`POST /v1/scheduleCancel`](/api-reference/exchange/schedule-cancel-all-dead-mans-switch) dead man's switch: arm, refresh, or disarm a per-subaccount deadline; if it elapses the gateway fires `cancelAllOrders` for that subaccount. * [`GET /v1/positions`](/api-reference/public/get-positions) no longer returns 500 when the oracle price read times out. * Position rows and [`GET /v1/leverages`](/api-reference/public/get-leverage-and-margin-mode-for-an-account-across-all-markets) carry `marginMode` and `isolated`. When this release shipped both read `CROSS` and `false` for every account, as per-account isolated margin was not yet enabled in production; it has since been turned on (see the latest entry above). * Cancel + place: every modify emits CANCELED (cancelReason: MODIFY\_CANCELED) then the replacement outcome —PLACED, fills, orREJECTED— under the same orderId. Same-price size decreases keep queue priority. You can expect to see the cancel + place/fill messages on the same GSN but increasing account subsequence numbers for clarity about ordering. Cancels should always arrive first before any fills/place from the newly modified order. ## WebSocket / Order book * `market` filter on [`positions`](/api-reference/channels#positions), [`orders`](/api-reference/channels#orders), and [`userFills`](/api-reference/channels#userfills) subscriptions and on the `get` RPCs for `fills`, `orders`, `positions`, `leverages`, `mids`, and `prices`. It is part of subscription identity, so `unsubscribe` must repeat it. * [`userFills`](/api-reference/channels#userfills) subscribe accepts `nFills` (1–500, default 500) to bound the initial snapshot. * `isolated` and `marginMode` are always present on [`accountAttributeUpdates`](/api-reference/channels#accountattributeupdates) leverage entries. * A fill could produce a duplicate account snapshot with a repeated `lastSequenceId`, and an `AccountUpdate` could be applied twice when a snapshot was scheduled. ## Markets * CPER-USD, GME-USD, and QNT-USD are enabled and tradeable, having been listed offline in `production-v0.1.8`. ## API changes * New filters on [`GET /v1/orders`](/api-reference/public/get-order-history) and [`GET /v1/fills`](/api-reference/public/get-fills): a `market` param (ticker or numeric id, case-insensitive). `GET /v1/fills` also gets a `role` param (`MAKER`/`TAKER`). The same market filter works on [`GET /v1/markets`](/api-reference/public/get-markets). * [`GET /v1/apiKeys`](/api-reference/onboarding/get-api-keys) with no `accountIndex` now returns keys for all subaccounts under the address, each tagged with its own `accountIndex` (previously just the default one). ## Authentication * API keys can carry a `withdraw` permission, allowing [`POST /v1/withdraw`](/api-reference/exchange/submit-withdrawal) to be authorized with an Ed25519 API-key signature instead of a wallet signature. Keys created through the public [`POST /v1/createApiKey`](/api-reference/onboarding/create-api-key) remain trade-only. * [`POST /v1/createApiKey`](/api-reference/onboarding/create-api-key) and [`POST /v1/revokeApiKey`](/api-reference/onboarding/revoke-api-key) accept an optional single-use `nonce` for replay protection; a replayed nonce returns HTTP 409. ## Rate limits * [`GET /v1/openOrders`](/api-reference/public/get-open-orders) costs less against the account rate limit: the per-item charge is now one unit per 50 orders returned, down from one per 20, so a full page costs roughly 2.5× less budget. ## WebSocket / Order book * [`l2Orderbook`](/api-reference/channels#l2orderbook) subscriptions now support multiple simultaneous aggregation levels per market on one connection. Frames echo `sigFigs`/`roundStep` so views can be demultiplexed. * Order book snapshots now carry up to 300 price levels (raised cap). ## Markets * MRNA-USD, NBIS-USD, MRVL-USD, BOT-USD, and MSTR-USD enabled. CPER-USD, GME-USD, and QNT-USD listed at 10x leverage, offline. ## Market data * The mark price no longer holds a stale premium when the book is too thin to sample an impact mid. The 2.5-minute impact-mid EWMA previously froze at its last value for as long as depth was missing, then stepped when depth returned; it now decays toward the oracle price during regular trading hours, and toward the sealed RTH settlement price off-hours. Mark price is what margin, unrealized PnL, and liquidations are valued off, so a stale premium moved those too. ## Order management * Fixed a case where a closed position could still read as open. An entry fill and a stop-loss close landing in the same engine tick could be persisted out of order, leaving a flat position showing a size; closing it was then rejected with `REDUCE_ONLY_WOULD_INCREASE`. Position rows are now versioned by publish order. * Mark price stability during thin markets — The oracle now falls back to the oracle or settlement price when the orderbook is too thin to sample an impact-mid price, preventing mark price gaps during low-liquidity conditions. * Increased WebSocket frame buffer — The WebSocket server's maximum request body size has been increased from 64 KB to 1 MB, reducing connection errors for clients sending larger payloads. * More reliable position history — Fixed an edge case in position write ordering that could cause duplicate or out-of-order position records under high sequencer throughput. ## Breaking changes * `from`/`to` time bounds are now epoch **microseconds** on the endpoints whose timestamps are nanosecond-backed — [`GET /v1/openOrders`](/api-reference/public/get-open-orders), [`GET /v1/orders`](/api-reference/public/get-order-history), [`GET /v1/fills`](/api-reference/public/get-fills), [`GET /v1/trades`](/api-reference/public/get-recent-public-trades), and [`GET /v1/accountTransferUpdates`](/api-reference/public/get-account-transfer-updates). Millisecond values are rejected with HTTP 400. Clients that previously sent milliseconds must update. ## API changes * [`GET /v1/openOrders`](/api-reference/public/get-open-orders) is now bounded and paginated: `limit` (1–1000) plus `from`/`to` on `createdAt`. An account can hold up to 10,000 live orders, so more than one page may be needed. ## Authentication * API keys can be scoped to a non-zero subaccount index. ## WebSocket / Order book * `userFills` default snapshot size increased to 500. ## Market data * Fixed a race that could silently truncate [`GET /v1/candles`](/api-reference/public/get-ohlcv-candles) responses. ## Breaking changes * Deprecated `*Bps` fee-tier fields removed. Use `makerFeePpm`/`takerFeePpm` (and `maker_fee_ppm`/`taker_fee_ppm`); the values are unchanged. ## API changes * [`GET /v1/markets`](/api-reference/public/get-markets) exposes `minOrderSize` and `maxOrderSize` in base-asset units. `maxOrderSize` is a per-order gate — reduce-only orders are not exempt. * `pnlHistory` on [`GET /v1/portfolio`](/api-reference/public/get-portfolio-history) is rebased per timeframe, so each timeframe's first point is `0`. ## WebSocket / Order book * [`l2Orderbook`](/api-reference/channels#l2orderbook) accepts `sigFigs` (2–5) and `roundStep` (1, 2, or 5) to bucket price levels. Bids round down and asks round up, so the displayed spread is never tighter than the true book. * Off-hours trading-band shapes updated; `boundEvent` is now always present on delta entries, with `exitRth`/`enterRth` derived from RTH state. * Fee-tier account attribute updates now fan out to all subaccounts. ## API changes * [`GET /v1/prices`](/api-reference/public/get-live-prices-for-all-markets) omits OFFLINE markets. * [`GET /v1/markets`](/api-reference/public/get-markets) adds rolling 24-hour `high24h` / `low24h` fields. ## WebSocket / Order book * L2 order book now publishes every 200 ms. ## Order management * Fills carry a liquidation/ADL marker, and forced-closure status is consistent between live and persisted views. ## Breaking changes * Fee-tier fields renamed from bps to ppm. ## Market data * Spread filter applied to BBO candles. ## Breaking changes * API-key create/revoke signing migrated to EIP-712, with a dual-accept window during which the legacy scheme still works. ## API changes * New [`POST /v1/transfer`](/api-reference/exchange/submit-internal-transfer) for internal transfers between subaccounts. * [`GET /v1/account/stats`](/api-reference/public/get-account-stats) is sectioned, with opt-in fixed 24h/7d/30d windows. * Optional `market` filter on [`GET /v1/fills`](/api-reference/public/get-fills). ## Authentication * API keys are scoped to `(address, publicKey)`, closing a cross-account revocation path. * Replayed withdrawal nonces are rejected at the gateway — a signed withdrawal is single-use. * API-key subaccount authorization enforced on `modifyOrder`. ## Order management * Maximum order size enforced at the gateway. * Per-market open-interest caps enforced in the engine. * TPSL handling corrected after trading-band expansion and liquidations. ## Market data * Fee-tier volume window widened from 14 to 30 days. * `priceChange24H` computed from mark price rather than trade fills. * Candles priced from mark price; no-trade buckets filled from the BBO mid. * Off-hours trading behavior reworked, including the EWMA anchor-price fallback. ## Referrals * 5% referral fee discount for referred users. ## API changes * [`GET /v1/candles`](/api-reference/public/get-ohlcv-candles) serves oracle-priced candles, with oracle OHLC overlaid on no-trade gap buckets. * Affiliate info expanded and a code lookup endpoint added. ## Authentication * Ordersign signature verification enforced on REST `cancelOrder` and WebSocket `batchCancelOrders`. ## Order management * `reduceOnly` no longer requires `IOC` or `FOK`. ## Rate limits * `cancelAllOrders` charges zero IP budget. * `get`/`post` frames exempt from the WebSocket outbound-message rate limit. ## API changes * Exchange write responses return the account's remaining rate-limit allowance. * Rate-limit failures return the client id. ## Rate limits * Order-write endpoints cost zero against the IP bucket; the account-pool cost is configurable. ## WebSocket / Order book * `setLeverage` rejections are surfaced on the `accountAttributes` channel. ## Market data * SOFR-carry funding formula for RWA perps, quoted on the ACT/360 divisor. * Off-hours endpoints always advertise a distinct next trading bound. ## Order management * Off-hours partial-fill takers are rejected when the limit price violates the trading bound. * A resting order is preserved when a modify is rejected in pre-flight. ## Market data * Historical 1h and 1d candle volume was repaired for `2025-06-01` through `2026-06-30`. Those buckets had kept stale volume because the rollup only re-derived each aggregate forward from its newest stored bucket, so a 1m re-fetch never reached them. Re-pull any 1h/1d candles cached from that window. ## API changes * Unenforced fields removed from the [`GET /v1/markets`](/api-reference/public/get-markets) response. ## Market data * Funding formula corrected. ## API changes * Flat \$5 minimum order notional enforced. * Minimum withdrawal size of \$1 enforced. * Trading stat endpoints added. * Settlement price and trading bounds surfaced on the market endpoint; open interest added. ## Authentication * API keys upsert by name — re-creating with the same `apiWalletName` revokes the old key. * Wallet block-list enforced across the trade, withdraw, and key paths. ## Order management * Entry TPSL support, including rejecting `entryTpsl` children when the parent entry order is rejected. * Reduce-only margin check corrected. ## Initial mainnet release * First production release of the Arcus API at `https://api.arcus.xyz`. Releases `production-v0.0.1` through `production-v0.0.5` covered launch-period stabilization with no separately documented public API changes; `production-v0.0.7`, `production-v0.1.1`, and `production-v0.1.3` shipped no changes an API trader needs to act on. # Changelog FE Source: https://docs.arcus.xyz/changelog/frontend Release notes for the Arcus trading app **Updates to the Arcus trading app** — web and mobile. ## Mobile (Arcus iOS app — pre-launch) * Fixed: invite codes can now be entered on the referral code screen. ## Web * **Invite codes.** The Referrals page now lists the invite codes you've earned through trading volume, with each code's status and when it was used. Copy a single code, or copy every code on the page at once with Copy All Codes. Viewing your codes requires trading to be enabled. * Your position's entry line on the chart is now green for a long and red for a short. * The market selector now opens faster. * Wallets blocked under the Terms of Use can now withdraw their funds. * Fixed: after you claim a referral code, the fee discount shown now includes the base discount every referred trader receives, not only the referrer's extra share. * Fixed: account equity no longer overstates the value of a short position while that market's live price is still loading. * Fixed: on the Referrals page, a long referral link no longer overlaps the code field and its buttons. ## Mobile (Arcus iOS app — pre-launch) * **Referral or invite code when joining.** The Enter Referral or Invite Code screen accepts either kind of code, and tells you specifically why a code can't be applied — for example, if it has already been used or is your own. * Rejected orders now explain more reasons: the order couldn't be found, you've reached the open order limit, or the order expired before it could fill. ## Web * The market ticker in the header can now be scrolled by hand, pauses while you hover over it, and stays still if your system is set to reduce motion. * Fixed: take-profit and stop-loss orders on markets with tiered price increments are no longer rejected for an off-increment price. * Fixed: transfers rejected by the exchange now show as Failed in your transfer history, with the reason on hover, instead of Completed. * Fixed: prices and sizes in your orders, trade history, and funding payments, and in their detail panels, now display at each market's own precision. * Fixed: after an app update, reloading now clears a page that failed to load, instead of showing the same error again. * Fixed: opening the trade page without choosing a market now takes you to perps trading instead of an empty page. ## Web * **Order and trade details.** Click any row in your orders or trade history to open a side panel with its full details — order type, time in force, reduce-only and margin mode, and for each fill its price, fee, maker or taker role, realized PnL, and whether it opened, added to, closed, or flipped your position. * **Order book hover details and click-to-fill.** Hover a price level to see its distance from the mid price, the average price, and the cumulative size in the asset and in USD up to that level. Click a price to set it as your limit price, or click a size or total to fill in your order size. New price levels briefly highlight as they appear. * Trade history now shows realized PnL on liquidation fills, instead of a dash. * Mark price now sits with the other market stats in the market header, with a tooltip explaining that it drives margin, unrealized PnL, liquidations, and TP/SL triggers. * Fixed: market orders on markets with tiered price increments are no longer rejected for an off-increment price. ## Mobile (Arcus iOS app — pre-launch) * **Isolated margin.** Choose cross or isolated margin for each market from the trade screen, and add or remove margin on an isolated position. * **Clear order results.** After you swipe to confirm, the app shows your order executing and then its actual result — placed, filled, partially filled, canceled, or rejected — with the specific reason when an order is rejected, and a warning if an attached take-profit or stop-loss didn't go through. * Order and fill detail screens now show a copyable order ID, created and updated times, reduce-only, order value, and whether you were the maker or taker. * If the app can't finish loading, or you're offline, it now shows a clear screen with a Try again button, and a notice tells you when you go offline and when you're back online. * Fixed: perps market pages now show the mark price, and the 24-hour change in dollars reads correctly. * Fixed: the full-screen chart shows the market's 24-hour change instead of 0.00%, with prices at the market's precision. ## Mobile (Arcus iOS app — pre-launch) * **Edit open orders.** Change the price or size of an open order from its detail screen. * **Cancel all open orders.** Cancel every open order at once from the Open orders view. * **Order book on market pages.** A new Book tab on perps market pages shows bids and asks side by side, with price grouping and USD or asset units. * Enter a close amount in USD or in the asset, and see your current position size on the close screen. * The Account screen now shows your total equity, matching the Home screen. * With no funds available, the trade screens show a Deposit USDG button instead of a disabled order button. * Fixed: the order book spread percentage now reads correctly. * Fixed: very large system text sizes no longer break the app's layout. ## Web * **Edit a resting take-profit or stop-loss.** Change the trigger price or size (full or partial) of an existing TP/SL order in place — from the orders table, the TP/SL dialog, or by dragging its line on the chart — without cancelling and re-creating it. * **Set a maximum slippage per market.** Choose the most slippage you'll accept for market orders and position closes on a given market, so an order won't fill beyond the price move you're willing to take. * The portfolio header's change figure now shows your PnL, in dollars and percent, when you're viewing the PnL breakdown — instead of raw equity change. * Shared PnL cards now include the trade's average exit price. * Fixed: clearer transaction error messages — for example, telling you when you don't have enough ETH to cover gas. * Fixed: closing a position now warns you when the close amount exceeds your position size. ## Web * **Dashboard fits your window.** Resize the browser and the trading dashboard re-fits its panels to fill the available height — and remembers the fitted layout across refreshes. * **Pull to refresh on mobile web.** Pull down at the top of the page to reload your data. * Authorize an API key with just your connected wallet — you no longer need to create a trading key first. ## Web * **Close all positions at once.** Close every open perp position in a single action — choose which positions to include, and the confirmation lists them in the order they submit. * Share a PnL card straight from your trade history, now including the trade's average price. * Sign in with a social account and your account menu now shows that provider's icon. * Disabled buttons now say why an action is unavailable — for example asking you to enable trading or reconnect your wallet — instead of a generic message. ## Web * **Isolated margin.** Switch a market to isolated margin mode, and add or remove collateral on an isolated position right from the positions panel. An isolated position's collateral and risk are walled off from the rest of your account, instead of sharing one cross-margin balance across every position. * Fixed: maintenance margin now calculates correctly. ## Web * The after-hours badge now explains itself with a tooltip — including on touch devices — noting that markets outside regular trading hours require more margin, which reduces your available collateral. * Liquidation price and account balance now include unpaid funding, so both read more accurately. ## Web * The default perps layout gives the chart more room — the order form column is slimmer at every desktop size. * Table sort direction is now shown with a single arrow instead of a stacked pair. ## Web * **Flex to fit.** A new control in the trade header resizes your dashboard so the panels fill the screen exactly, and any panel holding more than it can show now fades at the edge so there is visibly more to scroll. * The market search box has a clear button. * Fixed: order book sizes no longer overrun their column — the number of decimals adapts so each size fits. * Fixed: enabling trading could stall when your wallet needed reconnecting; the prompt now asks you to reconnect first. ## Web * **Vaults.** A new Vaults section: browse vaults with their performance sparkline and all-time return, open one for its chart, stats, and positions, deposit or withdraw from the vault panel, and track your own performance separately from the vault's. * **Trade from the price chart.** Add a take profit or stop loss by clicking the chart or dragging the line to the price you want, see your liquidation price marked, read unrealized PnL on the position line, and close a position without leaving the chart. * **Full trading history.** A dedicated, paginated history page for your complete perps activity, reachable with **View all** from the portfolio tables — no longer capped at the most recent rows. * Edit a resting order's price or size inline from the orders table. * API keys can be created for a specific subaccount. * The account summary now shows maintenance margin alongside a corrected margin ratio, and marks when a market is trading after hours. * Order book grouping is now a dropdown instead of a stepper. * Your referral is now shown on the position PnL card. * Fixed: referred volume is now labelled as 30-day. * Fixed: a spurious logout while the wallet connection was being monitored. * Fixed: switching tabs on a small screen no longer keeps the previous tab's scroll position. ## Mobile (Arcus iOS app — pre-launch) * Design polish throughout the app. * Fixed: a double tap on a navigation control could crash the app. ## Web * Unrecognized WebSocket error codes now show a message with the code instead of failing silently. ## Web * Order book unit selection (USD / asset) now persists across sessions. * Order-form fee estimates now always apply the correct maker/taker rate for limit orders that cross the book. ## Mobile (Arcus iOS app — pre-launch) * The Arcus iOS app is open for pre-order on the App Store. * Price charts now display your entry and liquidation prices and mark your past trades directly on the chart. # Changelog API (Testnet) Source: https://docs.arcus.xyz/changelog/testnet API and WebSocket spec changes, anchored to testnet releases **Testnet runs `testnet-v1.13.6-4`.** This page lists API spec changes as they reach testnet. Changes to existing endpoints can show up here before production; new endpoints are listed once they are live on mainnet. Building against production? See the [mainnet changelog](/changelog). ## testnet-v1.13.6-4 **Invite codes can't become referral codes.** [`POST /v1/affiliate/createCode`](/api-reference/referral/create-referral-code) and [`modifyCode`](/api-reference/referral/rename-referral-code) return `409` for a string that is not a valid referral code, such as an existing invite code. ## testnet-v1.13.4 **Invite codes only grant access.** [`POST /v1/affiliate/redeemInvite`](/api-reference/referral/redeem-an-invite-code) by a caller who can already trade returns `409` (`account is already whitelisted — invite code was not consumed`) and the code is not spent. An invite code never moves an existing referral binding. A redemption that would create a cycle in the referral chain returns `409` (`circular referral detected`). **Registering without trading access spends an invite code.** On [`POST /v1/affiliate/registerAffiliate`](/api-reference/referral/register-as-referee), a caller who cannot trade yet can only register by spending one of the code owner's unused invite codes, which grants trading access and shows as used in the owner's invite list. The request returns `403` when the owner does not allow it or invite codes are not available for the caller, `409` when the owner has none left, and `503` when invite codes are unavailable. A caller who can already trade spends no code. A registration cycle returns `409` (`circular referral detected`). ## testnet-v1.13.3 **Referral acknowledgements clarified.** A `202` from [`POST /v1/affiliate/createCode`](/api-reference/referral/create-referral-code) or [`registerAffiliate`](/api-reference/referral/register-as-referee) means no confirmation arrived within the server-side timeout. It is not a rejection, but the referral service can still drop the request, so poll to confirm. A `200` from [`redeemInvite`](/api-reference/referral/redeem-an-invite-code) also covers a caller already bound to the code's owner. ## testnet-v1.13.1 **Invite-code earning rule.** [`GET /v1/affiliate/inviteCodes`](/api-reference/referral/list-the-callers-invite-codes) documents that codes are earned on the caller's own lifetime perps volume plus the all-time volume of their direct referees; deeper levels do not count. **Redemption links by address.** [`POST /v1/affiliate/redeemInvite`](/api-reference/referral/redeem-an-invite-code) links the caller to the code owner's address in the referral tree, and the owner no longer needs a referral code. **Registration grants access.** A successful [`POST /v1/affiliate/registerAffiliate`](/api-reference/referral/register-as-referee) grants the caller trading access if they do not already have it. A new `502` means the access grant or the submit failed: nothing was registered, and the request is safe to retry. ## testnet-v1.13.0 **Referral totals cover the whole downline.** [`GET /v1/affiliate/info`](/api-reference/referral/get-affiliate-info) adds `traderCount`, the number of referees in the T1/T2/T3 downline. Its referred-volume and commission totals now cover that same downline, perps only, and equal the sums of the [`GET /v1/affiliate/referees`](/api-reference/referral/get-referees) rows. **Referee sub-trees.** `GET /v1/affiliate/referees` rows add `volumeAllTime`, each referee's all-time perps volume, and a new `parent` query parameter pages the part of the downline under one referee. **Self-redemption.** Redeeming your own invite code on [`POST /v1/affiliate/redeemInvite`](/api-reference/referral/redeem-an-invite-code) grants trading access and spends the code, but creates no referral binding. ## testnet-v1.11.7 **Order error coverage.** The reference now documents errors these endpoints already returned: * A `502` with `errorType: Transmission` on [`POST /v1/placeOrder`](/api-reference/exchange/place-order), [`batchPlaceOrders`](/api-reference/exchange/place-batch-orders), [`cancelOrder`](/api-reference/exchange/cancel-order), [`batchCancelOrders`](/api-reference/exchange/batch-cancel-orders), [`cancelAllOrders`](/api-reference/exchange/cancel-all-open-orders), [`modifyOrder`](/api-reference/exchange/modify-order) and [`batchModifyOrders`](/api-reference/exchange/batch-modify-orders): the request was valid but never reached the matching engine, so nothing changed and the identical request is safe to retry. * `RejectionReason` adds `ORDER_NOT_FOUND` (a cancel matched no live order), `OPEN_ORDER_CAP_EXCEEDED` (the account is at its open-order maximum) and `EXPIRED` (a resting good-til-time order passed its `goodTilTime`). * The `orders` WebSocket channel's `rejectionReason` is typed as the same enum, with a meaning for each value. * `DUPLICATE_CLIENT_ID` now points to `modifyOrder` for changing a live order in place. ## testnet-v1.11.6 **Open-interest cap documented under its wire name.** The operator-set open-interest cap on [`GET /v1/markets`](/api-reference/public/get-markets) and the `markets` WebSocket channel is `openInterestCapNotional`, which is the name the API has been serving; the reference previously called it `openInterestCap`. It is `null` when no cap is active, and the `OPEN_INTEREST_CAP_EXCEEDED` rejection description now references it. **Spec catch-up with fields already served.** The reference now documents `lastTradePrice` on `MarketInfo` (the most recently matched trade price, omitted until a trade is observed), `address` and `accountIndex` on order rows (`accountIndex` omitted when 0), `oracleEpoch` (Unix microseconds) and `markEpochNanos` on `oraclePrices` rows, and the `oraclePrices` subscribe reply, which carries the latest price for every market with `epoch: 0`. The `account` WebSocket snapshot is described as its own shape — `lastSequenceId` and `accountEquity` in place of REST's `sequenceNumber` and `equity`, with no pending-transfer fields. The `markets` snapshot is a map keyed by `marketId`, where REST returns an array. ## testnet-v1.11.0 **Item counts on list responses.** REST list responses carry a new integer `total`: the number of items in the returned page, not the size of the full result set matching the query. It appears on [`GET /v1/trades`](/api-reference/public/get-recent-public-trades), `/v1/candles`, `/v1/orders`, `/v1/openOrders`, `/v1/fills`, `/v1/positions`, `/v1/funding`, `/v1/fundingRates`, and `/v1/accountTransferUpdates`. It is REST-only: the `positions` WebSocket snapshot doesn't carry it. ## testnet-v1.10.13 **Per-market dead man's switch.** [`POST /v1/scheduleCancel`](/api-reference/exchange/schedule-cancel-all-dead-mans-switch) (and the WebSocket `scheduleCancel` action) accepts an optional `marketId`. Omit it for the existing account-wide switch, which cancels across every market; set it to arm a switch that cancels only that market's orders. A per-market switch is independent of the account-wide switch and of other per-market switches on the same subaccount, and the response echoes `marketId` and `marketDisplayName` when the switch is per-market. A wallet may hold only a bounded number of armed switches across all its subaccounts and markets: arming one beyond that limit returns `429`, while refreshing an already-armed switch is always allowed. **API keys scoped to all subaccounts.** [`POST /v1/createApiKey`](/api-reference/onboarding/create-api-key) and [`POST /v1/revokeApiKey`](/api-reference/onboarding/revoke-api-key) accept `accountIndex: 255`, which scopes one key to every subaccount of the address instead of a single index (`0`–`9`); values `10`–`254` are rejected. Like any non-zero index, `255` must be signed with the `accountIndex`-bearing EIP-712 type. Key rows on [`GET /v1/apiKeys`](/api-reference/onboarding/get-api-keys) and the create/revoke responses carry a new `allSubaccounts` boolean — use it as the scope discriminator; `accountIndex` is present only on single-subaccount keys. ## testnet-v1.10.12 **Rename a referral code.** New [`POST /v1/affiliate/modifyCode`](/api-reference/referral/rename-referral-code) renames the authenticated affiliate's code in place. Existing referees stay attached, since the referral link is by account rather than by code string. The affiliate must already hold a code (`404` otherwise), and the new string must be free: `409` when it equals the current code, is owned by another affiliate, or was previously retired. ## testnet-v1.10.9 **Spec correction: affiliate `volume30d`.** The spec for [`GET /v1/affiliate/info`](/api-reference/referral/get-affiliate-info) now names the affiliate's own trailing-30-day notional fill volume field `volume30d`, matching what the API already returned; it was previously documented as `rollingVolume`. No client change is needed. The separate `rollingVolume` on the `/v1/accountStats` reads is a different field. ## testnet-v1.10.7 **Look up a single trader on the leaderboard.** [`GET /v1/leaderboard`](/api-reference/public/get-the-all-traders-leaderboard) now accepts an `address` to return just that trader's row — its volume, fees, and realized PnL over the window, plus its `rank` on the `sortBy` column wherever it sits on the board (not only within the top-N). Every leaderboard row also carries that `rank` field, computed on the active `sortBy` column in the same query so a row's columns stay mutually consistent. When `address` is set, `limit` is ignored and an address that did not trade in the window returns an empty `entries` list. ## testnet-v1.10.4 **Affiliates: all-time referred volume.** [`GET /v1/affiliate/info`](/api-reference/referral/get-affiliate-info) adds `totalReferredVolumeAllTime` — the combined lifetime notional fill volume of all of the affiliate's current direct (T1) referees, in quote quantums (1e9 = \$1). Same definition as `totalReferredVolume30d` but with no time window. Reads `0` if the underlying rollup is momentarily unavailable. ## testnet-v1.10.2 **Get a fill by trade id.** [`GET /v1/fill/{tradeId}`](/api-reference/public/get-fill-by-id) returns the single fill your account owns for a given `tradeId` (pass `?address=`, optional `accountIndex`) — the point-lookup companion to [`GET /v1/fills`](/api-reference/public/get-fills). No authentication required. ## testnet-v1.10.1 **`closedPnl` now nets the fill fee.** Across [`GET /v1/fills`](/api-reference/public/get-fills) and the fills/`account` WebSocket rows, `closedPnl` is redefined as the cost-basis release (`old borrowedCapital − new borrowedCapital − Δsize × price`) minus the fill's fee. Opening and same-direction add legs now report `−fee` (previously `"0"`). ## Updates **New rejection reason `MISSING_MARK_PRICE`.** The market has no live mark price — a transient oracle gap — so the mark-based solvency gate cannot be evaluated. The move is not applied; retry once pricing resumes. ## New features **Invite codes are switched on (testnet only).** [`GET /v1/affiliate/inviteCodes`](/api-reference/referral/list-the-callers-invite-codes) and [`POST /v1/affiliate/redeemInvite`](/api-reference/referral/redeem-an-invite-code) — long present in the spec but answering `503` — now answer on testnet; mainnet still returns `503 {"error":"Invite codes not available yet"}` and the [mainnet changelog](/changelog) will announce that rollout. One invite code is earned per \$1M of lifetime perps volume, and codes materialize lazily — a batch of newly earned codes may take more than one read to appear (`materializationPending`). Codes are bearer secrets that grant perps trading access, so the list read is the one **signed** affiliate read: full header triple, message `X-Timestamp + inviteCodes`. Redemption is single-use and atomic — two callers racing on one code get exactly one win and one `409`. The same release settles redemption semantics for callers who are *already referred*: redeeming moves the attribution to the invite code's owner **only when the code is what grants access** — a caller who can already trade and has a referrer gets `409` and the code is not spent. A move is forward-only (the previous affiliate keeps commission already earned and stops accruing) and is confirmed asynchronously, so that path answers `202` rather than `200`. The redemption response also no longer names the code's owner, and [`GET /v1/affiliate/myReferrer`](/api-reference/referral/get-my-referrer) redacts the same address — a referral binding is readable without disclosing the wallet behind it. ## New features **Subaccounts across the WebSocket surface.** Subscriptions to the account-scoped channels — [`account`](/api-reference/channels#account), [`positions`](/api-reference/channels#positions), [`orders`](/api-reference/channels#orders), [`userFills`](/api-reference/channels#userfills), [`funding`](/api-reference/channels#funding), [`accountTransferUpdates`](/api-reference/channels#accounttransferupdates), and [`accountAttributeUpdates`](/api-reference/channels#accountattributeupdates) — take an optional `accountIndex` (0–9). Omit it for subaccount 0, which is what every client written before the field existed keeps getting. It is part of subscription identity: each subaccount is its own subscription with its own snapshot and stream, so one connection may hold several, and `unsubscribe` must repeat the same `accountIndex` — omitting it drops only subaccount 0. Every frame of an account-scoped channel now echoes `accountIndex`, including when it is 0, on `subscribed`, `unsubscribed`, `degraded`, and `channel_data` alike; that is what makes several subaccounts of one address demultiplexable on a single connection, since `id` alone is the master address and identical across all of them. Addresses in `id` come back lowercase whatever spelling was sent, so compare them case-insensitively, and an encoded core account id may be used instead — it already carries its own subaccount index, and supplying `accountIndex` alongside it is allowed only if the two agree. Supplying `accountIndex` on a channel that is not account-scoped is an error rather than an ignored field, as is a value outside 0–9: an out-of-range subaccount can never carry traffic, so accepting one would return a permanently silent subscription. ## Updates **Per-account sequence numbers on positions and orders.** Position and order rows carry `sequenceNumber` — the per-account sequence of the engine event that last wrote that row, the same counter as `AccountUpdate.sequenceNumber` — so a row can be ordered against account updates without inferring it from arrival order. The [`orders`](/api-reference/channels#orders) snapshot now carries `lastSequenceId` next to `openOrders` and `recentClosedOrders`, matching the [`positions`](/api-reference/channels#positions) snapshot, and streaming `PositionUpdate` envelopes stamp it too, so a client can tell exactly where its snapshot sits in the account's event sequence and discard the updates it already contains. Both are WebSocket-side; REST omits `sequenceNumber` when unset. **`marginUsed` reads by margin mode.** On position rows the field now depends on the row's `marginMode`. **CROSS:** the initial margin required at the current mark — |size| × mark × initial-margin fraction — so it moves as the mark moves. **ISOLATED:** the walled-off isolated-leg pool, which does *not* track IMR: it moves only when collateral actually moves, growing one-for-one with [`POST /v1/adjustIsolatedMargin`](/api-reference/exchange/add-or-remove-margin-on-an-isolated-mode-position). The full isolated-leg cash balance is derivable as `marginUsed + borrowedCapital`. ## Updates **New modify rejection reason `MODIFY_SIZE_ALREADY_FILLED`.** Under total-size modify semantics — where the size on a [`POST /v1/modifyOrder`](/api-reference/exchange/modify-order) is the order's new *total* size, filled quantity included — a new total at or below the quantity already filled leaves nothing to rest. The original order is canceled (a `CANCELED` update precedes the rejection) and the modify is rejected with this reason. Use [`POST /v1/cancelOrder`](/api-reference/exchange/cancel-order) when the intent is to stop the unfilled remainder. ## Updates **Market filter on the remaining reads and account channels.** An optional `market` — display ticker (`BTC-USD`, case-insensitive) or numeric market id (`1`) — now restricts [`GET /v1/prices`](/api-reference/public/get-live-prices-for-all-markets), [`GET /v1/mids`](/api-reference/public/get-all-mid-prices), [`GET /v1/positions`](/api-reference/public/get-positions), and [`GET /v1/leverages`](/api-reference/public/get-leverage-and-margin-mode-for-an-account-across-all-markets) to a single market; an unresolvable value returns HTTP 400 rather than an unfiltered response. On `prices` and `mids` a filter naming an **offline** market returns an empty object, matching how the unfiltered response omits offline markets; an account with no position in the requested market returns empty `positions`, not a 404. The same filter is accepted on the WebSocket [`positions`](/api-reference/channels#positions), [`orders`](/api-reference/channels#orders), and [`userFills`](/api-reference/channels#userfills) subscriptions, where it applies to the snapshot, the live frames, and the post-snapshot catch-up alike, so the baseline and the diffs always describe the same markets. It is part of subscription identity — one connection may hold several market-scoped views of the same account, and `unsubscribe` must repeat the market it subscribed with. Supplying `market` on any other channel is an error, not an ignored field. Frames on those three channels now carry `market` (the resolved ticker) so a client holding several views can demultiplex, and the `subscribed` / `unsubscribed` acks echo the filter. The WebSocket `get` RPCs take it too, on `fills`, `orders`, `positions`, `leverages`, `mids`, and `prices`. **`adjustIsolatedMargin` is confirmed on the `positions` channel.** Adding or removing isolated margin through [`POST /v1/adjustIsolatedMargin`](/api-reference/exchange/add-or-remove-margin-on-an-isolated-mode-position) now emits a `PositionUpdate` on the [`positions`](/api-reference/channels#positions) channel, carrying the originating `requestId` so it can be correlated with the 202 `ACK`, and `marginMode: ISOLATED`. Every position row — snapshot and streaming — now includes `marginMode` (`CROSS` or `ISOLATED`), so the collateral mode of a position is readable without a REST round-trip. **`isolated` and `marginMode` are always present on leverage entries.** On the [`accountAttributeUpdates`](/api-reference/channels#accountattributeupdates) channel, `type: "leverage"` entries previously carried the margin mode only when a mode change was part of the same `setLeverage` request. Both fields are now always populated: in snapshot frames they reflect the persisted state, and in streaming frames the account's margin mode after the triggering event, defaulting to `CROSS` if the mode lookup fails. ## New features **Adjust margin on an isolated position.** [`POST /v1/adjustIsolatedMargin`](/api-reference/exchange/add-or-remove-margin-on-an-isolated-mode-position) moves collateral between the cross bucket and an isolated-mode position's leg. `amount` is a dollar decimal string — positive adds margin to the isolated leg, negative removes it, and zero returns HTTP 400; send dollars, not quote quantums. The `(account, market)` must already be in isolated mode with an open position. Like the other engine-confirmed writes it carries a lifecycle `status` separate from the HTTP code: `APPLIED` (200), `ACK` (202, forwarded but unconfirmed — watch the [`positions`](/api-reference/channels#positions) channel), or `REJECTED` (422) with a `rejectReason` of `UNKNOWN_MARKET`, `INVALID_AMOUNT`, `NOT_ISOLATED`, `NO_OPEN_POSITION`, or `UNDERCOLLATERALIZED`. The response echoes `newIsolatedMarginQuoteBalance`, also in dollars. **Dead man's switch.** [`POST /v1/scheduleCancel`](/api-reference/exchange/schedule-cancel-all-dead-mans-switch) arms, refreshes, or disarms a per-subaccount deadline; if it passes without a refresh the gateway fires `cancelAllOrders` for that subaccount. Pass an absolute epoch-microsecond `time` to arm or refresh, and omit it (or send null) to disarm. Lead time must be between 5 seconds and 5 minutes, and successful auto-fires are capped at 10 per UTC day per subaccount — past which the endpoint returns HTTP 429. A 503 means the switch is **not** armed (cancel path disabled or store unavailable), so retry rather than assuming coverage. ## Updates **`netDeposits` semantics.** The field on [`GET /v1/account`](/api-reference/public/get-account) and the [`accounts`](/api-reference/channels#accounts) channel is documented as lifetime net deposits — cumulative inflows minus outflows across every *applied* account transfer, including deposits, withdrawals, and internal and self transfers between your own account indices, with rejected transfers excluded. Unlike `equity` and `freeCollateral` it never moves with oracle prices; only a transfer changes it. Subtract it from `equity` for all-time PnL, on the same basis [`GET /v1/portfolio`](/api-reference/public/get-portfolio-history) uses. ## Updates **`status` and `market` filters on order history.** [`GET /v1/orders`](/api-reference/public/get-order-history) and [`GET /v1/openOrders`](/api-reference/public/get-open-orders) accept an optional `status` alongside `market`, which separates orders resting on the book (`OPEN`) from TPSLs parked on the untriggered book (`UNTRIGGERED`). Values are `OPEN`, `UNTRIGGERED`, `FILLED`, `CANCELED`, `REJECTED`, `LIQUIDATED`, and `ADL`, case-insensitive; `status` accepts either a JSON array (`["OPEN","CANCELED"]`) or a comma-separated string (`"OPEN,CANCELED"`), and an unrecognized value returns HTTP 400. It matches an order's *current* status, not any status it held earlier. Both filters apply before the row cap, so a filtered page still carries up to `limit` orders. The same filters are available on the WebSocket `orders` RPC, where they apply to both the `openOrders` and `recentClosedOrders` halves of the result. **`from`/`to` on funding are microseconds.** The bounds on [`GET /v1/funding`](/api-reference/public/get-funding-payments) and [`GET /v1/fundingRates`](/api-reference/public/get-market-funding-rates) are epoch **microseconds**, matching the unit of the timestamp field each response reports — so a value read off one page is a valid bound for the next. The spec previously described them as epoch milliseconds; a millisecond value is now far below the accepted minimum. **WebSocket error codes completed.** Ten codes the gateway already returned were missing from the published enum: `TimestampReused`, `ModifiesArrayEmpty`, `ModifiesArrayTooLarge`, `RateLimited`, and `AddressNotOnWhitelist` on `post`, and `MarketOffline`, `GetLeveragesFailed`, `GetRateLimitFailed`, `LeverageHandlerUnavailable`, and `RateLimitHandlerUnavailable` on `get`. No behavior change — clients exhaustively matching on the enum can now recognize all of them. ## New features **Batch order modification.** [`POST /v1/batchModifyOrders`](/api-reference/exchange/batch-modify-orders) reprices or resizes up to 100 open orders in one request. Every row must share the same `(address, accountIndex)` — a batch is scoped to a single subaccount — and each row carries its own `signature` over the same typed canonical modify payload (`op` 3) a standalone [`POST /v1/modifyOrder`](/api-reference/exchange/modify-order) signs, so the signed bytes are identical either way. Like the other batch routes it authenticates per element rather than with a verified envelope signature, and failures are per-row: a row rejected for validation or signature reasons comes back with `status: ERROR` and an `error` message while the remaining rows still reach the matching engine. The same operation is available over WebSocket. **Isolated margin mode.** [`POST /v1/setLeverage`](/api-reference/exchange/set-leverage-for-a-market) accepts an optional `isolated` flag that sets the margin mode for a `(account, market)` alongside the leverage — `true` for isolated, `false` for cross, omitted to leave the mode unchanged. Switching cross → isolated is rejected with `HAS_OPEN_POSITION` when a position is already open in that market; isolated → cross and no-op changes are always accepted. [`GET /v1/leverages`](/api-reference/public/get-leverage-and-margin-mode-for-an-account-across-all-markets) now returns the margin mode (`isolated` / `marginMode`) next to the effective leverage for every market, and the [`accountAttributeUpdates`](/api-reference/channels#accountattributeupdates) channel folds the margin-mode outcome into the same entry via `marginModeStatus` and `marginModeRejectReason`, including on the `leverageReject` entry emitted when the engine refuses a request. ## Updates **Modify an order by client id.** `ModifyOrderRequest` accepts `clientId` as an alternative to `orderId`, resolved engine-side against the account's client-id index — so an order can be modified before the place response carrying its server-generated id has arrived. Supply exactly one: setting both is rejected, and so is setting neither. **`side` filter on order and fill history.** [`GET /v1/orders`](/api-reference/public/get-order-history) and [`GET /v1/fills`](/api-reference/public/get-fills) accept an optional case-insensitive `side` (`BUY` / `SELL`) to restrict results to a single trade direction. Omit it to return both sides; an unknown value returns HTTP 400. **Snapshot size control on `userFills`.** Subscribing to the [`userFills`](/api-reference/channels#userfills) channel accepts an optional `nFills` (1–500, default 500) that bounds the initial snapshot. It is ignored on other channels, and live `channel_data` frames are unaffected. ## Updates **Market filter on funding payments.** [`GET /v1/funding`](/api-reference/public/get-funding-payments) accepts an optional `market` to restrict results to a single market, given as either the display name (e.g. `BTC-USD`, case-insensitive) or the numeric market id. Omit it to return every market; an unresolvable value returns HTTP 400. ## New features **Scoped API keys and API-key-signed withdrawals.** [`GET /v1/apiKeys`](/api-reference/onboarding/get-api-keys) entries carry an optional `permissions` array describing scopes granted beyond the default. Every key can read and trade; the `withdraw` scope additionally authorizes [`POST /v1/withdraw`](/api-reference/exchange/submit-withdrawal) using the `X-API-Key` / `X-Timestamp` / `X-Signature` header triple with an Ed25519 signature over the ordersign `WithdrawV1` typed canonical payload (`op` 5), in which case the body `signature` field is ignored. The key must be bound to exactly the requested `(ethereumAddress, accountIndex)`. Replay protection matches the wallet-signed mode: the `X-Timestamp` drift window plus a single-use `nonce` per `(ethereumAddress, accountIndex)`, with HTTP 409 on reuse. The scope is grantable only through the operator's provisioning flow — keys created via the public [`POST /v1/createApiKey`](/api-reference/onboarding/create-api-key) are always trade-only and are rejected with HTTP 403. The original wallet-signed EIP-712 mode is unchanged, and withdraw-to-self remains the only supported destination on both modes. ## Updates **`GET /v1/apiKeys` spans every subaccount by default.** [`GET /v1/apiKeys`](/api-reference/onboarding/get-api-keys) has no default `accountIndex` — omitting it lists the keys of every subaccount, each entry tagged with the `accountIndex` it belongs to, so a listing can be grouped client-side without one request per subaccount. Pass `accountIndex` to scope the listing to a single subaccount. Unlike other account-scoped reads, an omitted value is not the same as `accountIndex=0`, and a non-integer or out-of-range value returns HTTP 400 rather than silently falling back to 0. The response is all-or-nothing across subaccounts: an unreadable subaccount surfaces as HTTP 500 rather than a short list that would look like a revoked key. ## Updates **Market and role filters on order and fill history.** [`GET /v1/orders`](/api-reference/public/get-order-history) and [`GET /v1/fills`](/api-reference/public/get-fills) accept an optional `market`, and [`GET /v1/fills`](/api-reference/public/get-fills) additionally accepts a case-insensitive `role` to restrict results to a single liquidity role. Market filters across the API — including [`GET /v1/markets`](/api-reference/public/get-markets) — now accept either the display name (case-insensitive) or the numeric market id, and an unresolvable value returns HTTP 400. **Multiple orderbook aggregations per market on one connection.** A connection may subscribe to the [`l2Orderbook`](/api-reference/channels#l2orderbook) channel more than once for the same market as long as each subscribe uses a distinct (`sigFigs`, `roundStep`) pair. `subscribed`, `channel_data`, and `unsubscribed` frames echo those fields — omitted for full-precision views — so a client holding several views of one market can demux them. Unsubscribing must repeat the same pair to drop one view without affecting the others. ## New features **API-key replay protection & subaccount scoping.** [`POST /v1/createApiKey`](/api-reference/onboarding/create-api-key) and [`POST /v1/revokeApiKey`](/api-reference/onboarding/revoke-api-key) accept an optional `nonce` (single-use per `(address, accountIndex)`) for replay protection and an `accountIndex` (0–9) that scopes the key to a subaccount. Both are bound into the EIP-712 signature: a non-zero `accountIndex` must use the new accountIndex-bearing `CreateApiKey`/`RevokeApiKey` type, and including a `nonce` requires the combined type that binds both. Send the nonce as nanoseconds since the Unix epoch (accepted within −48h/+24h; a legacy opaque UUID also works); a replayed nonce returns HTTP 409. Index 0 still accepts the original index-less type and the legacy EIP-191 fallback during the migration window. ## Updates **New order-rejection reason `POSITION_SIZE_CAP_EXCEEDED`.** Orders, modifies, or fills that would exceed a market's per-market position notional cap are rejected with `POSITION_SIZE_CAP_EXCEEDED` — distinct from `UNDERCOLLATERALIZED`, since a fully funded account can still hit it. ## Breaking changes **Time-window bounds are now microseconds.** The `from`/`to` query parameters on [`GET /v1/openOrders`](/api-reference/public/get-open-orders), [`GET /v1/orders`](/api-reference/public/get-order-history), [`GET /v1/fills`](/api-reference/public/get-fills), [`GET /v1/trades`](/api-reference/public/get-recent-public-trades), and [`GET /v1/accountTransferUpdates`](/api-reference/public/get-account-transfer-updates) are now epoch **microseconds**, matching the `createdAt` / `updatedAt` / `timestamp` precision these endpoints already report — so a value read from one page is a valid bound for the next with no conversion. Millisecond- or second-scale values are rejected with HTTP 400 (the server requires at least `1e14`); a millisecond bound read as microseconds lands in 1970 and silently returns nothing. Clients that previously sent milliseconds must update. ## Updates **`GET /v1/openOrders` is now bounded and paginated.** [`GET /v1/openOrders`](/api-reference/public/get-open-orders) accepts `limit` (1–1000; default and maximum both 1000, larger values silently clamped) plus `from` / `to` (epoch microseconds) to bound the window on `createdAt`. Responses stay newest-first; page backward by sending the oldest `createdAt` you received as the next request's `to`, deduplicating by `orderId` since the inclusive bound overlaps page edges. An account can hold up to 10,000 live orders, so more than one page may be needed. ## Breaking changes **Deprecated `*Bps` fee-tier aliases removed.** The `makerFeeBps`/`takerFeeBps` fields on [`GET /v1/feetiers`](/api-reference/public/get-fee-tier-table), [`GET /v1/account/stats`](/api-reference/public/get-account-stats), and `feeTierConfig` entries on [`exchangeAttributeUpdates`](/api-reference/channels#exchangeattributeupdates) — along with the spot schedule's `maker_fee_bps`/`taker_fee_bps` — are gone, completing the ppm rename announced in `testnet-v1.2.31`. Use `makerFeePpm`/`takerFeePpm` (and `maker_fee_ppm`/`taker_fee_ppm`); the values are unchanged. ## New features **Orderbook price aggregation.** [`GET /v1/l2OrderBook/{market}`](/api-reference/public/get-l2-orderbook-snapshot) and the [`l2Orderbook`](/api-reference/channels#l2orderbook) channel accept an optional `sigFigs` (2–5) to bucket price levels to that many significant figures, plus `roundStep` (1, 2, or 5) to refine the finest bucket when `sigFigs` is 5. Bids round down and asks round up so the displayed spread is never tighter than the true book, merged sizes are summed, and aggregation is applied before `nLevels` truncation. Omit `sigFigs` for full precision. ## Updates **Order-size bounds on markets.** [`GET /v1/markets`](/api-reference/public/get-markets) and the [`markets`](/api-reference/channels#markets) channel add `minOrderSize` and `maxOrderSize` (base-asset units). `maxOrderSize` is a per-order security gate rather than a dust filter — reduce-only orders are not exempt. ## Updates **Explicit RTH-transition events on marketAttributes.** The [`marketAttributes`](/api-reference/channels#marketattributes) channel's `boundEvent` adds `exitRth` / `enterRth` for Regular Trading Hours boundary crossings, and is now present on every delta entry — only snapshots omit it. `boundSide` is likewise always present on deltas (`null` on RTH transitions and VWAP seals). **Portfolio PnL rebased per timeframe.** `pnlHistory` on [`GET /v1/portfolio`](/api-reference/public/get-portfolio-history) now rebases each timeframe so its first point is `0` — every point is the trading + funding PnL earned since the start of that timeframe, and lifetime PnL is the last point of the `all` timeframe. The curve still does not jump on deposits or withdrawals. ## New features **Off-hours market attributes over WebSocket.** New [`marketAttributes`](/api-reference/channels#marketattributes) channel streams per-market Regular Trading Hours / off-hours trading-band **state** for every online market — `isOutsideRth`, the sealed `currentSettlementPrice` anchor, both current trading-band edges and their next-level edges, and per-side expansion-zone state with its clocks. It is a global channel (no `id`): a full per-market snapshot on subscribe (`isSnapshot: true`), then a single-market update on each RTH boundary crossing, VWAP seal, and off-hours bound event — each carrying that market's complete post-event block plus a `timestamp` and `marketSequenceNum`. Fields that do not apply are `null` (24/7 crypto markets are included with `isOutsideRth: false` and all band fields `null`). Supersedes the `marketAttributes` entry variant on [`exchangeAttributeUpdates`](/api-reference/channels#exchangeattributeupdates). ## Updates **Docs default to mainnet.** The API playground and the curl / Python / JavaScript code samples throughout the [API reference](/api-reference/introduction) now target the production host — `https://api.arcus.xyz` (REST) and `wss://api.arcus.xyz/v1/ws` (WebSocket) — instead of testnet. Testnet (`api.testnet.arcus.xyz`) remains fully available and is still selectable from the server dropdown on every endpoint and channel page; the two environments share identical paths, payloads, and signing, so a sample runs unchanged against either by switching the host. This supersedes the earlier note (below) that samples and the playground targeted testnet. ## Updates **Forced closures are marked on fills.** Fill rows add a `liquidation` object — `method` (`LIQUIDATION` or `ADL`) plus `liquidatedUser` — present on the liquidated account's leg of a liquidation and the deleveraged counterparty's leg of an auto-deleverage; absent on voluntary fills. The marker is persisted, so [`GET /v1/fills`](/api-reference/public/get-fills) and [`userFills`](/api-reference/channels#userfills) snapshots return it too, and it is the supported way to detect forced closures (the `liq:` order-ID prefix is an internal detail). On the liquidated leg, `fee` carries the liquidation penalty paid to the insurance fund and `closedPnl` reads `"0"` by design — the realized loss is already reflected in `netQuoteBalance` on the accompanying account update. ## New features **Account transfer events over WebSocket.** New [`accountTransferUpdates`](/api-reference/channels#accounttransferupdates) channel streams deposits, withdrawals, internal transfers, and referral claims for an account: a newest-first snapshot on subscribe, then one event per transfer as the engine applies it. Amounts are decimal strings in quote currency, and `globalSequenceId` gives strict ordering. The REST equivalent is [`GET /v1/accountTransferUpdates`](/api-reference/public/get-account-transfer-updates). **Predicted funding over WebSocket.** New [`predictedFunding`](/api-reference/channels#predictedfunding) channel streams the predicted next-hour funding rate per market as a `{market, rate1h}` payload, refreshed each time the funder recomputes the prediction. Unlike most channels, payloads are not wrapped in an `isSnapshot` envelope. ## Updates **Subaccount selector on all account-scoped reads.** The optional `accountIndex` query parameter (0–9, default 0) previously accepted only by [`GET /v1/fills`](/api-reference/public/get-fills) now applies across account-scoped endpoints: `GET /v1/account`, `/v1/positions`, `/v1/openOrders`, `/v1/orders`, `/v1/order/{orderId}`, `/v1/funding`, `/v1/accountTransferUpdates`, `/v1/portfolio`, `/v1/rateLimit`, `/v1/leverages`, and `/v1/apiKeys`. Values above 9 return `400`. **Faster orderbook snapshots.** The [`l2Orderbook`](/api-reference/channels#l2orderbook) full-snapshot cadence tightened from \~500 ms to \~200 ms. ## Updates **24-hour high/low on markets.** [`GET /v1/markets`](/api-reference/public/get-markets) and the [`markets`](/api-reference/channels#markets) channel add `high24h` and `low24h` — highest and lowest transaction price over the trailing 24-hour window, computed from closed 1-minute candles with at least one trade and omitted when no trades occurred in the window. ## Updates **Fee-tier fields renamed to ppm.** `makerFeeBps`/`takerFeeBps` become `makerFeePpm`/`takerFeePpm` in the fee-tier shape returned by [`GET /v1/feetiers`](/api-reference/public/get-fee-tier-table), [`GET /v1/account/stats`](/api-reference/public/get-account-stats), and `feeTierConfig` entries on [`exchangeAttributeUpdates`](/api-reference/channels#exchangeattributeupdates); the spot fee schedule likewise renames `maker_fee_bps`/`taker_fee_bps` to `maker_fee_ppm`/`taker_fee_ppm`. The values were always parts-per-million despite the old names. The `*Bps` fields remain as deprecated aliases (identical values) for one release cycle — migrate to the `*Ppm` names now. ## Breaking changes **Testnet withdraw/transfer signing domain rotated.** The July 22 testnet reset rotated the `BridgeVault` `verifyingContract` for [`POST /v1/withdraw`](/api-reference/exchange/submit-withdrawal) and [`POST /v1/transfer`](/api-reference/exchange/submit-internal-transfer) to `0x9a6d3499149fea853efe775a3577701539054eaf`; signatures against the previous address are rejected with `401`. This address rotates on every testnet reset — read the current value from the withdraw endpoint's per-environment table. The [funding guide](/guides/fund-testnet-account) contract addresses rotated in the same reset. ## Updates **Perp candle timestamp bounds declared in the schema.** [`GET /v1/candles`](/api-reference/public/get-ohlcv-candles) `from`/`to` (Unix **microseconds**) now declare `minimum: 1e14`, matching what the handler always enforced. ## Updates **API key create/revoke signing migrates to EIP-712.** [`POST /v1/createApiKey`](/api-reference/onboarding/create-api-key) and [`POST /v1/revokeApiKey`](/api-reference/onboarding/revoke-api-key) are now authenticated by an **EIP-712 typed-data signature** (`eth_signTypedData_v4`) instead of an EIP-191 `personal_sign`. Both schemes are accepted during the migration window; EIP-191 is deprecated and will be rejected once the window closes. Always send `validUntil` explicitly and sign that value — omitting it makes the server verify against its own default. See each endpoint description for the domain, types, and signing examples. ## New features **Internal transfers between subaccounts.** New [`POST /v1/transfer`](/api-reference/exchange/submit-internal-transfer) moves collateral between two account indexes of the **same wallet** — in v0 there is no cross-wallet recipient. The request is signed with an EIP-712 typed-data signature over the `Transfer` payload (`eth_signTypedData_v4`) and carries a single-use nanosecond-timestamp `nonce` for replay protection (a repeated nonce returns `409`). `amount` is a quote-quantum integer string (1e9 = \$1); unlike withdrawals it never settles on-chain, so it is not constrained to collateral-base-unit multiples. Acceptance is asynchronous: the response carries `status: PENDING` and a `transferId`, with the terminal outcome delivered on the account transfer update stream — insufficient free collateral rejects there with `REJECTED_INSUFFICIENT_COLLATERAL`. **Per-market open-interest caps.** [`GET /v1/markets`](/api-reference/public/get-markets) and the `markets` WebSocket channel add `openInterestCap` — the operator-set maximum open interest as a USD-notional decimal string (present only while a cap is active). While a market is at its cap, the engine rejects OI-increasing fills with the new rejection reason `OPEN_INTEREST_CAP_EXCEEDED`; OI-neutral and OI-reducing trades (closes, and opener-vs-closer pairings) still match. Fills executed before the halt stand, and the cap clears as open interest falls or the operator raises it. ## Updates **Signed withdrawals are single-use.** [`POST /v1/withdraw`](/api-reference/exchange/submit-withdrawal) now rejects a replayed `(ethereumAddress, accountIndex, nonce)` with `409` — resubmitting an identical signed body (or any body reusing the same `nonce`) no longer queues a second withdrawal. Use a fresh nonce for each new withdrawal; the recommended nonce is the current time in nanoseconds since the Unix epoch. ## Updates **API keys are scoped to `(address, publicKey)`.** An Ed25519 public key is globally unique across accounts: [`POST /v1/createApiKey`](/api-reference/onboarding/create-api-key) returns `409 Conflict` when the `publicKey` is already registered to a different account (including expired-but-unrevoked keys), and only the owning account may register or renew it. [`POST /v1/revokeApiKey`](/api-reference/onboarding/revoke-api-key) returns `404` when the key is not registered to the caller's account — including cross-account revoke attempts — with no side effects. ## New features **Sectioned account stats.** [`GET /v1/account/stats`](/api-reference/public/get-account-stats) is now sectioned so clients fetch only what they need via the `include` query parameter: `feeTier` (the current fee tier — cheap, right shape for frequent fee-rate polling) and `volumes` (notional fill volume and fees paid over the 14-day fee-tier rolling window and all-time — backed by analytics storage, fetch on demand). The `volumes` section also accepts opt-in fixed windows via `windows=24h,7d,30d`. **Market filter on fills.** [`GET /v1/fills`](/api-reference/public/get-fills) adds an optional `market` query parameter to restrict fill and trade history to a single market, alongside the existing `from`/`to` time bounds and `accountIndex` subaccount selector. ## Breaking changes **Withdraw EIP-712 signing domain rotated; production domain published.** The `BridgeVault` `verifyingContract` for [`POST /v1/withdraw`](/api-reference/exchange/submit-withdrawal) changed on testnet (`0x317253c5c134c3ac60e84c4ca069c9f588030480`) — withdrawal signatures produced against the previous addresses are rejected. The production domain is now published: `chainId` `4663`, `verifyingContract` `0x14b107cf534239c59571b066cb6497a321da897c`. ## Updates **Modify order is no longer marked "coming soon".** The stale caveat claiming [`POST /v1/modifyOrder`](/api-reference/exchange/modify-order) returns `501 Not Implemented` is removed — the endpoint has been live for some time. The docs now also spell out the queue-priority contract: a modify is applied **in place** (keeping queue priority) only when it leaves the price unchanged and reduces size; any price move or size increase is an atomic **cancel + replace** that loses priority. On the `orders` WebSocket channel a non-crossing modify arrives as a single update; a crossing modify arrives as a fill followed by a `PLACED` (or `REJECTED`) — no separate cancel/place event pair is emitted. **`goodTilTime` is required on every order — the schema now says so.** The order schema previously required `goodTilTime` only for resting time-in-forces (`GTT`, `ALO`) and described it as "ignored" for `IOC`/`FOK`; the API has always rejected orders without it. It is now documented as unconditionally required on [`POST /v1/placeOrder`](/api-reference/exchange/place-order), the batch variants, and the WebSocket order payloads: for `IOC`/`FOK` it acts as a mandatory replay-protection timestamp, and it must be at least one month in the future. **Time-in-force: submit `GTT`, read `GTC`.** Order **submissions** must use `GTT` (Good Till Time) for resting orders, but order **reads** — REST order queries and the `orders` WebSocket channel — currently report resting orders as `GTC`, the legacy name for the same bucket. A new `TimeInForceResponse` schema documents the read-side enum; treat `GTC` and `GTT` as equivalent when reading. **`modifyOrder` signing payload documented in full.** The typed canonical payload for `modifyOrder` now shows its complete field set — `{"ad","ai","c","ct","g","id","m","op":3,"p","q","r","s","t","v"}`. `id` is always required, and `g`/`r`/`s`/`t` echo the resting order's `goodTilTime` (in nanoseconds), `reduceOnly`, side, and time-in-force. The modify request body now also requires `reduceOnly` and `goodTilTime` to be echoed explicitly (a differing `goodTilTime` performs a cancel-replace with the new expiry). **Subaccount authorization on order writes.** The `403` responses on [`POST /v1/placeOrder`](/api-reference/exchange/place-order), [`POST /v1/cancelOrder`](/api-reference/exchange/cancel-order), the batch variants, and [`POST /v1/modifyOrder`](/api-reference/exchange/modify-order) now also cover the case where `accountIndex` does not match the API key's authorized subaccount. ## Breaking changes **Off-hours trading-band fields reshaped.** On [`GET /v1/markets`](/api-reference/public/get-markets) and the `markets` WebSocket channel, the off-hours band is now reported as **actual USD price boundaries** rather than multiplier strings. `currentTradingBound` / `nextTradingBound` are removed; use `upperTradingBound` / `lowerTradingBound` (plus `nextUpperTradingBound` / `nextLowerTradingBound`) and the new per-side expansion-zone state — `isUpperInExpansionZone` / `isLowerInExpansionZone`, `upperZoneEnteredAt` / `lowerZoneEnteredAt`, and `upperExpectedExpansionAt` / `lowerExpectedExpansionAt`. Each side's band now expands independently on the `0.5×` → `1×` → `2×` → `4×` ladder. **Rejection reason consolidated.** `ORDER_WILL_TAKE_LIQUIDITY_DURING_MARKET_HALT` and `PRICE_WILL_EXCEED_MAXIMUM_OUTSIDE_RTH_TRADING_BOUND` are **deprecated** (no longer emitted) and replaced by a single `FILL_WILL_EXCEED_TRADING_BOUND`, covering crossing takers, crossing `modifyOrder` cancel-replaces, triggered TPSLs, and placements that would rest reachable beyond the band. ## New features **Maximum order size enforced.** [`POST /v1/placeOrder`](/api-reference/exchange/place-order), [`POST /v1/modifyOrder`](/api-reference/exchange/modify-order), and the batch variants now reject an order whose `quantity` exceeds the market's `maxOrderSize` (see [`GET /v1/markets`](/api-reference/public/get-markets)) with `OrderSizeTooLarge`. Reduce-only orders are **not** exempt. **Market-attribute events over WebSocket.** The `exchangeAttributeUpdates` channel now emits `marketAttributes` entries carrying per-market Regular Trading Hours and off-hours band events: RTH boundary crossings (`isOutsideRth`), VWAP settlement seals (`sealedVwapSettlementPrice`), and expansion-zone / band-expansion events (`boundEvent`: `expansionZoneEntered`, `expansionZoneExited`, `tradingBoundExpanded`). ## Updates **Candle prices are oracle-derived.** [`GET /v1/candles`](/api-reference/public/get-ohlcv-candles) `open` / `high` / `low` / `close` are derived from the market's oracle price feed, not executed trades. `volume`, `takerBuyVolume`, the notional volumes, and `tradeCount` remain trade-derived statistics for the bucket. ## Breaking changes **`reduceOnly` no longer requires `IOC` or `FOK`.** [`POST /v1/placeOrder`](/api-reference/exchange/place-order) and [`POST /v1/modifyOrder`](/api-reference/exchange/modify-order) now accept `reduceOnly: true` with any `timeInForce` — the previous constraint forcing reduce-only orders to be `IOC` or `FOK` is removed. Reduce-only position semantics are enforced regardless of TIF, so a resting reduce-only order is now valid. ## New features **Rolling fees-paid on account stats.** [`GET /v1/account/stats`](/api-reference/public/get-account-stats) adds `rollingFeesPaid` — perps trading fees paid over the 14-day fee-tier rolling window (quote quantums, 1e9 = \$1), the same window as `rollingVolume`. It's computed from an hourly rollup, so it can trail live fills by up to the current partial hour, and reads `0` if that rollup is temporarily unavailable. ## New features **Lightweight referral-code lookup.** New [`GET /v1/affiliate/code`](/api-reference/referral/get-the-callers-referral-code) returns just `{ address, code, isAffiliate }` from a single point read — none of the volume, commission, or fee-tier enrichment that `GET /v1/affiliate/info` performs. It's meant for UI surfaces that only need the code (an "Invite Friends" / copy-referral-link control) and may poll every session. Public — no `X-API-Key`, no signature. "Not an affiliate" is a valid empty state served as `200` with `code: ""` and `isAffiliate: false`, never a `404`, so callers don't have to overload the status code. Initially mainnet-only; available on both environments since mid-July 2026. ## New features **Per-subaccount rate-limit snapshot on write responses.** [`POST /v1/placeOrder`](/api-reference/exchange/place-order), [`POST /v1/batchPlaceOrders`](/api-reference/exchange/place-batch-orders), [`POST /v1/cancelOrder`](/api-reference/exchange/cancel-order), [`POST /v1/batchCancelOrders`](/api-reference/exchange/batch-cancel-orders), [`POST /v1/cancelAllOrders`](/api-reference/exchange/cancel-all-open-orders), and [`POST /v1/modifyOrder`](/api-reference/exchange/modify-order) now return a `rateLimit` object (`pool` — `order` or `cancel` — and `remaining` tokens after the request) on both REST and WebSocket, so clients can track their remaining budget without an extra call. `remaining` can be `0` or negative while the request still succeeds when the account is on the drip throttle; `-1` is a sentinel meaning the per-subaccount layer was not enforced for this request. **`clientId` echo on rate-limit rejections.** The `429` body now echoes the request's `clientId` (single-order endpoints) or `clientIds` (batch endpoints, positionally aligned with the submitted array), so a client firing concurrent requests can correlate the rejection to a specific order or batch. **New modify rejection reason.** `MODIFY_WOULD_CROSS_OUTSIDE_RTH_TRADING_BOUNDARY` — a non-crossing `modifyOrder` reprice rejected because the new price violates the active off-hours trading band. Unlike `PRICE_WILL_EXCEED_MAXIMUM_OUTSIDE_RTH_TRADING_BOUND` (the crossing cancel-replace path, which removes the original), the original order stays resting on the book unchanged. **Rejected `setLeverage` is now observable on WebSocket.** The [`accountAttributes`](/api-reference/channels#accountattributes) channel adds a streaming-only `leverageReject` entry — the `REJECTED` half of the `setLeverage` lifecycle. It carries the `rejectReason` (`UNDERCOLLATERALIZED`, `INVALID_LEVERAGE`, or `UNKNOWN_MARKET`) and the originating `requestId` for correlation; an applied change still arrives as a `leverage` entry. ## Updates **Deterministic market ordering documented.** [`GET /v1/markets`](/api-reference/public/get-markets) now documents that markets are returned sorted by ascending `marketId`, and the mids map on [`GET /v1/mids`](/api-reference/public/get-all-mid-prices) is ordered by ascending `marketId` and includes only ONLINE markets (OFFLINE markets are omitted). ## Breaking changes **Position-tiered margin fields removed.** The [`markets`](/api-reference/channels#markets) WebSocket channel no longer returns `transferMarginFraction`, `incrementalInitialMarginFraction`, `incrementalPositionSize`, `baselinePositionSize`, or `maxPositionSize`. Initial margin is no longer scaled by position-size brackets in the published market schema; `initialMarginFraction`, `maintenanceMarginFraction`, and the off-hours variants remain. **`minOrderSize` and `maxOrderSize` removed.** The same market reads drop `minOrderSize` and `maxOrderSize`, and `minOrderSize` is no longer a required field. Per-market order sizing is now expressed through `minOrderNotional` (the \$5 minimum-notional floor introduced in testnet-v1.1.93) and `stepSize`. ## Breaking changes **Minimum order notional enforced.** [`POST /v1/placeOrder`](/api-reference/exchange/place-order) and [`POST /v1/batchPlaceOrders`](/api-reference/exchange/place-batch-orders) now reject position-opening orders whose notional (`quantity` × `price`) is below **\$5** with `InvalidRequest`. Reduce-only orders (including TPSLs) are exempt. The per-market floor is published as `minOrderNotional` on [`GET /v1/markets`](/api-reference/public/get-markets). **All-traders leaderboard replaces the affiliate volume leaderboard.** `GET /v1/affiliate/volumeleaderboard` (`getVolumeLeaderboard`) is gone, replaced by [`GET /v1/leaderboard`](/api-reference/public/get-the-all-traders-leaderboard) (`getTraderLeaderboard`). A new `sortBy` query param (`volume`, `pnl`, or `fees`; default `volume`) chooses the ranking column, and every row now carries `volume`, `feesPaid`, and `pnl` (realized only) together, computed in one query so the three columns are always mutually consistent. The `window` (`all`/`30d`/`24h`, default `30d`) and `limit` (max 100, no cursor) semantics are unchanged. **API key `keyName` removed.** [`POST /v1/createApiKey`](/api-reference/onboarding/create-api-key) no longer accepts or returns the optional `keyName` label. ## New features **Off-hours trading-band fields.** The [`markets`](/api-reference/channels#markets) WebSocket channel now exposes `currentSettlementPrice`, `currentTradingBound`, and `nextTradingBound` for equity-class markets while outside regular trading hours (omitted in-RTH and for 24/7 crypto markets). **Lifetime volume & fees on the fee-tier read.** [`GET /v1/feetiers`](/api-reference/public/get-fee-tier-table) now returns `lifetimeVolume` and `lifetimeFeesPaid` alongside the existing 14-day `rollingVolume`. ## Updates **Open interest is now populated.** `openInterest` on the [`markets`](/api-reference/channels#markets) WebSocket channel now reports the total size of open long positions (equal to total short, in base-asset units), refreshed from a periodic snapshot, instead of the previous placeholder `"0"`. ## New features **Mainnet is live.** The production API is now serving at `https://api.arcus.xyz` (REST) and `wss://api.arcus.xyz/v1/ws` (WebSocket), selectable from the server dropdown on every endpoint page. Testnet (`api.testnet.arcus.xyz`) remains available, and the two environments share identical paths, payloads, and signing — only the host differs. The code samples and API playground throughout the docs continue to target testnet. See the [API introduction](/api-reference/introduction). ## Updates **Minimum withdrawal size enforced.** [`POST /v1/withdraw`](/api-reference/exchange/submit-withdrawal) now rejects amounts below the collateral currency's minimum with `400` — e.g. `validation error on field 'amount': must be at least 1000000000 quote quantums` (1e9 quote quantums = \$1). Amounts must also be exactly representable in collateral base units. ## Breaking changes **Order time-in-force rules tightened.** New per-order-type `timeInForce` constraints, enforced at request validation: * **Every order must include `goodTilTime`** (epoch microseconds) set **at least one month in the future** — now required even for `IOC` orders, which previously ignored it. A missing value is rejected with `validation error on field 'goodTilTime': is required`. * **`MARKET` orders must use `IOC`.** `GTT`, `FOK`, and `ALO` are rejected with `validation error on field 'timeInForce': must be IOC for MARKET orders`. * **TPSL orders:** `LIMIT` TPSL legs must use `GTT`; `MARKET` TPSL legs must use `IOC`. (TPSL legs must also be `reduceOnly`, and are placed via `batchPlaceOrders` groupings.) ## New features **Server time.** [`GET /v1/time`](/api-reference/public/get-current-server-time) returns the current server time — useful for aligning the nanosecond signing timestamp with the gateway's ±30s drift window. ## Breaking changes **Order signing moved to the ordersign typed canonical payload.** `placeOrder`, `cancelOrder`, and `modifyOrder` — and each element of `batchPlaceOrders` / `batchCancelOrders` — now sign a compact, key-sorted JSON payload built from the parsed request: the payload object **is** the Ed25519 message, with no `timestamp + action` prefix. The timestamp is carried as the `ct` field (it must equal `X-Timestamp`), and price/size are signed as integer ticks/quantums derived from the market's `tickSize` / `stepSize`. The previous `X-Timestamp + action + canonicalJSON(body)` scheme is now rejected with `401` for these endpoints. `cancelAllOrders`, `setLeverage`, and WebSocket authentication keep the legacy message. See [Authentication](/api-reference/authentication). **`GTC` replaced by `GTT`.** The `timeInForce` enum is now `GTT`, `IOC`, `FOK`, `ALO`. Resting orders (`GTT`, `ALO`) require `goodTilTime` (epoch microseconds) set **at least one month in the future** at the time the order is processed; nearer or missing values are rejected. **Withdrawals now use EIP-712 typed data.** [`POST /v1/withdraw`](/api-reference/exchange/submit-withdrawal) authenticates with an `eth_signTypedData_v4` signature over a `Withdraw` typed payload (domain and types in the endpoint reference) instead of the previous SHA-256 ECDSA canonical message. **Market orders require a protective price bound.** A `MARKET` order's `price` must be within **10%** of the current mark price (TPSL market orders: within 10% of the trigger price), or the order is rejected. With this bound, market orders behave like marketable limit orders. ## Updates **New order rejection reason.** `PRICE_WILL_EXCEED_MAXIMUM_OUTSIDE_RTH_TRADING_BOUND` is added to the `RejectionReason` set delivered on the `orders` WebSocket channel — emitted when a resting order's price would breach the maximum off-hours trading bound once the current regular-trading-hours session ends. ## Breaking changes **API base URLs moved to `api.testnet.arcus.xyz`.** REST is now `https://api.testnet.arcus.xyz` and WebSocket `wss://api.testnet.arcus.xyz/v1/ws/`. The previous hostnames are being retired — update any saved base URLs, SDK configs, and clients at your earliest convenience to avoid disruption. Paths and payloads are unchanged. See the [API introduction](/api-reference/introduction). ## Breaking changes **Candles pagination replaced.** [`GET /v1/candles`](/api-reference/public/get-ohlcv-candles) no longer accepts `limit`. A request must now pass `to` — the upper bound of the window as a Unix **microsecond** timestamp (seconds- or milliseconds-scale values are rejected) — and may scope the lower bound with either `countback` (number of closed bars counting back from `to`) or `from` (a microsecond lower bound); `from` and `countback` are mutually exclusive. A request without `to` now returns `400`. **`volume30d` renamed to `rollingVolume`.** [`GET /v1/account/stats`](/api-reference/public/get-account-stats) and the affiliate commission and claim schemas now report notional fill volume under `rollingVolume`, measured over the fee-tier rolling window rather than a fixed trailing 30 days. Update any client that reads `volume30d`. ## Updates **New order rejection reasons.** The `RejectionReason` set delivered on the `orders` WebSocket channel adds `OPEN_ORDER_CAP_EXCEEDED`, `ORDER_NOT_FOUND`, `ORDER_NOT_FOUND_FOR_MODIFY`, `MODIFY_CHANGED_IMMUTABLE_FIELD`, `MODIFY_ZERO_SIZE`, and `ORDER_WILL_TAKE_LIQUIDITY_DURING_MARKET_HALT`. **Market asset name in metadata.** Market metadata responses now include `fullAssetName` (e.g. `Bitcoin`) when a canonical name is configured for the market. ## Breaking changes **Unified request signing.** REST and WebSocket now share one signing message: `X-Timestamp + action + canonicalJSON(body)`, where `action` is the canonical operation name — the REST path's final segment or the WebSocket request `type` (e.g. `placeOrder`, `cancelOrder`). The HTTP method and the `/ws/v1/` path prefix are no longer part of the signed message; re-sign with the new message before sending or the request is rejected with `401 invalid signature`. Batch methods sign each element over its singular action (e.g. `placeOrder` for each order in `batchPlaceOrders`) with one shared timestamp. The cutover also invalidates API keys registered before it — re-register via `POST /v1/createApiKey` (EIP-191 registration is unchanged). See [Authentication](/api-reference/authentication). ## Breaking changes **Protected requests now require nanosecond timestamps.** The signing timestamp — `X-Timestamp` for REST, `timestamp` for WebSocket order methods — must now be Unix time in **nanoseconds** as a decimal string (e.g. `1713825891591000000`), not milliseconds. Millisecond or second epochs are rejected with `401 Unauthorized`. Update any client that builds the signing message before re-signing. See [Authentication](/api-reference/authentication). ## Updates **User-facing timestamps are now microseconds.** Response time fields — candle `openTime`, funding payment times, and order placement/update times — are emitted in Unix microseconds for consistent precision across the API. ## New features **Volume leaderboard REST endpoint.** [`GET /v1/affiliate/volumeleaderboard`](/api-reference/public/get-the-all-traders-leaderboard) returns the top addresses ranked by signed-notional fill volume over a rolling window — `all`, `30d`, or `24h` (default `30d`). **Referral commission rate schedule.** [`GET /v1/commissionrates`](/api-reference/public/get-referral-commission-rate-schedule) returns the referral commission rate schedule used to compute affiliate payouts on each fill. ## New features **Rate-limit usage REST endpoint.** [`GET /v1/rateLimit`](/api-reference/public/get-current-rate-limit-usage) returns the state of both per-subaccount throttle pools (order and cancel), so clients can self-pace and see which limit is currently constraining them. **Withdrawals REST endpoint.** [`POST /v1/withdraw`](/api-reference/exchange/submit-withdrawal) submits a withdrawal of collateral to the chain. **Referral code availability.** [`GET /v1/affiliate/codeAvailable`](/api-reference/referral/check-whether-a-referral-code-is-available) returns `{ code, available }` for a single referral code string. ## New features **Fee tier table REST endpoint.** [`GET /v1/feetiers`](/api-reference/public/get-fee-tier-table) returns the full fee tier table, sorted ascending by level (`0=Base` through `4=Platinum`) — the maker/taker fee BPS for each tier and the 30-day notional volume threshold required to reach it. **Exchange attribute updates WebSocket channel.** Subscribe to [`exchangeAttributeUpdates`](/api-reference/channels#exchangeattributeupdates) for exchange-wide state, discriminated by entry `type`. The current type, `feeTierConfig`, sends the full fee tier table on subscribe and a fresh replacement whenever an operator updates it — the streaming counterpart to `GET /v1/feetiers`. ## New features **Account attribute updates WebSocket channel.** Subscribe to [`accountAttributeUpdates`](/api-reference/channels#accountattributeupdates) for live per-account state — effective leverage per market and the account's current trading fee tier — discriminated by entry `type` (`"leverage"` or `"feeTier"`). On subscribe the server sends one leverage entry per market plus, when available, one fee tier entry; updates stream a single-entry delta for whichever attribute changed. **Leverages REST endpoint.** [`GET /v1/leverages`](/api-reference/public/get-leverage-and-margin-mode-for-an-account-across-all-markets) returns the effective leverage for `(address, accountIndex)` across every market, sorted by ascending `marketId`. Each entry reflects the user's override (set via [`POST /v1/setLeverage`](/api-reference/exchange/set-leverage-for-a-market)) when one exists, otherwise the market default. **Account stats REST endpoint.** [`GET /v1/account/stats`](/api-reference/public/get-account-stats) returns 30-day rolling notional fill volume and the current trading fee tier (level + BPS) for any Ethereum address. ## Updates **Affiliate endpoints moved.** All `/v1/api/affiliate/*` paths are now under [`/v1/affiliate/*`](/api-reference/referral/get-affiliate-info) — the legacy `/api/` segment has been dropped. Update any clients that hard-coded the old prefix. Operation IDs and request/response schemas are unchanged. ## New features **Trades REST endpoints.** Query recent public trades for a market with [`GET /v1/trades`](/api-reference/public/get-recent-public-trades), or fetch a single trade by ID with [`GET /v1/trade/{tradeId}`](/api-reference/public/get-a-single-trade-by-id). **Funding payments — REST and WebSocket.** Track per-account funding history with [`GET /v1/funding`](/api-reference/public/get-funding-payments) and per-market rates with [`GET /v1/fundingRates`](/api-reference/public/get-market-funding-rates). Stream live funding payments over the new [`funding`](/api-reference/channels#funding) WebSocket channel — snapshot of the 100 most recent payments on subscribe, then one event per `(market, account)` at each funding time. Positive `payment` = received, negative = paid. **Portfolio history.** [`GET /v1/portfolio`](/api-reference/public/get-portfolio-history) returns account equity, PnL, and value-over-time history for charting. **Positions REST endpoint.** [`GET /v1/positions`](/api-reference/public/get-positions) returns the current open positions for an account — the REST counterpart to the existing `positions` WebSocket channel. **Cancel-all and set-leverage.** [`POST /v1/cancelAllOrders`](/api-reference/exchange/cancel-all-open-orders) cancels every open order for an account in one call; [`POST /v1/setLeverage`](/api-reference/exchange/set-leverage-for-a-market) updates per-market leverage. **Account transfer updates.** [`GET /v1/accountTransferUpdates`](/api-reference/public/get-account-transfer-updates) returns deposit/withdrawal history with cursor pagination. **Referral program.** Twelve new endpoints under [`/v1/api/affiliate/*`](/api-reference/referral/get-affiliate-info) cover the full affiliate lifecycle — create and revoke referral codes, register affiliates, view referees and leaderboard, track commissions and kickbacks, and claim commission payouts. ## New features **REST and WebSocket API now live on testnet.** Trade, stream market data, and manage accounts against `https://api.testnet.arcus.xyz` (REST) and `wss://api.testnet.arcus.xyz/v1/ws/` (WebSocket). Both APIs are non-custodial — register an Ed25519 API key against your Ethereum address and the server never sees a private key. See the [API introduction](/api-reference/introduction) to get started. **Real-time market data channels.** Subscribe to live updates over WebSocket: * [`l2Orderbook`](/api-reference/channels#l2orderbook) and [`l2OrderbookUpdates`](/api-reference/channels#l2orderbookupdates) — full snapshots every \~500 ms, or initial snapshot plus incremental updates (1–100 levels). * [`bbo`](/api-reference/channels#bbo) — best bid and offer at the top of book. * [`trades`](/api-reference/channels#trades) — live public trade stream. * [`markets`](/api-reference/channels#markets) — global market metadata with funding rate, oracle and index prices, and 24h volume. * [`oraclePrices`](/api-reference/channels#oracleprices) — per-market oracle prices from the on-chain Slinky aggregator. * [`candles`](/api-reference/channels#candles) — OHLCV across 13 timeframes from `1m` to `1w`, with up to 200-candle snapshots. **Account and order channels.** Track your account state and order lifecycle in real time with [`account`](/api-reference/channels#account), [`positions`](/api-reference/channels#positions), [`orders`](/api-reference/channels#orders), and [`userFills`](/api-reference/channels#userfills). **Order routing over WebSocket.** Place, cancel, modify, and batch orders directly over WebSocket. All order methods are asynchronous and return a `202 ACK` — observe lifecycle through the `orders` and `userFills` channels. See [Placing orders](/api-reference/websocket#placing-orders). **Ed25519 API keys with Ethereum-signed registration.** Generate an Ed25519 key pair, register it with an ECDSA signature from your master Ethereum address, and sign protected requests with a Unix-millisecond timestamp. Full instructions in [Authentication](/api-reference/authentication). ## Updates **Rebranded to Arcus.** The product is now Arcus across all user-facing surfaces, including the API spec and documentation. ## Known limitations * `batchModifyOrders` is not yet implemented and returns `501`. * `createApiKey` is REST-only; the WebSocket equivalent returns `501 NotImplemented`. # Disclaimer and Terms Source: https://docs.arcus.xyz/concepts/disclaimer Important information about the Arcus protocol, Stock Tokens, and the risks of trading. Arcus is a blockchain-based smart contract protocol that permits self-custodial peer-to-peer trading of Stock Tokens, cryptoassets and perpetual futures. Arcus is not a regulated financial services provider, and it is not available in the U.S., Canada, United Kingdom and other restricted jurisdictions. Use of the protocol is subject to the [Terms of Use](https://arcus.xyz/legal/terms). Stock Tokens are tokenised securities that provide economic exposure to a relevant underlying equity instrument or ETP through a contractual claim against the Issuer for a cash Redemption. Stock Tokens involve risks not present, or not present to the same extent, in traditional stock ownership, including private-key loss or compromise, limited redemption access, liquidity constraints, price or tracking divergences from the underlying, and uncertain or evolving regulatory treatment. Trading Stock Tokens, cryptoassets or perpetual futures is risky and involves risks of loss, particularly when using leverage. DYOR. NFA # Architecture Source: https://docs.arcus.xyz/concepts/exchange-architecture How the Arcus perpetuals exchange combines a high-performance off-chain matching engine with a permissioned appchain and an EVM rootchain to settle trades non-custodially The Arcus perpetuals exchange is a hybrid DEX built around a high-performance off-chain CLOB matching engine. Trades are validated, batched, and settled non-custodially: the core exchange matches orders off-chain, the Arcus appchain anchors consensus and re-verifies that exchange rules were respected, and fund custody plus final settlement live on an EVM-based public blockchain. This page describes the **perpetuals** exchange. Spot trading uses a different model — see the [Spot](/concepts/spot-rfq) concepts. The page is conceptual; for wire formats and endpoints, see the [API reference](/api-reference/introduction). ## High-level view At the top level, the core exchange feeds the appchain validators, which drive both settlement on the EVM rootchain and publication to decentralized storage. ```mermaid theme={null} flowchart LR subgraph servers["Arcus Servers"] core["Core Exchange"] end subgraph appchain["Arcus Appchain"] validator["Validator set"] end subgraph rootchain["EVM Rootchain"] checkpoint["Checkpoint Manager"] vault["Bridge Vault 💲"] end subgraph storage["Decentralized Storage"] da["Data Availability"] end core -->|executed trades| validator validator -->|committed state roots| checkpoint validator -->|publish block data| da checkpoint -->|settle / custody| vault ``` ## The layers | Layer | Component | Role | | - | - | - | | **Off-chain** | Core exchange | The CLOB matching engine. Ingests requests through API servers, orders them with a sequencer, and runs a deterministic state machine that matches and batches trades. Not bottlenecked by blockchain settlement. | | **Appchain** | Validator set | A permissioned set that independently replicates the exchange state machine — re-verifying signatures, checking margin, and confirming fills are valid against orders — then batches transactions, computes a state commitment (e.g. a Merkle root), and votes it on-chain. This is what keeps the exchange non-custodial. | | **Rootchain (EVM)** | Checkpoint Manager | Records the validator set's state commitment and the resulting on-chain side effects (withdrawals) onto the public EVM rootchain. | | **Rootchain (EVM)** | Bridge Vault | Custodies user funds and stores withdrawable balances, performs final settlement, and honors withdrawals published by the validator set. Includes the escape hatch (emergency settlement) so withdrawals can't be censored. | | **Storage** | Data availability | Decentralized storage that keeps block data retrievable — the escape hatch depends on it. | ## Key properties of the hybrid design * The core exchange operates in a centralized manner, so its performance is **not limited by blockchain settlement** — it sustains **100k+ orders per second** with **\~20ms trade confirmations**. * Executed trades emitted by the core exchange can be considered **final**. * The only exception is escape-hatch activation (a full exchange shutdown), which occurs only if Arcus or the appchain validators go offline. In that case, trades since the last committed and executed state root would be "busted." * The appchain is operated by a **permissioned validator set**. It validates that exchange rules are respected so the core exchange cannot exert substantial control over user funds — i.e. the exchange is **non-custodial**. Verification includes checking that all state transitions are acceptable and re-verifying signatures on orders, withdrawals, and oracle prices. * **Custody of funds and final trade settlement are on the EVM rootchain.** The rootchain contracts include an escape hatch ("emergency settlement") that prevents the core exchange from censoring withdrawals. This escape hatch depends on data availability, which the appchain validators ensure. In these general respects the design is comparable to other hybrid DEXes such as StarkEx and Lighter. ## Rootchain contract requirements Subject to the system's trust assumptions, the rootchain smart contracts must: * ensure user funds can't be stolen; * ensure users always have a means to exit and withdraw their funds; * prevent denial of service. ### Security audits The rootchain contracts have been independently reviewed: * [Trail of Bits — Arcus Labs Rootchain Security Review (June 2026)](https://github.com/trailofbits/publications/blob/master/reviews/2026-06-arcuslabs-rootchain-securityreview.pdf) * [OpenZeppelin — Arcus Rootchain Contracts Audit](https://www.openzeppelin.com/news/arcus-rootchain-contracts-audit) ## Inside the core exchange Zooming into the off-chain core exchange, the request pipeline is a straight line. Two kinds of input feed it: * **API servers** accept client requests, verify their signatures, and detect and verify on-chain events — deposits, forced trades, and forced withdrawals — before passing them in. * **Upstream services** supply the other transaction types the exchange needs: oracle price ticks, funding ticks, liquidations, and market config changes. Both streams converge on the **sequencer**, which assigns every transaction a single deterministic order — the property that makes execution reproducible and fair. From there the **matching engine** maintains the order book and matches trades, and together they advance the **exchange state machine**: account balances, oracle prices, and market configs. Matched results are published to two places — real-time **WebSocket feeds** for market data, and **persistence** for history and downstream consumers. ```mermaid theme={null} flowchart LR api["API servers"] --> seq["Sequencer"] feeds["Upstream services"] --> seq seq --> match["Matching engine"] match --> ws["WebSocket feeds"] match --> db["Persistence"] ``` # Onboarding & deposits Source: https://docs.arcus.xyz/concepts/onboarding What you need to start trading on Arcus, how deposits work through the Fun flow, and how funds settle as USDG To trade on Arcus you need a supported wallet, funds in it, and eligibility in your region. New users can create a wallet with email or social sign-in; existing wallets connect directly. Funds are deposited through an in-app flow powered by [Fun](https://fun.xyz) and settle to your Arcus account as **USDG**, the protocol's settlement stablecoin. This page covers the concepts. The deposit and withdrawal flows themselves are part of the app experience; the cross-chain routing described here is handled for you. ## What you need to start * A **supported wallet** — created in-app via email or social sign-in, or connected from an existing self-custody wallet. * **Funds** to trade with (these arrive as USDG; see below). * **Eligibility** in your region. ## Collateral: USDG Arcus settles in **USDG**, a regulated stablecoin issued by Paxos under the Global Dollar Network, redeemable 1:1 for USD and backed by cash and US Treasuries. Whatever you deposit is converted to USDG and credited to your Arcus account. ## Depositing through Fun Deposits run through the [Fun](https://fun.xyz) flow. You first choose whether the funds land in **USDG (Perps)** or **USDG (Spot)**, then pick a route: | Route | How it works | | - | - | | **Transfer crypto** | Send crypto from a self-custody wallet on any major chain (Bitcoin, Ethereum, Solana, Base, Arbitrum, and others). It converts to USDG on arrival — typically instant. | | **Connect an exchange** | Pull funds directly from a centralized-exchange balance (Coinbase at launch, more coming). | | **Use cash** | Pay with Apple Pay, card, Google Pay, or AstroPay. A processing fee applies and is shown before you confirm. | Behind a single deposit address, Fun automatically swaps and bridges your deposit to USDG and delivers it to your Arcus account in **one atomic (all-or-nothing) flow**, routing through established non-custodial bridges (such as Circle CCTP, Relay, Across, and LayerZero) selected by asset, route, and size. The route, fees, and estimated time are always shown before you confirm. **Arcus charges no deposit fee of its own**, and USDG deposits and withdrawals carry zero fees; any third-party processing or conversion cost is displayed upfront. Fun is a third-party onboarding provider. For the full list of supported chains, payment methods, limits, and fees, see [fun.xyz](https://fun.xyz) and the [Fun help guide](https://intercom.help/funxyz/en/collections/9212192-fun-xyz-s-help-guide). ## Next steps What you can trade and how 24/7 perps work. Generate an API key and start trading programmatically. # Fees Source: https://docs.arcus.xyz/concepts/perpetuals/fees How trading fees work on Arcus — the maker/taker fee-tier model and where to read live fee values Trading fees on Arcus follow a **maker/taker fee-tier model**: the rate you pay depends on whether your order adds liquidity (maker) or removes it (taker), and on your tier, which is driven by trailing trading volume. This page is conceptual. Your current tier and the live fee schedule are returned by [`GET /v1/feetiers`](/api-reference/public/get-fee-tier-table); always read fees from the API rather than hard-coding them. You can also check your latest fee tier from the [Arcus portfolio page](https://app.arcus.xyz/portfolio). ## The model * **Maker** orders rest on the book and provide liquidity; **taker** orders execute against resting liquidity and remove it. Makers are charged less than takers, and high-volume tiers can earn **maker rebates**. * **Tiers** are determined by your trailing trading volume over the past 30 days. As your volume grows, you move into lower-fee tiers automatically. * Funding payments are **not** a fee — they are transfers between traders. See [Funding](/concepts/perpetuals/funding). ## Current state * **Spot trading is free at launch.** * **Perp fee tiers** publish closer to launch — set to be competitive with leading perp venues, with maker rebates for market makers and high-volume traders. Specific rates per tier are read from the API once live. Because the exact schedule is still being finalized and will change over time, this page deliberately avoids quoting rates. [`GET /v1/feetiers`](/api-reference/public/get-fee-tier-table) is the source of truth, and your portfolio page shows your current tier. ## Account costs beyond trading fees * **Deposits/withdrawals:** Arcus charges no fee of its own; USDG deposits and withdrawals carry zero fees. Third-party processing or conversion costs (for card/fiat or cross-chain routes) are shown upfront. See [Onboarding & deposits](/concepts/onboarding). # Funding Source: https://docs.arcus.xyz/concepts/perpetuals/funding How funding payments keep Arcus perps anchored to the underlying, how real-world-asset events are handled, and the off-hours funding and price-band regime **Funding** is the mechanism that keeps a perp's price tethered to its underlying. Periodically, the side of the market trading at a premium pays the side trading at a discount, in proportion to the gap between the perp price and the [oracle price](/concepts/perpetuals/prices#oracle-price). When the perp trades above the oracle, longs pay shorts; when it trades below, shorts pay longs. There is no fee taken by the exchange on funding — it is a transfer between traders. This page is conceptual. Live and historical funding values are available from [`GET /v1/fundingRates`](/api-reference/public/get-market-funding-rates) and the [`funding` WebSocket channel](/api-reference/channels#funding). ## The basic model The funding rate is driven by the **basis** — the gap between the perp's price and the oracle price. A persistent premium produces positive funding (longs pay), which incentivizes traders to close longs / open shorts and pulls the price back toward the oracle; a persistent discount does the reverse. This is the same convergence mechanism used across perp markets. **Base rate.** With no premium, funding sits at a **base rate**: **0.01% per 8h** for crypto perps, and **SOFR + 0.5% per year** for real-world-asset perps. The premium (below) moves the live rate above or below that base. ## How the rate is calculated Funding is **charged once an hour**, and each hour's rate is built from two pieces: * a **base rate** — the baseline cost of holding the position, charged even when the perp trades right at fair value; and * a **premium** — an adjustment that grows as the perp drifts above or below the oracle, nudging the price back. The premium is **sampled about once a minute**, and the rate for the hour is the **median** of that hour's samples, so one brief spike can't swing it. Throughout, `clamp(x, ±y)` just means "keep `x` within the range −y to +y." ### Step 1 — adjust the oracle to "today" (real-world assets only) Stocks settle a day or more after they trade (T+1), so a live stock price isn't directly comparable to a perp you hold continuously. For RWA markets the oracle is nudged to a **same-day** value before the premium is measured (crypto settles same-day, so this step does nothing): ``` oracle_sameday = oracle / (1 + sofr_rate_hourly × 24 × days_to_settle) ``` `days_to_settle` is the calendar days until the stock settles (usually 1), and `sofr_rate_hourly` is the RWA base rate expressed per hour: ``` sofr_rate_hourly = (SOFR + 0.5%) / 360 / 24 ``` **SOFR** (Secured Overnight Financing Rate) is the standard benchmark for the cost of borrowing US dollars overnight, published daily by the [New York Federal Reserve](https://www.newyorkfed.org/markets/reference-rates/sofr) — the same reference traditional finance uses for short-term funding. `sofr_rate_hourly` is simply that annual rate plus 0.5%, divided down to a per-hour figure. The effect on the oracle is tiny. *Example.* AAPL oracle = `$200`, base rate ≈ 5%/yr (so `sofr_rate_hourly ≈ 0.00000579`), T+1 settlement → `200 / (1 + 0.00000579 × 24 × 1)` ≈ `$199.97` — under 3 cents. ### Step 2 — measure the premium The **premium** is how far the perp trades from the [oracle price](/concepts/perpetuals/prices#oracle-price), as a fraction of the oracle. It uses **impact prices** — the average price to buy or sell a fixed test size by walking the order book — instead of the best bid/ask, so one tiny quote can't distort it: ``` premium = ( max(impact_bid − oracle, 0) − max(oracle − impact_ask, 0) ) / oracle ``` * perp **above** the oracle (impact bid > oracle) → **positive** premium → longs pay shorts * perp **below** the oracle (impact ask \< oracle) → **negative** premium → shorts pay longs * oracle sitting **between** the impact bid and ask → premium is **zero** *Example.* BTC oracle = `$100,000`, and buying the test size would cost `$100,050` on average → premium = (100,050 − 100,000) / 100,000 = **0.05%**. ### Step 3 — turn the premium into the hourly rate By convention the full premium is paid off over **8 hours**, so each hour charges **one-eighth** of it on top of the base rate, and the total is capped at **±4% per hour**. **Crypto** — base rate **0.01% per 8h** (≈ 10.95% per year). A small **dead-band** holds funding exactly at the base rate until the premium moves more than **0.05%** away from the base level, so trivial premiums don't add funding noise: ``` base_rate_hourly = 0.01% / 8 # the 8h base, spread over the hour funding_rate_hourly = clamp( premium/8 + clamp(base_rate_hourly − premium/8, ±0.05%/8), ±4% ) ``` **Real-world assets** — base rate **SOFR + 0.5% per year** (`sofr_rate_hourly`, from Step 1), with **no dead-band**, so the premium always counts and the rate lands exactly on the base rate when the perp trades at the same-day oracle: ``` funding_rate_hourly = clamp( sofr_rate_hourly + premium/8, ±4% ) ``` *Example (RWA).* With `sofr_rate_hourly ≈ 0.00000579` and a perp trading 0.08% above the same-day oracle (`premium = 0.0008`): `funding_rate_hourly ≈ 0.00000579 + 0.0008/8` ≈ **0.0106%** for the hour. ### Step 4 — the payment ``` payment = funding_rate_hourly × position_size × oracle_price ``` Funding values the position at the **oracle price** (unlike margin, PnL, and liquidations, which use the [mark price](/concepts/perpetuals/prices#mark-price)). A positive rate means **longs pay shorts**; a negative rate, the reverse. *Example.* At **0.0106%/hr** on a `$10,000` position, a long pays about `$1.06` that hour. Outside extended trading hours (04:00–20:00 ET) this whole calculation is suspended and the rate is locked — see [Off-hours funding](#off-hours-funding) below. ## Real-world assets For perps on equities, commodity ETFs, and index ETFs, funding does more than close the basis: it also passes through the events that drive a real holder's return — **dividends, splits, and futures rolls** — so the perp tracks the **total return** of the underlying. Those mechanics are covered on [Real-world asset perps](/concepts/perpetuals/real-world-assets). ## Off-hours funding The lock applies only when the underlying is fully dark — **outside extended trading hours, i.e. overnight from 20:00 to 04:00 ET** (and weekends/holidays). Then there's no reliable oracle to measure a premium against, so the live calculation above is suspended and **funding is locked to the base rate (SOFR + 0.5%)**, fixed at the close. During premarket (04:00–09:30 ET) and after-hours (16:00–20:00 ET) the underlying still trades, so funding runs **live**, exactly as during the regular session. This removes funding uncertainty for traders carrying a position overnight or over a weekend, beyond the price of the underlying itself. Crypto perps have **no** off-hours regime — funding stays live and basis-driven around the clock. Off-hours also raises margin and constrains prices to widening bands; those guardrails are covered under [Real-world asset perps](/concepts/perpetuals/real-world-assets#trading-when-the-underlying-is-closed) (and [Margin](/concepts/perpetuals/margin#off-hours-margin)). Regular-trading-hours calendars differ by market, and the exact premium sampling and clamp parameters are per-market config. Read live funding from the API rather than recomputing it. # Liquidations Source: https://docs.arcus.xyz/concepts/perpetuals/liquidations A high-level look at when an Arcus perp position is liquidated, what happens to it, and how off-hours price bands reduce cascade risk A position is **liquidated** when it can no longer meet its [maintenance margin](/concepts/perpetuals/margin) — that is, when account equity (collateral plus unrealized PnL) falls below the minimum the position is required to keep. Liquidation closes the at-risk position so that losses stay bounded by the collateral backing it. This page describes the model at a high level. Liquidation parameters are part of each market's risk configuration and are subject to change. ## When it happens As a position moves against you, its unrealized loss reduces your account equity. While equity stays above maintenance margin, the position is healthy. Once equity drops to or below the maintenance margin requirement, the position becomes **eligible for liquidation**. Two levers most directly affect how close you are: * **Adverse price movement** in the [mark price](/concepts/perpetuals/prices#mark-price) of your position — the value used for all margin and liquidation math. * **Margin requirement changes** — for example, the higher initial margin applied to RWA perps when the underlying market is closed (note that *maintenance* margin is not raised off-hours). ## What happens to the position When a position is liquidated, it is closed to bring the account back within its margin requirements. You keep any remaining equity after the position is wound down. The goal of the system is to close risk in an orderly way and keep the exchange solvent, not to penalize beyond the collateral at stake. ## Off-hours protection For perps on assets with set trading hours, Arcus applies **price bands** while the underlying market is closed. Trading and the mark price are constrained to a range around the last regular-hours price, and the range only widens gradually if sustained pressure builds against a bound. These bands are designed so that positions are not easily liquidated purely because of thin overnight or weekend liquidity. They give the market time to consolidate and limit how fast the mark price can move, but they do not prevent liquidation outright — a large or sustained off-hours move can still push a position past its maintenance margin. See [Trading when the underlying is closed](/concepts/perpetuals/real-world-assets#trading-when-the-underlying-is-closed). ## Staying clear of liquidation * Keep a buffer of free collateral above your maintenance margin. * Use [reduce-only stop-loss orders](/concepts/perpetuals/order-types#take-profit--stop-loss) to exit on your own terms before maintenance margin is breached. * Remember that opening or increasing RWA positions off-hours costs more margin. * Use [isolated margin](/concepts/perpetuals/margin-modes) to contain a position's risk to its own collateral rather than sharing it across your whole account. # Margin & leverage Source: https://docs.arcus.xyz/concepts/perpetuals/margin How initial and maintenance margin work on Arcus perps, leverage limits, and the higher initial-margin regime when an underlying market is closed Perps are traded on **margin**: you post collateral and control a larger position with leverage. Two thresholds govern every position — **initial margin** (what you need to open or increase a position) and **maintenance margin** (the minimum you must keep to avoid [liquidation](/concepts/perpetuals/liquidations)). This page is conceptual. Live per-market margin and leverage parameters come from [`GET /v1/markets`](/api-reference/public/get-markets); your account's current margin and equity come from [`GET /v1/account`](/api-reference/public/get-account). ## Collateral Collateral is held as **USDG**, the protocol's settlement stablecoin, in your Arcus account. Your account equity is your collateral balance plus the unrealized profit and loss of your open positions. See [Onboarding & deposits](/concepts/onboarding) for how funds arrive as USDG. ## Initial vs. maintenance margin | Term | What it controls | | - | - | | **Initial margin** | The collateral required to open a new position or add to an existing one. Higher initial margin = lower maximum leverage. | | **Maintenance margin** | The minimum collateral a position must retain. If equity falls below this level, the position becomes eligible for [liquidation](/concepts/perpetuals/liquidations). | Each market sets its own initial and maintenance margin requirements based on its risk profile, expressed as a fraction of position notional. ## Cross and isolated margin Arcus supports both **cross margin** (positions share the account's collateral, so the whole balance backs every position) and **isolated margin** (collateral is allocated to a single position, capping its risk to that allocation). Every position is cross margin by default; isolated margin is live and can be enabled per market. See [Cross & isolated margin](/concepts/perpetuals/margin-modes) for how each mode measures risk, how to switch modes, and how to move collateral onto and off an isolated position. ## Leverage Leverage is the inverse of the initial margin requirement. Arcus supports up to **50x** leverage; the maximum is set per market and published with each market's parameters. Higher-volatility or less-liquid markets carry lower maximum leverage. ## Off-hours margin For perps whose underlying has set trading hours (equities, commodity and index ETFs), Arcus **raises the initial margin requirement while the underlying market is closed**. This makes it more capital-intensive to open or grow positions during thin overnight and weekend liquidity, reducing the risk of disorderly moves and liquidation cascades. Maintenance margin is **not** raised off-hours — existing, adequately-margined positions are not pushed toward liquidation simply because the market closed. Crypto perps have no off-hours regime; their margin requirements are constant. ### Crossing the session boundary The uplift is not a one-off charge applied at the close — it is a function of the market's **current** session state, so it switches on at the RTH → off-hours boundary and switches back off when the session reopens: | Transition | Initial margin | Maintenance margin | | - | - | - | | **RTH → off-hours** (close, weekend, holiday) | Rises to `offHoursInitialMarginFraction` | Unchanged | | **Off-hours → RTH** (reopen) | Returns to `initialMarginFraction` | Unchanged | Nothing is snapshotted or carried over: the requirement reverts to exactly the in-session value at the reopen, and the effective maximum leverage recovers with it. Read the current state from [`GET /v1/markets`](/api-reference/public/get-markets) — `isOutsideRth` tells you which of the two fractions is live, and `regularTradingHours` gives the session window (it is `null` for 24/7 crypto markets, which never transition). Across every RWA market today, `offHoursInitialMarginFraction` is **1.5× the in-session `initialMarginFraction`** — a flat 50% increase in the initial-margin requirement while the underlying is closed. Effective maximum leverage drops in step: a market you can open at 10x in-session tops out near 6.7x off-hours, and a 5x market near 3.3x. This is the current configuration rather than a fixed protocol rule — the fractions are per-market, so read both fields from `GET /v1/markets` instead of hard-coding the ratio. Two consequences worth designing for: * **Your buying power changes without you trading.** Because the requirement feeds the margin figures the API reports, `freeCollateral` falls at the RTH → off-hours boundary and recovers at the reopen, with no fill, transfer, or price move involved. Account equity itself is unaffected — only the collateral held against open positions changes. Don't treat a drop across the boundary as an error or a missed fill. * **A position opened at max in-session leverage can sit below the off-hours initial-margin threshold.** That is expected and is **not** a liquidation trigger: liquidation is governed by maintenance margin, which does not move. The position stays open and can still be reduced or closed; what you cannot do until the session reopens is *open or increase* while below the higher requirement. A per-account leverage override does not opt out of the uplift. The override is floored at the market requirement, so off-hours it can only ever raise your requirement, never hold it at the in-session level. See [Trading when the underlying is closed](/concepts/perpetuals/real-world-assets#trading-when-the-underlying-is-closed) for the related funding and price-band behavior. Exact off-hours margin multipliers and per-market requirements are part of each market's published parameters and may change. Always read the live values from the API rather than hard-coding them. # Cross & isolated margin Source: https://docs.arcus.xyz/concepts/perpetuals/margin-modes The two margin modes on Arcus perps — how cross and isolated margin measure risk differently, how to switch modes, and how to move collateral onto and off an isolated position Every perp position runs in one of two **margin modes**, which decide what collateral backs the position and how its risk is contained. * **Cross margin** — positions share your account's collateral, so the whole balance backs every position. Profit on one position can support another; a loss on one draws on the same shared equity. * **Isolated margin** — collateral is allocated to a single position, capping that position's risk to its own allocation. A loss is contained to the collateral you have posted to that leg and cannot pull down the rest of your account. **All positions are cross margin by default.** A position is only isolated after you explicitly enable isolated mode for that market. This page is conceptual. For the underlying account and leverage mechanics see [Margin & leverage](/concepts/perpetuals/margin); read the current mode and effective leverage per market from [`GET /v1/leverages`](/api-reference/public/get-leverage-and-margin-mode-for-an-account-across-all-markets). ## How margin is measured in each mode The `marginUsed` field on a position row reflects the mode, and the two behave differently: | Mode | How `marginUsed` behaves | | | | - | - | - | - | | **Cross** | The initial margin required at the current mark — \` | size | × mark price × initial-margin fraction\`. It **tracks the mark price**, moving up and down as the market moves. | | **Isolated** | The walled-off collateral posted to that position's leg. It does **not** track the mark; it changes **only** when you move collateral in or out via `adjustIsolatedMargin`. | | | In cross mode your used margin floats with the mark; in isolated mode it is a fixed pool you control directly, insulated from the rest of the account. ## Switching modes Set the margin mode with the optional `isolated` flag on [`POST /v1/setLeverage`](/api-reference/exchange/set-leverage-for-a-market): * `isolated: true` switches the market to isolated mode. * `isolated: false` switches it back to cross mode. * Omit the flag to leave the current mode unchanged. A **cross → isolated** switch is rejected with `HAS_OPEN_POSITION` if you already have an open position in that market — close the position first, then switch to isolated mode. The reverse (**isolated → cross**) and any no-op change are always accepted. Changing leverage or margin mode does not affect a position's maintenance margin or liquidation price — those depend on the market's maintenance-margin fraction, not the per-account leverage cap. ## Moving collateral in and out Once a market is in isolated mode with an open position, use [`POST /v1/adjustIsolatedMargin`](/api-reference/exchange/add-or-remove-margin-on-an-isolated-mode-position) to move collateral between your shared cross bucket and the isolated leg: * `amount` is a **dollar decimal string** (e.g. `"100"` for \$100) — send dollars, not quote quantums. * **Positive** adds margin to the leg (cross → isolated); **negative** removes it (isolated → cross); zero is rejected. The move requires the market to be in isolated mode with an open position, otherwise it is rejected with `NOT_ISOLATED` or `NO_OPEN_POSITION`. Adding margin fails with `UNDERCOLLATERALIZED` if the cross bucket lacks the free collateral to fund it; removing margin fails with `UNDERCOLLATERALIZED` if it would leave the isolated leg below its initial-margin requirement. ## Reading the current mode The margin mode is exposed as `marginMode` (`CROSS` / `ISOLATED`) and its boolean twin `isolated` in three places: * On **position rows** (snapshot and streaming). * On [`GET /v1/leverages`](/api-reference/public/get-leverage-and-margin-mode-for-an-account-across-all-markets), alongside the effective leverage for every market. * On the [`accountAttributeUpdates`](/api-reference/channels#accountattributeupdates) WebSocket channel, which folds the margin-mode outcome into leverage entries. ## What it means for liquidation risk The mode determines how far a losing position can reach into the rest of your account: * In **cross** mode, one shared balance backs every position, so a single position's loss draws on the collateral supporting all of them. * In **isolated** mode, a position's collateral and risk are contained to its own leg. A move against that position can only consume the margin you have posted to it — the rest of your account is insulated. Liquidation itself is still governed by maintenance margin the same way in both modes. See [Liquidations](/concepts/perpetuals/liquidations) for how a position becomes eligible for liquidation and what happens when it does. # Order types Source: https://docs.arcus.xyz/concepts/perpetuals/order-types Order types, time-in-force options, and take-profit / stop-loss on Arcus perps, at a conceptual level Arcus runs a central limit order book (CLOB). You place orders against it with an order type and a time-in-force that together control how aggressively the order executes and how long it rests. This page explains the concepts. For exact request fields, enums, and validation rules, see the order endpoints — [`POST /v1/placeOrder`](/api-reference/exchange/place-order), [`cancelOrder`](/api-reference/exchange/cancel-order), [`modifyOrder`](/api-reference/exchange/modify-order) — and note that constraints are enforced server-side, so the spec is authoritative. ## Order types | Type | Behavior | | - | - | | **Limit** | Executes at your specified price or better. Rests on the book if it doesn't fully fill. | | **Market** | Executes immediately against the book. A protective price bound is required so a market order behaves like a marketable limit order and can't fill at a runaway price. | ## Time-in-force Time-in-force (TIF) controls how long an order stays active: | TIF | Meaning | | - | - | | **GTT** (good-til-time) | Rests on the book until filled or until its expiry time. | | **IOC** (immediate-or-cancel) | Fills what it can immediately; cancels any remainder. | | **FOK** (fill-or-kill) | Fills completely and immediately, or is canceled entirely. | | **ALO** (add-liquidity-only) | Posts to the book as a maker; rejected if it would take liquidity. | A few rules worth knowing at the concept level (the API enforces the exact constraints): * Every order carries a **`goodTilTime`** expiry — including `IOC` orders. * **Market** orders must use **`IOC`**. * Resting orders (`GTT`, `ALO`) require an expiry set sufficiently far in the future. ## Modifying orders and queue priority A [`modifyOrder`](/api-reference/exchange/modify-order) replaces the resting order with a new one, which can cost you your place in the queue. The modify response echoes an **`originalSize`** field — the order's size *before* the modify — so you can tell whether priority was kept: * If the **price is unchanged** and **`originalSize` is less than the new size** (you increased the order's size), the order **loses its queue priority** — the added size can't jump ahead of orders already resting behind you. * A same-price modify that leaves the size equal or smaller keeps the order's place in the queue. `originalSize` is only meaningful on the `modifyOrder` response; queue priority is a modify-specific concern. ## Take-profit / stop-loss Take-profit (TP) and stop-loss (SL) are **trigger orders**: they sit dormant until the market reaches a trigger price, then submit an order to close or reduce your position. * A **stop-loss** triggers when the price moves against you, to cap a loss. * A **take-profit** triggers when the price moves in your favor, to lock in a gain. On Arcus, TP/SL legs are **reduce-only** (they can only shrink a position, never flip or grow it) and are submitted through [`POST /v1/batchPlaceOrders`](/api-reference/exchange/place-batch-orders) using a TPSL `grouping` — `positionTpsl` (close the full open position), `partialTpsl` (close a specific quantity), or `entryTpsl` (an entry order plus its bound TP/SL child legs). Each TP/SL leg sets `tpslType` and `stopPrice`, is reduce-only, and follows the TIF rules for its order type — a limit leg uses `GTT`, a market leg uses `IOC`. Single-order TP/SL via `placeOrder` is **not supported** and returns `501` (`NotImplemented`). Validation (required fields, allowed TIF per order type, trigger semantics) is enforced by the gateway and can evolve. Build against the [API reference](/api-reference/introduction) and the [changelog](/changelog) rather than memorizing values here. # Perpetuals overview Source: https://docs.arcus.xyz/concepts/perpetuals/overview What perpetual futures are on Arcus, the asset classes they cover, and how 24/7 trading works across crypto and real-world assets A **perpetual future** ("perp") is a contract that tracks the price of an underlying asset without an expiry date. You take a long or short position with leverage, post collateral as margin, and pay or receive [funding](/concepts/perpetuals/funding) to keep the contract's price anchored to the underlying. There is no settlement date and no delivery of the underlying — you hold the position as long as your margin supports it. This section is conceptual. For request formats, fields, and live values, see the [API reference](/api-reference/introduction). ## What you can trade Arcus offers perps across several asset classes from a single account and a single collateral balance: | Class | Examples | Underlying market hours | | - | - | - | | Crypto | BTC, ETH, SOL, and other digital assets | 24/7 — always liquid, no off-hours regime | | Equities | Single-stock perps (e.g. NVDA, TSLA, AAPL) | US trading hours, with pre/post market | | Commodity ETFs | Gold, silver, oil exposure (e.g. GLD, SLV, USO) | The referenced market's hours | | Index ETFs | Broad-market exposure (e.g. SPY, QQQ) | The referenced market's hours | New markets are added on an ongoing basis. The tradable set and per-market parameters (leverage, tick/step sizes) are served by the [`GET /v1/markets`](/api-reference/public/get-markets) endpoint. ## Always-on markets All Arcus perps trade **24/7** — including overnight, weekends, and holidays. Liquidity is deepest when the underlying market is open and can thin outside those windows, which widens spreads. Crypto perps run continuously with no special handling. For perps whose underlying has set trading hours (equities, commodity and index ETFs), Arcus applies an **off-hours regime** when that market is closed: * **Funding** switches from a live, market-driven rate to a fixed off-hours rate. * **Initial margin** requirements rise, so opening or growing a position off-hours takes more collateral. * **Price bands** limit how far the market can move while the underlying is closed, to curb liquidation cascades in thin liquidity. See [Funding](/concepts/perpetuals/funding) and [Margin](/concepts/perpetuals/margin) for how each behaves. ## How the pieces fit together Initial vs. maintenance margin, leverage, and the off-hours margin regime. The two margin modes, how to switch, and moving collateral onto a position. What happens when a position can no longer support its margin. The mark price that drives risk, and the oracle that anchors it. How perps stay anchored to the underlying, including real-world assets. 24/7 leveraged exposure to equities, commodities, and indices. Order types, time-in-force, and take-profit / stop-loss. Trading fees and the fee-tier model. # Prices Source: https://docs.arcus.xyz/concepts/perpetuals/prices The prices an Arcus perp market tracks — the mark price that drives all risk, the oracle price that anchors it, and the order-book inputs that feed it An Arcus perp references a few different prices, and it helps to know which does what: * **[Mark price](#mark-price)** — the exchange's fair-value estimate for the perp. It's the number your position is marked at and the **sole reference for margin, PnL, and [liquidations](/concepts/perpetuals/liquidations)**. Start here. * **[Oracle price](#oracle-price)** — an external view of the underlying's fair value. It's an *input* to the mark price and drives the [funding](/concepts/perpetuals/funding) basis; it does not value your position directly. * **Order-book prices** — the last trade price, the best bid and ask, and the *impact mid* (the average price to fill a fixed reference size on each side). These feed the mark price. This page is conceptual. Live prices are available from [`GET /v1/prices`](/api-reference/public/get-live-prices-for-all-markets) and the market-data [WebSocket channels](/api-reference/channels). ## Mark price The **mark price** is the exchange's fair-value estimate for a perp, and it is the **sole valuation reference** for risk: margin, unrealized PnL, and [liquidations](/concepts/perpetuals/liquidations) are all computed from it — there is no oracle fallback. It's also the number the off-hours [price bands](/concepts/perpetuals/real-world-assets#trading-when-the-underlying-is-closed) constrain. It blends the oracle price with the perp's own order book, so it tracks where the contract is actually trading without drifting far from fair value. Two inputs feed the calculation: * **Impact mid** — the midpoint of the *impact bid* and *impact ask*: the average price to fill a fixed reference size on each side of the book. Using impact prices rather than the top of book keeps the mark robust to a single thin quote. * **Oracle price** — the external reference described [below](#oracle-price), available for crypto around the clock and for RWA perps during the underlying's regular trading hours (RTH). ### Markets with a live oracle Crypto perps (24/7) and RWA perps **during RTH**. The mark price is the **average of two components**, then clamped to a tight band around the oracle: 1. **Order-book price** — the **median** of the last trade price, the best bid, and the best ask. 2. **Oracle-anchored price** — the oracle price plus a **2.5-minute EWMA** of (impact mid − oracle price). This tilts the mark toward where the perp is actually trading. The average of the two is then **clamped to within ±min(10 ÷ leverage, 1)% of the oracle price** — at most ±1%, and tighter for higher-leverage markets. The clamp bounds the published mark's total deviation from the oracle; it is *not* applied to the EWMA term on its own. (If there's no recent trade, component 1 drops out and the mark is component 2 alone, still clamped.) ### Markets with no live oracle RWA perps **outside RTH**, when the underlying is closed and no reliable external quote exists. The mark price is the **2.5-minute EWMA of the impact mid** alone (no oracle clamp), bounded by the off-hours [price bands](/concepts/perpetuals/real-world-assets#trading-when-the-underlying-is-closed). In this regime [funding is also locked](/concepts/perpetuals/funding#off-hours-funding) to the base rate. The reference size behind the impact mid, and the exact band parameters, are per-market risk settings read from the API. Treat the API's published prices as the source of truth rather than recomputing the mark yourself. ## Oracle price The **oracle price** represents the fair value of the *underlying*. It anchors the mark price (above) and drives the [funding](/concepts/perpetuals/funding) basis. How it's sourced depends on whether a reliable external price is available right now. | Situation | Oracle price source | | - | - | | **Underlying market open** (or a 24/7 crypto market) | Derived from external market data for the underlying. | | **Underlying market closed** (RWA off-hours) | Derived from the exchange's own mark price, since no reliable external quote exists. | For crypto, a market is effectively always "open," so the oracle tracks external crypto pricing continuously. ### External pricing for real-world assets For equities, commodity ETFs, and index ETFs, Arcus references external market data for the underlying while that market is trading. Because cash instruments settle on a delay in traditional markets, the external price is adjusted to a consistent same-day basis so the perp tracks the economics a trader actually experiences. This adjustment is small in normal conditions. For markets built on futures (such as some commodities), Arcus follows a roll schedule and accounts for the difference between the contracts being rolled out of and into, so the oracle reflects continuous exposure rather than jumping on roll dates. The economic effect of rolls is passed through [funding](/concepts/perpetuals/funding) rather than appearing as an unexplained price gap. # Real-world asset perps Source: https://docs.arcus.xyz/concepts/perpetuals/real-world-assets How Arcus perps on equities, commodities, and indices give 24/7 leveraged exposure that tracks the total return of the underlying — including dividends, splits, and futures rolls Alongside crypto, Arcus offers perps on **real-world assets (RWAs)** — equities, commodity ETFs, and index ETFs. They work like crypto perps (a CLOB, [margin](/concepts/perpetuals/margin), [funding](/concepts/perpetuals/funding), and [liquidations](/concepts/perpetuals/liquidations)), but they reference assets that trade on a schedule and whose returns come from more than just price. Arcus is designed so an RWA perp tracks the **total return** of its underlying, around the clock. This page is educational and conceptual. The exact risk parameters (margin multipliers, price-band widths, funding intervals) are part of each market's configuration and are read from the API, not documented here. ## Why RWA perps * **24/7 exposure.** The underlying stock or commodity market is open only part of the week; the perp lets you take or adjust a position any time — overnight, weekends, holidays. * **Leverage and shorting.** Go long or short with [leverage](/concepts/perpetuals/margin), from the same USDG collateral you use for crypto. * **One account, many asset classes.** Crypto, equities, commodities, and indices share a single balance and margin system. ## Tracking total return Holding a stock or commodity in the traditional world earns (or costs) more than the change in its quoted price — dividends, corporate actions, and the shape of the futures curve all matter. Arcus passes these through so the perp reflects what a holder of the underlying would actually experience, rather than drifting away from it. The adjustments flow through [funding](/concepts/perpetuals/funding) and the [oracle price](/concepts/perpetuals/prices#oracle-price). ### Dividends When a stock goes ex-dividend, its market price drops by roughly the dividend. On its own that would show up as a loss for a long. Arcus offsets it with a funding payment to longs, so the dividend isn't lost — it's reflected in the position's return. > *Illustrative:* a stock priced at \$100 pays a \$2 dividend and opens at \$98. A long sees -\$2 from the price move and +\$2 from funding — net flat from the dividend itself, exactly as a holder of the stock would experience. ### Splits On a stock split or reverse split, open orders and a position's price reference are adjusted by the split factor so the economics are unchanged. > *Illustrative:* in a 2:1 split, a position of 100 units at \$10 becomes 200 units at \$5 — same exposure, same value. ### Futures rolls For markets built on futures, the contract being referenced changes on a roll schedule. Arcus accounts for the price difference between the contract rolled out of and the one rolled into and passes it through funding ("roll payment"), so your exposure stays continuous instead of jumping on roll dates. ## Trading when the underlying is closed Outside the underlying's regular hours, liquidity is thin and there's no reliable external price to anchor to. Arcus keeps the market open but adds guardrails so off-hours trading stays orderly and positions are not easily liquidated purely because of thin overnight or weekend liquidity: | Guardrail | What it does off-hours | | - | - | | **Fixed funding** | Funding switches from the live, market-driven rate to a fixed base rate (SOFR + 0.5%), set at the close — so carrying a position overnight or over a weekend has a predictable funding cost. | | **Higher initial margin** | Opening or increasing a position requires more collateral; maintenance margin is unchanged. The requirement rises at the close and returns to its in-session value at the reopen. See [Margin](/concepts/perpetuals/margin#crossing-the-session-boundary). | | **Price bands** | Trading and the mark price are limited to a range around the last regular-hours price, widening only gradually under sustained pressure — curbing [liquidation](/concepts/perpetuals/liquidations) cascades in thin markets. | | **Internal mark price** | With no live external quote, the [mark price](/concepts/perpetuals/prices#mark-price) is a 2.5-minute EWMA of the order book's impact mid, bounded by the price bands. | ### How price bands widen When a market leaves its regular-hours window, the engine **seals a settlement anchor** — `currentSettlementPrice`, a VWAP of the closing session — and centers the band on it. The band starts narrow and can only widen in fixed steps, a **`0.5x` → `1x` → `2x` → `4x`** multiplier ladder, under sustained one-directional pressure. Each edge sits at: ``` upperTradingBound = currentSettlementPrice × (1 + initialMarginFraction × bandMultiplier) lowerTradingBound = currentSettlementPrice × (1 − initialMarginFraction × bandMultiplier) ``` Because the width scales with `initialMarginFraction`, how far price can travel inside the band scales **inversely with leverage** — a lower-leverage (better-collateralized) market is allowed a wider move. At the `4x` ceiling, for example: | Max leverage | Allowed move from the settlement anchor | | - | :-: | | 10x | up to ±40% | | 5x | up to ±80% | The anchor is **per session**: a fresh anchor seals at each **weekday RTH close (20:00 ET, Monday–Friday)**, where the previous band is discarded and both sides reset to the `0.5x` rung around it. There is no weekend re-seal — Friday's 20:00 ET close carries the whole weekend, until regular hours reopen Monday at 04:00 ET. Within a session the ladder is **one-way** — an expanded side never contracts, even if price fully recovers; it only resets when the next session's anchor seals. The **upper and lower sides expand independently** — each carries its own multiplier and its own clock, so in a one-sided run one edge can reach `4x` while the other is still at `0.5x`. Each step up is gated by a **1-hour sustained-pressure clock**. A side *enters its expansion zone* when the 1-hour EWMA of that side's impact price **reaches or passes 90% of the distance from the anchor to the edge — including prices beyond the edge itself**. It is a one-sided threshold, not a bounded strip: the zone is everything at or past the 90% mark, with no upper limit (a price already trading through the edge is still in-zone, driving the clock toward expansion). On a 10x market at the first rung, for example, the upper zone starts at `anchor × 1.045` — 90% of the way to the `anchor × 1.05` edge — and extends through the edge and beyond. The zone edges are not separate API fields, but derive directly from the formula: `anchor × (1 ± 0.9 × initialMarginFraction × bandMultiplier)`. Entering the zone starts the clock; if the EWMA stays in the zone for a continuous hour (with resting depth still present), that side widens one rung. If it eases out of the zone first, the clock resets and no expansion happens. There is **no halt state** — the market never stops, and trading inside the band is never restricted. The bound is enforced per order: a crossing order fills as far as the boundary allows, and only the portion that would execute at or beyond it is rejected (`FILL_WILL_EXCEED_TRADING_BOUND`) — so a run can't blow through every level at once. Here is one full off-hours session on a 10x market (anchor `65.71`, first rung ±5%), showing a single lower-side expansion: Price chart of one off-hours session: settlement anchor at 65.71 with trading bounds at ±5% and next bounds at ±10%; price drops into the lower expansion zone (at or past 90% of the way to the lower edge), holds for an hour, and the lower bound steps from −5% to −10% while the next lower bound steps to −20%; price then recovers but the widened bounds remain; the upper side is untouched throughout. The same lifecycle as a state machine — note the two very different ways back from the zone: easing out resets only the *clock*, never the band; rungs are permanent for the session: ```mermaid theme={null} flowchart LR Start([Market exits RTH
anchor seals, both sides at 0.5×]) --> Rest["Band holds at its current rung
(rungs only ever increase)"] Rest -->|"impact-price EWMA enters the zone
→ ZoneEnteredAt stamped, 1h clock starts"| Zone["In expansion zone
clock running"] Zone -->|"EWMA eases out before the hour
→ clock resets — band UNCHANGED,
it does not narrow"| Rest Zone -->|"in zone for the full hour
→ widen one rung, permanent
for the session"| Rest Rest -->|"at the 4× ceiling"| Ceil([No further widening
next…TradingBound == …TradingBound]) ``` The diagram shows **one side**. The upper and lower sides run this state machine independently and can be on different rungs at the same time. The whole machine restarts — new anchor, both sides back at `0.5×` — only when the next session's anchor seals. Crypto perps have **no** off-hours regime — their markets never close. ### Reading the band from the API The band is fully observable. The [`markets` fields](/api-reference/public/get-markets) below are read from `GET /v1/markets` (and the same values ship on the streaming `markets` channel). Off-hours band state and **every off-hours transition** stream on the dedicated `marketAttributes` channel — a per-market snapshot on subscribe, then that market's full updated block on each event: RTH⇄off-hours crossings (`isOutsideRth`; band fields populate on the off-hours exit and return to `null` on the next open), VWAP seals, and the finer bound events (a side entering or leaving its zone, or a bound expanding — `boundEvent`: `expansionZoneEntered`, `expansionZoneExited`, `tradingBoundExpanded`). It supersedes the older per-market `marketAttributes` entries on the `exchangeAttributeUpdates` channel. **These fields carry a value only while the underlying is closed.** The keys are **always present** on every RWA market; each holds a real value only when the market is outside RTH with a sealed anchor (`isOutsideRth` is `true`), and reads **`null`** otherwise — during regular trading hours, and for **24/7 crypto markets**, which have no settlement anchor and no band. Decide whether the band applies by checking `isOutsideRth` (or a non-`null` `currentSettlementPrice`) — do **not** test for the field's absence, as the key is always there. The four zone-timing fields are `null` unless their own side's `is…InExpansionZone` is `true`. Bound values are also **per session**: at every weekday 20:00 ET re-seal, all of them recompute against the fresh anchor. Compare the `is…InExpansionZone` flags only against bounds read in the same snapshot — against bounds cached from an earlier session they will look wrong, and a flag can legitimately flip `true` at the boundary itself if price is already inside the new, narrower band's zone. | Field | Meaning | Non-`null` when | | - | - | - | | `currentSettlementPrice` | Sealed VWAP anchor the band is centered on, fixed at the RTH close. | Off-hours + sealed anchor | | `upperTradingBound` | Current upper edge. Fills at or above it are rejected until the side expands. | Off-hours + sealed anchor | | `lowerTradingBound` | Current lower edge. Fills at or below it are rejected until the side expands. | Off-hours + sealed anchor | | `nextUpperTradingBound` | Where the upper edge moves if it expands one rung; equals `upperTradingBound` at the `4x` ceiling. | Off-hours + sealed anchor | | `nextLowerTradingBound` | Where the lower edge moves if it expands one rung; equals `lowerTradingBound` at the `4x` ceiling. | Off-hours + sealed anchor | | `isUpperInExpansionZone` | Upper side's impact EWMA is at or past 90% of the way to its edge — including beyond the edge (clock running). | Off-hours + sealed anchor | | `isLowerInExpansionZone` | Lower side's impact EWMA is at or past 90% of the way to its edge — including beyond the edge. | Off-hours + sealed anchor | | `upperZoneEnteredAt` | Unix seconds the upper side entered its zone (clock start); constant while in-zone. | `isUpperInExpansionZone` is `true` | | `upperExpectedExpansionAt` | Unix seconds of the **earliest** the upper edge can widen — exactly one hour after entry. Not a guarantee: the EWMA must still be in the zone with depth when the hour completes. | `isUpperInExpansionZone` is `true` | | `lowerZoneEnteredAt` | Unix seconds the lower side entered its zone. | `isLowerInExpansionZone` is `true` | | `lowerExpectedExpansionAt` | Unix seconds of the earliest the lower edge can widen. | `isLowerInExpansionZone` is `true` | There is no single combined `currentTradingBound` / `nextTradingBound` in the API: because the two sides move independently, the "current" and "next" bound are reported per side as the `upper…` / `lower…` pairs above. ## Related concepts How dividends, splits, rolls, and off-hours rates are applied. The mark price that drives risk, and how the oracle is sourced open vs. closed. Collateral, leverage, and the off-hours margin regime. What triggers a liquidation and how off-hours bands help. # Spot integration Source: https://docs.arcus.xyz/concepts/spot-integration What it takes to integrate the Arcus Spot Router — embedding Stock Token trading in a wallet or app, joining the RFQ maker network, builder-code economics, and answers to common integrator questions. **The Spot Router is built to be integrated.** Wallets and applications can offer Stock Token trading through a single integration while Arcus handles pricing, routing, execution, and settlement; professional market makers can connect to provide RFQ liquidity. This page covers what integrating looks like and answers common questions — for how routing works under the hood, see [Spot Router](/concepts/spot-router). Integrating requires an API key issued by Arcus, so getting in touch is the first step. To request access or discuss an integration, [contact the Arcus team](mailto:vip@arcus.xyz). ## What you can build Embed Stock Token trading directly in your own interface. You keep the frontend and the user relationship; Arcus provides pricing, routing, execution, and settlement behind it. Connect to the RFQ network to quote against taker flow from Arcus and integrated partners. You choose which tokens to quote. ## What a single integration provides Rather than building and maintaining a connection to each liquidity source, a partner integrates once and gets access to: * **The Arcus RFQ network** — firm, signed quotes from professional makers, including liquidity for long-tail names that lack deep on-chain pools. * **On-chain AMM liquidity** and **external liquidity venues**, evaluated together for best execution. * **The full Stock Token universe** — hundreds of tokenized equities, without sourcing and integrating market makers for each one. The live list is served by the router (`GET /v1/tokens`). * **Gasless, self-custodial trading** — users trade from their own wallet without needing a separate gas token (native-asset purchases excepted). ## Builder codes and economics Each integration is issued a dedicated API key. Volume routed through the key is attributed to the partner, and a partner (builder) fee can be configured against it — so partners can earn revenue from trading activity within their own product. Fees and attribution are set up with the Arcus team during onboarding. ## Environments | Network | Chain ID | Use | | - | - | - | | Robinhood Chain mainnet | `4663` | Production trading. | | Robinhood Chain testnet | `46630` | End-to-end integration testing in a sandbox. | ## FAQ [Reach out to the Arcus team](mailto:vip@arcus.xyz). Integrations require an API key (which also carries your builder-code configuration), so getting in touch is the first step; you'll receive SDKs, a testnet sandbox, and integration support. Hundreds of tokenized stocks, plus other supported markets. The canonical, live list is served by the router at `GET /v1/tokens` (filterable by chain), so there is no static list to maintain. For most trades, no — settlement is gasless and the router submits the transaction. Buying with the chain's native asset is the exception, where the user broadcasts and pays gas. Yes. Trading is fully self-custodial — users hold their Stock Tokens and authorize every trade with their own signature. See [self-custody](/concepts/spot-overview#self-custody). The router evaluates available liquidity across sources and executes each trade against the best available route, chosen at submission time. See [Spot Router](/concepts/spot-router) and [Spot RFQ](/concepts/spot-rfq). Through builder codes: volume routed through your API key is attributed to you, and a partner fee is configured against the key. See [Builder codes and economics](#builder-codes-and-economics). Yes — Robinhood Chain testnet (chain ID `46630`) for end-to-end validation before going live. [Reach out to the Arcus team](mailto:vip@arcus.xyz). Makers run a maker server that receives RFQ requests for the tokens they choose to quote, and respond with firm, signed quotes. See [Spot RFQ](/concepts/spot-rfq) for the model. ## Learn more How the router aggregates liquidity and settles each trade. The request-for-quote model behind RFQ liquidity. # Stock Tokens Source: https://docs.arcus.xyz/concepts/spot-overview What Stock Tokens are on Arcus — self-custodied, on-chain tokenized equities traded spot with zero fees, 24/7, and how spot differs from perps **Stock Tokens** are on-chain, tokenized representations of equities that you own outright and trade **spot** — no leverage, no [funding](/concepts/perpetuals/funding), no [liquidations](/concepts/perpetuals/liquidations). Spot trading is **free at launch** and runs **24/7**. This is distinct from [perps](/concepts/perpetuals/overview), where you take leveraged exposure against margin rather than holding the asset. This page is conceptual. For how spot trades are priced and settled, see [Spot RFQ](/concepts/spot-rfq); for how long-tail names are represented, see [Wrapped tokens](/concepts/wrapped-tokens). ## Self-custody Spot is **non-custodial**: your Stock Tokens live in your own wallet, and trades settle directly on-chain between wallets. Arcus never holds your funds or keys. Because you sign for what you own, trading a Stock Token from a self-custodial wallet prompts you to **sign each trade**, plus a **one-time authorization** the first time you interact with a given token. ## How Arcus sources liquidity Arcus does not run a spot order book of its own. It acts as a **router**: for each trade it sources liquidity from the venues available for that token and executes against the best-priced option, always settling on-chain and non-custodially to your wallet. Two kinds of venue feed the router: * **On-chain AMM liquidity.** Automated market-maker pools that hold the tokenized asset on-chain. The router trades into a pool when it offers the best price. More on-chain venues are being added over time. * **The Arcus RFQ network.** A [request-for-quote (RFQ)](/concepts/spot-rfq) service where professional makers who have integrated with Arcus return firm, signed quotes on demand. The best quotes settle atomically alongside your signed intent. For a given order the router may draw on either source — whichever gives you the better execution. ## How trading and delivery work Whichever venue fills your order, the result settles atomically in a single on-chain transaction — slippage-bounded and non-custodial. What you actually receive depends on liquidity in the underlying token: * **Real token (preferred).** When a native fill against the tokenized equity is available, you receive the real token directly. * **Wrapped placeholder (fallback).** For long-tail names where no native fill is available, a whitelisted maker mints a [wrapped token](/concepts/wrapped-tokens) (e.g. `wTSLA`) that behaves like the underlying in your account and is automatically settled to the real token on the maker's cadence — no action needed from you. Either way you hold spot exposure to the equity. A wrapped token is a **delivery mechanism for a Stock Token**, not a separate product. ## Backing and custody Stock Tokens are issued by **Bitstamp** — minted by Bitstamp Global Ltd. (BVI), a custodial virtual-asset and securities exchange holding VASP/CASP, MiFID, and other licenses. Arcus is the trading venue, not the issuer. Each Stock Token is backed by the issuer's reserves of the underlying equity — real shares held in regulated custody — or cash and equivalents when the underlying is on loan. The issuer's reserves are intended to be publicly attestable, and wrapped-token supply and escrow balances are verifiable on-chain in real time. Holding a Stock Token gives you a **contractual right to redeem with the issuer for the cash market value** of the linked security — not the share certificate itself. Economic exposure passes through, but not every shareholder right: * **Dividends** are not paid out directly; their value is reinvested into the underlying backing the token, and your displayed balance adjusts via an on-chain oracle update — no action needed. * **Stock splits** work the same way: an oracle update adjusts every holder's displayed balance. * **Voting rights** are not passed through. The tokens themselves are yours to hold, transfer, or trade — see the [disclaimer](/concepts/disclaimer) for the risk profile relative to traditional stock ownership, and the [Help Center](https://help.arcus.xyz) for the full Stock Token FAQ. ## Spot vs. perps | | Stock Tokens (spot) | Perpetuals | | - | - | - | | What you hold | The tokenized asset, in your wallet | A leveraged position against margin | | Leverage | None | Up to 50x | | Funding / liquidations | None | Yes | | Custody | Self-custodied on-chain | Collateral in on-chain settlement contracts | | Fees | Free at launch | Maker/taker [fee tiers](/concepts/perpetuals/fees) | ## Control and limits Arcus is non-custodial and cannot move or seize the Stock Tokens you self-custody. Two narrow, network-level exceptions exist: transfers to sanctioned addresses are blocked by compliance controls, and a [wrapped](/concepts/wrapped-tokens) placeholder can be force-settled to its real token (which only ever delivers what you are owed). ## Learn more How spot trades are quoted and settled atomically. How long-tail equities are represented until settled. # Spot RFQ Source: https://docs.arcus.xyz/concepts/spot-rfq How Arcus prices and settles spot trades through a request-for-quote flow with off-chain makers and atomic on-chain settlement **Request-for-quote (RFQ)** is one of the liquidity sources the Arcus [spot router](/concepts/spot-overview#how-arcus-sources-liquidity) draws on — alongside on-chain AMM pools — when filling a spot trade. In the RFQ flow a taker asks for a price, professional makers respond with firm quotes, and the server settles the **single best quote** against the taker's signed intent in **one atomic settlement transaction**. Takers sign an intent that caps their downside; makers sign quotes that bind their terms; nothing settles unless every leg is satisfied at once. This page explains the model and the message flow. It is conceptual — for the wire formats and endpoints, see the [API reference](/api-reference/introduction). ## Why RFQ Spot markets for tokenized assets are often thin and fragmented. RFQ lets the exchange source liquidity from many makers on demand and pick the best quote per trade, instead of requiring makers to post resting depth. The taker gets one signed, slippage-bounded outcome; the maker only commits capital when its quote is actually selected. ## Roles | Term | Meaning | | - | - | | **Taker** | The party requesting to trade — typically the end user or their wallet. | | **Maker** | A liquidity provider that prices and fulfills the RFQ. | | **Server** | Relays messages between the taker and makers, and submits the settlement transaction on-chain. | | **Settlement** | The on-chain contract that finalizes the trade atomically. | | **Indicative liquidity** | Non-binding price/size information from a maker; used to build the intent. | | **Intent** | The trade the taker commits to, including `minBuyAmount`; signed by the taker. | | **Firm quote** | A binding price and terms with an expiry — distinct from indicative liquidity. | ## The flow The taker sends quote parameters, including a `slippageBps` tolerance. The server requests **indicative liquidity** from makers, derives the `minBuyAmount` floor from `slippageBps` and the quoted price, and returns the intent for the taker to sign. `slippageBps` is validated server-side — invalid values are rejected. The taker signs the intent (EIP-712 typed data) and returns it to the server. The server forwards the intent to makers **without** `minBuyAmount`, so makers cannot price against the taker's limit. Each maker quotes the **entire `sellAmount`**; the server ranks the firm quotes by output amount and discards any below `minBuyAmount`. The server bundles the signed taker intent with the **single winning firm quote** and submits **one transaction** to the settlement contract. Every leg is atomic — all fill or none do. ```mermaid theme={null} sequenceDiagram participant Taker participant Server participant MakerA as Maker_A participant MakerB as Maker_B participant Settlement Taker->>Server: quote request Server->>MakerA: price request Server->>MakerB: price request MakerA->>Server: indicative liquidity MakerB->>Server: indicative liquidity Server->>Server: build intent Server->>Taker: intent Taker->>Taker: sign intent Taker->>Server: signed intent Server->>MakerA: intent without minBuyAmount Server->>MakerB: intent without minBuyAmount MakerA->>Server: firm quote MakerB->>Server: firm quote Server->>Server: select best quote Server->>Settlement: submit intent plus winning quote Settlement-->>Server: tx ack Server-->>Taker: fill status ``` ## Quote parameters What the taker sends to request a quote. The server forwards the same shape to each maker. | Param | Type | Required | Description | | - | - | :-: | - | | `chainId` | positive integer | Yes | Chain where settlement will occur. | | `sellToken` | address | Yes | Token the taker is selling. | | `buyToken` | address | Yes | Token the taker is buying. | | `sellAmount` | positive integer string | Yes | Amount of `sellToken` in base units (fixed sell size). | | `taker` | address | Yes | Taker wallet address; included in the intent and signatures. | | `slippageBps` | integer | No | Slippage tolerance in basis points; the server derives `minBuyAmount` from it. | Slippage protection is set with `slippageBps`: the server derives the taker-signed `minBuyAmount` floor from it (invalid values are rejected) and enforces that floor when ranking firm quotes. ## Intent The server builds the intent from maker indicative liquidity and returns it to the taker for signing. | Field | Purpose | | - | - | | `chainId` | Target chain for settlement. | | `sellToken` / `buyToken` | Asset addresses. | | `sellAmount` | Amount of `sellToken` in base units. | | `minBuyAmount` | Minimum `buyToken` the taker will accept — derived server-side from `slippageBps`, and **never** sent to makers. | | `taker` | Taker wallet address. | | `nonce` | Replay protection for the intent signature. | | `deadline` | Unix timestamp after which the intent is void. | | `toSign` | EIP-712 typed data (and/or permit structure) the taker must sign. | Correlation between the intent, each maker's firm quote, and the fill status uses a **router-issued per-maker `quoteId`** (bytes32) — the taker does not sign it, and each maker sees only its own. `minBuyAmount` is the taker's protection against bad fills. Because it stays server-side, makers price the trade on its own merits and the server enforces the floor when ranking firm quotes. ## Firm quote A firm quote is a maker's binding response. Each maker quotes the **entire `sellAmount`** — quotes are never split — and the server settles at most one winning quote per round. | Field | Purpose | | - | - | | `quoteId` | Echoes the router-issued per-maker id; correlates the firm-quote round and fill status. | | `maker` | Maker signer address; the server recovers `signature` and checks it matches. | | `signedPayload` | EIP-712 typed data the maker signed (a Permit2 `PermitWitnessTransferFrom` with an `ArcusRfqQuote` witness). | | `signature` | The maker's EIP-712 signature over `signedPayload`. | Trade terms are bound inside `signedPayload.message.witness`: | Witness field | Purpose | | - | - | | `quoteId` | Must match the request's router-issued id. | | `taker` | Taker wallet from the intent. | | `maker` | Must match the envelope `maker` and the signature. | | `makerBuyToken` / `makerSellToken` | Asset addresses from the maker's view — the maker buys what the taker sells (`makerBuyToken == sellToken`, `makerSellToken == buyToken`). | | `makerBuyAmount` / `makerSellAmount` | Sizes in base units. v1 is fixed-sell: `makerBuyAmount` must equal the intent's `sellAmount`; `makerSellAmount` is the maker's firm output. | | `nonce` | Binds to the Permit2 `message.nonce`. | | `validUntil` | Unix seconds after which the quote is void; bound to the Permit2 `message.deadline`. | `chainId` lives in `signedPayload.domain.chainId`. The Permit2 `message.deadline` must stay valid through settlement — makers enforce a minimum time-to-live so the server can submit before it lapses. ## Settlement guarantees * **Atomic.** The signed intent and the winning firm quote settle in one transaction. If any leg fails, the whole trade reverts. * **Slippage-bounded.** The server only submits when the winning quote's output meets the taker's `minBuyAmount`. * **Non-custodial.** Token movements are authorized by the taker's intent signature and each maker's Permit2 signature — no party pre-deposits funds with the exchange. # Spot Router Source: https://docs.arcus.xyz/concepts/spot-router How the Arcus Spot Router aggregates on-chain AMM and RFQ liquidity behind one integration, routes each spot trade to the best execution, and settles on-chain, gasless in most cases. **The Spot Router** is the aggregation and execution layer behind spot trading on Arcus. Through a single integration it evaluates liquidity across on-chain AMMs and the [Arcus RFQ network](/concepts/spot-rfq), selects the best available route for each trade, and settles it on-chain — non-custodially and, in most cases, gasless. Wallets and applications integrate once instead of building and maintaining a connection to each liquidity source. This page is for integrators. For what Stock Tokens are and how they settle to your wallet, see [Stock Tokens](/concepts/spot-overview); for the request-for-quote model, see [Spot RFQ](/concepts/spot-rfq). ## Liquidity sources The router aggregates liquidity from several sources and executes each trade against the source — or, for on-chain pools, the combination of pools — that offers the best price: | Source | How it works | | - | - | | **Arcus RFQ network** | Professional makers return firm, signed quotes on demand. RFQ also provides liquidity for long-tail names that lack deep on-chain pools. See [Spot RFQ](/concepts/spot-rfq). | | **On-chain AMMs** | Uniswap v2, v3, and v4 pools. The router trades into a pool — or splits across pools — when they offer the best execution. | | **External venues** | The router can also route to select external liquidity venues to widen the routing set. | For each trade the router selects the best available execution across these sources, and may source on-chain AMM liquidity from more than one pool. ## How a trade flows The integration requests a quote (`GET /v1/quote`) for a given sell token, buy token, and size. The router returns the best available route and a payload for the user to sign. Use `GET /v1/price` for indicative pricing without a signable payload. The user signs the returned intent (EIP-712 typed data) from their own wallet. Signing commits to a slippage-bounded outcome, not to a specific route. The integration submits the signed payload (`POST /v1/submit`). The router evaluates liquidity **at execution time** and picks the best route then — so the time a user spends approving does not constrain which sources can fill the trade. The trade settles atomically on-chain. Settlement is [gasless](#gasless-and-self-custodial) for token-to-token trades — the router submits the transaction — so the user does not need to hold a gas token. Track a submitted trade with `GET /v1/status`. Because the route is chosen at submission rather than locked in at quote time, the router can use whatever liquidity is best when the trade actually executes. ## Gasless and self-custodial Users remain self-custodial throughout: Stock Tokens stay in the user's wallet and every trade is authorized by the user's own signature (see [self-custody](/concepts/spot-overview#self-custody)). For most trades the router submits the settlement transaction on the user's behalf, so no separate gas token is required. Buying with the chain's native asset is the exception — there the user broadcasts the transaction and pays gas. ## API surface The router is served at `https://router.spot.arcus.xyz`. | Endpoint | Purpose | | - | - | | `GET /health` | Service health check. | | `GET /v1/tokens` | Live list of supported tradable tokens; filterable by `chainId`. This is the canonical source — there is no static list to maintain. | | `GET /v1/price` | Indicative pricing for a prospective trade. | | `GET /v1/quote` | Best route and the payload for the user to sign. | | `POST /v1/submit` | Submit the signed payload for atomic on-chain settlement. | | `GET /v1/status` | Track a submitted trade. | Supported chains: **Robinhood Chain mainnet** (`chainId` `4663`) and **testnet** (`chainId` `46630`). ## Partner economics Each integration is issued a dedicated API key. Volume routed through the key is attributed to that partner, and a partner (builder) fee can be configured against the key. Fee configuration and attribution are set up with the Arcus team during integration onboarding rather than passed per trade. ## Integrate Stock Token trading can be embedded directly in a wallet or application: the partner keeps its own interface and user relationship while the router handles pricing, routing, execution, and settlement. RFQ market makers can connect to the network to quote against incoming taker flow. SDKs, reference implementations, and the maker protocol specification are provided during onboarding. To start an integration or join the RFQ network, contact the Arcus team. ## Learn more What Stock Tokens are and how they settle to your wallet. The request-for-quote model behind the router's RFQ liquidity. # Subaccounts Source: https://docs.arcus.xyz/concepts/subaccounts How Arcus subaccounts work — the (wallet address, accountIndex) model, up to 10 isolated silos per wallet, API-key scoping, and same-wallet internal transfers Every Arcus account is addressed by a pair: the **wallet address** and an **`accountIndex`**. A single wallet can hold up to **10 subaccounts**, one per index from `0` to `9`. Index `0` is the primary account; indexes `1`–`9` are additional subaccounts you can use to separate strategies, books, or risk. An `accountIndex` outside the `0`–`9` range is rejected with HTTP 400 — the one exception is API-key scope, where `255` means all subaccounts (see below). In signed payloads the field is abbreviated `ai`. ## Implicit creation There is **no create-subaccount endpoint**. A subaccount comes into existence the first time it is used — when you scope an API key to it, deposit into it, or transfer funds into it. Until then it has no state: `GET /v1/account` returns **404** with `"this account has no activity yet"`, and the same address and index return `200` after the first event lands. ## Each index is an isolated silo Every `accountIndex` is a self-contained account. It keeps its **own collateral balance**, its **own rate-limit pools**, and its **own volume credit** for fee tiering. Positions, orders, and free collateral under one index are independent of every other index on the same wallet. Because rate limits are tracked per subaccount, spreading activity across indexes gives each index its own budget. See [Rate limits](/api-reference/rate-limits) for how the pools are counted. Subaccounts and internal transfers are currently available **through the API only** — they are not yet exposed in the Arcus frontend / web console. ## API keys and `accountIndex` An API key's scope is set when the key is created: an `accountIndex` of `0`–`9` binds it to that **single** subaccount, and `255` scopes it to **all** of the wallet's subaccounts (the default for web/mobile trading keys). A single-subaccount key's order writes — place, cancel, modify, and batch operations — are authorized against its bound index, and a mismatch is rejected with HTTP 403. Account-scoped **reads** take an optional `accountIndex` query parameter and default to `0` when it is omitted. The one exception is `GET /v1/apiKeys`: called without `accountIndex`, it lists keys across **all** of the wallet's subaccounts. Single-subaccount entries carry the `accountIndex` they belong to; all-subaccounts keys omit it and set `allSubaccounts: true`. See [Authentication](/api-reference/authentication) for how keys are registered and how requests are signed. ## Subaccounts are not margin isolation Subaccounts are an **account-partitioning** mechanism, not a margining one. Margin mode (**cross** or **isolated**) is chosen **per market / per position**, so a single subaccount can hold both cross-margined and isolated-margined positions at the same time. Do not use separate subaccounts as a substitute for isolated margin. For how margin modes work, see [Cross & isolated margin](/concepts/perpetuals/margin-modes). ## Moving funds between subaccounts To move collateral from one subaccount to another **of the same wallet**, submit an internal transfer with [`POST /v1/transfer`](/api-reference/exchange/submit-internal-transfer). Deposits and withdrawals are separate, per-subaccount operations — depositing into a specific index is covered in [Fund a testnet account](/guides/fund-testnet-account) (via the `accountIndex` argument). A few properties define the flow: * **Same wallet only.** In v0 an internal transfer moves collateral between two `accountIndex` values that both belong to the same master wallet address. There is no cross-wallet recipient; the source and destination indexes must differ. * **Self-signed, not API-key auth.** The request is authenticated by a self-signed EIP-712 typed-data signature (the `Arcus Transfer` domain), so it does not use the `X-API-Key` / `X-Signature` headers. The signing mechanics live in [Authentication](/api-reference/authentication) and on the [endpoint page](/api-reference/exchange/submit-internal-transfer). * **Asynchronous settlement.** A `202 Accepted` means the transfer was validated and queued with `status: PENDING` and a `transferId`. The terminal outcome is delivered separately — read it back on `GET /v1/accountTransferUpdates` or the `accountTransferUpdates` WebSocket channel, correlating on `transferId`. Insufficient free collateral surfaces there as `REJECTED_INSUFFICIENT_COLLATERAL`. Internal transfers never settle on-chain — funds stay in custody and move between indexes inside the exchange. ## Next steps Register an API key, scope it to a subaccount, and sign requests. Move collateral between subaccounts of the same wallet. # Wrapped tokens Source: https://docs.arcus.xyz/concepts/wrapped-tokens How Arcus Spot RFQ represents stock tokens as upgradeable wrapped tokens that are minted as a fallback when no native fill is available, held in your account until the maker settles them, and settled by makers Wrapped tokens are the **fallback delivery mechanism for [Stock Tokens](/concepts/spot-overview)** — not a separate product. When you buy a stock spot, a real fill against the stock token is always preferred; a wrapped token is only minted when no native fill is available (typically long-tail names). A **wrapped token** is an on-chain representation of a stock token — for example `wTSLA` stands in for TSLA. Once minted, it is held in your Arcus account and behaves like the underlying for display, but is **non-transferable until the maker settles it to the real token**. The maker that issued it **settles** on a regular cadence. This lets the exchange offer equity exposure without requiring the underlying asset to be on-chain at the moment of every trade. This page describes the smart-contract architecture and trust model. It reflects the **v1** design. ## Factory and upgradeability Each wrapped token is deployed by a **`WrappedTokenFactory`** as an upgradeable proxy. The proxy delegates to a shared **upgrade beacon**, which points at the current **implementation** (`v1`, `v2`, …). Upgrading the beacon migrates every wrapped token at once. ```mermaid theme={null} flowchart LR Factory[WrappedTokenFactory] -->|Creates| Proxy[WrappedToken
Proxy] Proxy --> Beacon[WrappedToken
Upgrade Beacon] Beacon --> V1[WrappedToken
Implementation v1] Beacon -.-> V2[WrappedToken
Implementation v2] V2 -.-> Etc[etc...] ``` * **Upgrades are gated by a timelocked multisig** — no single party can swap the implementation, and changes are visible during the timelock window. * Transfers **may be restricted** — for example, only whitelisted operators may move wrapped tokens. This keeps wrapped tokens inside the exchange's settlement perimeter (see [Trading and product UX](#trading-and-product-ux)). ## Minting: buying a stock When a taker wants to buy a stock and no native fill is available, a maker supplies the wrapped token and the exchange mints it against the taker's collateral. (When a real fill *is* available, it is used instead and no wrapped token is minted.) Both sides express the trade as a **signed trade intent**; an **executor** pairs them and hands the matched order to the **Arcus Router**. ```mermaid theme={null} flowchart LR Taker((taker)) -->|Signed trade intent
WTB 1 TSLA| Exec[executor] Maker((maker)) -->|Signed trade intent
selling wTSLA| Exec Exec --> Router[Arcus Router] Router -->|Look up registered
wrapped token
TSLA → wTSLA| Factory[WrappedTokenFactory] Router -->|Execute| Escrow[WrappedTokenEscrow] Escrow -->|Mint| WT[WrappedToken] ``` How each step works: * **Executor** receives both signed intents — the taker's *want-to-buy* and the maker's *selling wTSLA* — and forwards a matched order to the router. * **Arcus Router** executes an atomic swap, subject to satisfying both signed intents. It looks up the registered wrapped token for the underlying (`TSLA → wTSLA`) in the factory and **accepts `wTSLA` as a substitute for `TSLA`**. If no wrapped token applies, execution is delegated to downstream protocols (treated as a black box). * **WrappedTokenEscrow** verifies the maker (seller) is **whitelisted** to issue wrapped tokens, then **mints the wrapped token and locks the taker's USDG in escrow**. **Fungibility across makers.** The escrow can support multiple makers, with wrapped tokens fungible regardless of which maker issued them. Each maker is trusted to settle regularly. If a maker fails to settle, the maker is responsible for any difference between the price of the collateral and the current price of the stock token. There can be a loss — for example if the wrapped token appreciated since issuance — that someone else would have to cover to settle on its behalf. ## Trading and product UX After minting, wrapped tokens **behave like stock tokens inside the exchange** — but stay within the settlement perimeter until a maker redeems them. * In the app, holding `wTSLA` **looks and behaves like holding TSLA** — it can be sold like any other position. * The app **restricts withdrawing wrapped tokens off the exchange** and marks them as *“awaiting settlement,”* keeping them within the settlement perimeter until a maker redeems them. ## Settlement A maker is expected to **settle on a regular cadence** (for example every 24 hours), **burning all outstanding wrapped tokens** it issued and reclaiming the escrowed USDG. ```mermaid theme={null} flowchart LR Maker((maker)) -->|Settle| Escrow[WrappedTokenEscrow] Escrow -->|Burn and transfer| Taker((taker)) ``` * To settle, say, **400 `wTSLA`**, the maker brings **400 units** to the contract as `wTSLA` and/or actual `TSLA`, and **receives the associated USDG** from escrow. * When the maker brings **`wTSLA`**, the token is simply **burned**. * When the maker brings **`TSLA`**, a holder's `wTSLA` is **burned and replaced with `TSLA`** — **no action is required from the wrapped-token holder**. **v1 trust assumption.** Makers may misbehave by not settling on time. The design relies on whitelisted, trusted makers: if a maker fails to settle, that maker is responsible for any difference between the price of the collateral and the current price of the stock token, and any unsettled position is a credit risk that must be covered to settle on its behalf. ## Trust model at a glance | Property | v1 behavior | | - | - | | Who can mint | Whitelisted makers only, verified by the escrow. | | Backing | Taker USDG locked in escrow until the maker settles. | | Fungibility | Wrapped tokens are fungible across all issuing makers. | | Upgrades | Gated by a timelocked multisig. | | Transfers | May be restricted to whitelisted operators. | | Settlement | Maker burns wrapped tokens on a regular cadence and reclaims USDG. | | Misbehavior | Relies on whitelisted, trusted makers; a maker that fails to settle is responsible for the difference between the collateral and the current stock-token price. | # Fund a testnet account Source: https://docs.arcus.xyz/guides/fund-testnet-account Deposit collateral on-chain when the Testnet Deposit button is not enough — for API traders who need a larger or programmatic balance **Most users don't need this.** In the Arcus testnet web app, connect your wallet and click **Testnet Deposit** to instantly credit **\~\$1,000 in USDG** of paper-trading collateral — that's enough for nearly everything. This guide is for **API traders** who need a larger balance, or who want to fund **programmatically** as part of a test harness. ## The flow Funding is an on-chain deposit on **Robinhood Chain testnet**. The exchange credits your trading account automatically once it observes the deposit on-chain (typically under a minute). The steps never change: The testnet collateral token (`USDG`) has an **open mint** — anyone can mint themselves test tokens. Mint the amount you want to deposit. Approve the deposit proxy contract to pull your `USDG`. The spender is the **deposit proxy**, not the bridge. Call `initiateDeposit` on the deposit proxy. It pulls your `USDG`, wraps it into the margin token, and forwards it to the bridge. `owner` must equal the wallet that signs the transaction. Poll [`GET /v1/account`](/api-reference/public/get-account) until your balance reflects the deposit. It also shows up as an `APPLIED DEPOSIT` on [`GET /v1/accountTransferUpdates`](/api-reference/public/get-account-transfer-updates). That sequence — **mint → approve → `initiateDeposit` → wait** — is stable. The contract addresses below are **not**. ## Testnet parameters These addresses are **testnet deployment details that change on every redeploy or network reset** — confirm them before relying on them in automation. A stale address fails in one of two ways: an obvious `WrongToken` / transfer revert, **or** a transaction that succeeds on-chain but **never credits your account** (you minted/approved/deposited against a retired deployment that the live exchange no longer watches). The *flow* is stable; the *values* are not. You only ever interact with two contracts: the collateral token (`USDG`) and the deposit proxy. The proxy forwards to the bridge internally, so you never call the `BridgeVault` directly. | Parameter | Value (testnet, verify before use) | | - | - | | Chain ID | `46630` | | RPC URL | `https://rpc.testnet.chain.robinhood.com` | | `USDG` — collateral token (`MockERC20`, **6 decimals**, open mint) | `0x293b337712d4312776a3a2d292f44410e7873bad` | | `PaxosDepositProxy` — deposit entry point | `0xb872366eef371d4afb7c6d4d2abd53c17292a34d` | Amounts use the token's **6 decimals** — e.g. `$1,000` = `1000000000`, `$100,000` = `100000000000`. Your wallet also needs a **small amount of RH-testnet ETH** (≈`0.001`) to pay gas for the transactions below. There is currently **no public faucet** for RH-testnet ETH — seed a fresh wallet by transferring from a wallet you already gas-funded, or ask the Arcus team. (When seeding wallet-to-wallet, note the chain rejects a bare 21,000-gas transfer with `intrinsic gas too low` — let your tooling estimate gas rather than hard-coding the classic minimum.) ## Deposit with `cast` Using [Foundry's `cast`](https://book.getfoundry.sh/cast/). `$KEY` / `$ADDR` are the depositing wallet's private key / address: ```bash theme={null} RPC=https://rpc.testnet.chain.robinhood.com USDG=0x293b337712d4312776a3a2d292f44410e7873bad PROXY=0xb872366eef371d4afb7c6d4d2abd53c17292a34d AMT=1000000000 # $1,000 at 6 decimals # 0. Sanity-check the chain and that you have gas cast chain-id --rpc-url $RPC # → 46630 cast balance $ADDR --rpc-url $RPC # → must be > 0 (RH-testnet ETH for gas) # 1. Mint test USDG to your wallet (open mint, testnet only) cast send $USDG "mint(address,uint256)" $ADDR $AMT --rpc-url $RPC --private-key $KEY # 2. Approve the deposit proxy to pull your USDG (spender is the PROXY, not the bridge) cast send $USDG "approve(address,uint256)" $PROXY $AMT --rpc-url $RPC --private-key $KEY # 3. Deposit into account index 0 — owner must equal the caller, token must be USDG cast send $PROXY "initiateDeposit(address,uint16,address,uint256)" $ADDR 0 $USDG $AMT --rpc-url $RPC --private-key $KEY ``` The `initiateDeposit` arguments are `(owner, accountIndex, token, amount)`. Use `accountIndex` `0` unless you trade a non-default subaccount, and `token` must be the `USDG` address (the proxy rejects anything else with `WrongToken`). ## Deposit programmatically (web3.py) The same three transactions from a test harness, using [web3.py](https://web3py.readthedocs.io/) and the wallet's private key: ```python theme={null} from web3 import Web3 RPC = "https://rpc.testnet.chain.robinhood.com" USDG = Web3.to_checksum_address("0x293b337712d4312776a3a2d292f44410e7873bad") PROXY = Web3.to_checksum_address("0xb872366eef371d4afb7c6d4d2abd53c17292a34d") AMT = 1_000_000_000 # $1,000 at 6 decimals w3 = Web3(Web3.HTTPProvider(RPC)) assert w3.eth.chain_id == 46630 acct = w3.eth.account.from_key(PRIVATE_KEY) assert w3.eth.get_balance(acct.address) > 0 # RH-testnet ETH for gas usdg = w3.eth.contract(address=USDG, abi=[ {"name": "mint", "type": "function", "inputs": [{"name": "to", "type": "address"}, {"name": "amount", "type": "uint256"}], "outputs": []}, {"name": "approve", "type": "function", "inputs": [{"name": "spender", "type": "address"}, {"name": "amount", "type": "uint256"}], "outputs": [{"type": "bool"}]}, ]) proxy = w3.eth.contract(address=PROXY, abi=[ {"name": "initiateDeposit", "type": "function", "inputs": [ {"name": "owner", "type": "address"}, {"name": "accountIndex", "type": "uint16"}, {"name": "token", "type": "address"}, {"name": "amount", "type": "uint256"}], "outputs": []}, ]) def send(fn): tx = fn.build_transaction({"from": acct.address, "nonce": w3.eth.get_transaction_count(acct.address)}) signed = acct.sign_transaction(tx) receipt = w3.eth.wait_for_transaction_receipt(w3.eth.send_raw_transaction(signed.raw_transaction)) assert receipt.status == 1, f"reverted: {receipt.transactionHash.hex()}" return receipt send(usdg.functions.mint(acct.address, AMT)) # 1. mint send(usdg.functions.approve(PROXY, AMT)) # 2. approve the proxy send(proxy.functions.initiateDeposit(acct.address, 0, USDG, AMT)) # 3. deposit ``` ## Verify ```bash theme={null} # Balance should reflect the deposit curl "https://api.testnet.arcus.xyz/v1/account?address=$ADDR&accountIndex=0" # The deposit shows up here as an APPLIED DEPOSIT curl "https://api.testnet.arcus.xyz/v1/accountTransferUpdates?address=$ADDR&limit=50" ``` An address with **no activity at all** returns **404** (`"this account has no activity yet"`) from `GET /v1/account` — that's the expected empty state, not an error. Registering an API key already counts as activity: an onboarded-but-unfunded account returns **200 with `equity: "0"`**. Once the deposit applies, the response reflects the credited balance. ## Troubleshooting | Symptom | Cause / fix | | - | - | | `initiateDeposit` reverts with `WrongToken` | The `token` argument must be the `USDG` address. If it already is, the configured underlying may have changed — confirm the current addresses. | | Reverts on transfer / allowance | The approval (step 2) must be on `USDG` with the **`PaxosDepositProxy`** as spender — not the bridge. Re-run step 2. | | `intrinsic gas too low` / `max fee per gas less than base fee` | Top up the wallet's RH-testnet ETH and retry; the base fee can rise between blocks. This also fires on a hard-coded 21,000-gas ETH transfer — let your tooling estimate gas. | | `initiateDeposit` reverts once, with mint and approve confirmed | Transient — retry the identical transaction before debugging. Verify the allowance is in place first (`allowance(owner, PROXY)` ≥ amount); a straight re-send has been observed to succeed. | | Transaction succeeds but no credit appears | Confirm the deposit tx landed on-chain. If it did and the balance still hasn't moved after a few minutes, the exchange's deposit observer didn't pick it up — this can be per-deposit **or a temporary outage of the crediting pipeline itself** (more likely shortly after a testnet reset). Flag it to the Arcus team with your wallet address and the tx hash; the deposit credits once the observer catches up. | ## Next steps With a funded account, place your first order — [Trade over REST](/guides/rest-trading) or [Trade over WebSocket](/guides/websocket-trading). # Optimize for latency Source: https://docs.arcus.xyz/guides/latency Where end-to-end latency comes from and how to minimize it — connectivity, host sizing, and the order-entry fast paths End-to-end latency is more than the matching engine: a request crosses the network, the API servers, and order-entry prioritization before it reaches the engine, and the acknowledgement crosses them again on the way back. Measure round trips end to end, and optimize each leg. ## Receiving market data **Use WebSocket, not REST polling.** The [WebSocket API](/api-reference/websocket) is the recommended way to receive market data and interact with the exchange: one connection multiplexes every [channel](/api-reference/channels) and order entry, and updates are pushed the moment they happen. REST is fine for bootstrapping state, but polling it adds a full round trip to every observation. **Colocate in Asia.** The exchange is hosted in Asia, so network round-trip time is dominated by your distance from it. Running your trading systems in an Asian region gives the lowest and most stable round trips. **Provision enough hardware.** A host that cannot drain its socket promptly falls behind, and the server force-disconnects clients whose connections stay saturated. Use at least **2 CPU cores and 4 GB RAM**; **4 cores and 8 GB** is preferred for busy accounts or full-depth market-data subscriptions. Keep the read loop light — parse and hand off, and do strategy work on other cores. ## Placing and canceling orders **Trade over the socket.** Order entry over the existing WebSocket connection skips connection setup entirely — see [Trade over WebSocket](/guides/websocket-trading). **Use ALO for the fastest placement.** Order entry applies a **taker speed bump of 50 ms**: any order that could remove liquidity — a non-post-only place, or a modify whose time-in-force allows taking — is held for 50 ms before sequencing. `ALO` (post-only) orders can never take, so they **skip the speed bump** and go straight to the engine. If your flow is maker-only, submit `ALO`: it is the fastest path in, and modifies of an `ALO` order keep the fast path too. (The 50 ms value is operator configuration and could be tuned in the future.) **Cancels are prioritized.** Cancel requests are processed on a dedicated lane ahead of queued order placements, so pulling quotes stays fast even when order entry is busy. `cancelOrder` (or a batch of them) beats a burst of competing placements to the engine. **Use client IDs to skip a round trip.** Supply a `clientId` when placing and you can [cancel by `clientId`](/api-reference/exchange/cancel-order) immediately — no need to wait for the acknowledgement to learn the server-assigned `orderId`. Client IDs also let you correlate `orders`-channel events with in-flight requests. A `clientId` must be unique among your live orders (reuse after the order reaches a terminal state; placing a live `clientId` again is rejected with `DUPLICATE_CLIENT_ID`, and `modifyOrder` changes a live order in place), and an account can hold up to 10,000 live client IDs. # Trade over REST Source: https://docs.arcus.xyz/guides/rest-trading Register a key and place your first order over REST — the simplest way to get started REST is the simplest way to place your first order: one signed request per call, no socket to manage. This quickstart gets you from nothing to a live order on testnet in three steps. For production or low-latency trading, use the [WebSocket track](/guides/websocket-trading) — the primary interface. Requests target testnet (`https://api.testnet.arcus.xyz`); new accounts start empty — fund yours with the **Testnet Deposit** button (\~\$1,000), or an [on-chain deposit](/guides/fund-testnet-account) for more. See [Authentication](/api-reference/authentication) for signing. ## Step 1: Install Only published libraries — no Arcus package required. ```bash theme={null} pip install cryptography eth-account ``` ## Step 2: Get an API key Your API key is an **Ed25519 keypair**; the server stores only the public half, and the key is registered to your wallet by a one-time **EIP-712 typed-data signature** (`eth_signTypedData_v4`) — see [`POST /v1/createApiKey`](/api-reference/onboarding/create-api-key) for the domain and types (the legacy EIP-191 scheme is deprecated but still accepted during the migration window). The Arcus web app does all of this for you and is the quickest way to get a key; you can also do it entirely in code. ### Option A — Generate on the web app (recommended) 1. Open the API Keys page — [testnet](https://testnet.arcus.xyz/api-keys), or [mainnet](https://app.arcus.xyz/api-keys) for production — and connect the wallet you'll trade from. 2. Enter a name and click **Generate**. The app creates the keypair in your browser. 3. **Copy the API Signing Key now** — it's the private half, shown once, and cannot be recovered. Pick a **Subaccount #** (default `0`) and **Days Valid** (up to 180), then click **Authorize** and confirm the signature in your wallet. That registers the key — nothing to run. Load the signing key you copied and skip to [Step 3](#step-3-place-an-order): ```python theme={null} # guide-test: skip — placeholder values; Option B is the runnable path. from cryptography.hazmat.primitives.asymmetric import ed25519 priv = ed25519.Ed25519PrivateKey.from_private_bytes(bytes.fromhex("")) api_key = priv.public_key().public_bytes_raw().hex() # matches the "API Key" the app shows account_index = 0 # the Subaccount # you selected wallet_address = "0x..." # the wallet you connected ``` The steps below read the master address as `wallet.address`; with a web-app key, use your `wallet_address` wherever you see it. ### Option B — Generate in code Generate a fresh keypair and register it with one `createApiKey` call — the same operation the web app performs: ```python theme={null} import json, time, urllib.request from cryptography.hazmat.primitives.asymmetric import ed25519 from eth_account import Account from eth_account.messages import encode_defunct BASE = "https://api.testnet.arcus.xyz" def canonical(obj): return json.dumps(obj, separators=(",", ":"), sort_keys=True).encode() def call(method, path, body=None, headers=None): data = canonical(body) if body is not None else None req = urllib.request.Request(BASE + path, data=data, method=method, headers={"Content-Type": "application/json", **(headers or {})}) with urllib.request.urlopen(req, timeout=10) as r: return json.loads(r.read() or b"{}") priv = ed25519.Ed25519PrivateKey.generate() api_key = priv.public_key().public_bytes_raw().hex() wallet = Account.create() # or Account.from_key("0x...") to reuse an address valid_until = int(time.time() * 1000) + 7 * 86_400_000 # within [now+1d, now+180d] msg = canonical({"apiWalletName": "quickstart", "apiWalletPublicKey": api_key, "validUntil": valid_until}) sig = wallet.sign_message(encode_defunct(primitive=msg)) created = call("POST", "/v1/createApiKey", { "address": wallet.address, "publicKey": api_key, "apiWalletName": "quickstart", "validUntil": valid_until, "signature": {"r": hex(sig.r), "s": hex(sig.s), "v": hex(sig.v)}}) account_index = created["accountIndex"] # A new key takes a moment to start authenticating — wait for it. for _ in range(60): if any(k["apiKey"] == api_key for k in call("GET", f"/v1/apiKeys?address={wallet.address}")["apiKeys"]): break time.sleep(1) print("key is live, accountIndex:", account_index) ``` ## Step 3: Place an order Order requests sign a **typed canonical payload** — the compact, key-sorted JSON object shown below **is** the signing message (no prefix). The timestamp is carried inside as `ct` and must equal the `X-Timestamp` header (Unix **nanoseconds**). Price and size are signed as integer **ticks** and **quantums**, so you first convert them with the market's `tickSize` / `stepSize`. See [Authentication](/api-reference/authentication) for the full field reference. ```python theme={null} from decimal import Decimal OP_PLACE, OP_CANCEL = 1, 2 SIDE = {"BUY": 0, "SELL": 1} TIF = {"GTT": 0, "FOK": 1, "IOC": 2, "ALO": 3} # BTC-USD (marketId 1): tick/step sizes convert price & size to engine integers. mkt = next(m for m in call("GET", "/v1/markets")["markets"] if m["marketId"] == 1) TICK, STEP = mkt["tickSize"], mkt["stepSize"] addr = wallet.address.lower() # the signed payload uses the lowercase address def to_int(value, unit): # exact decimal → integer ticks/quantums n = Decimal(str(value)) / Decimal(unit) if n != n.to_integral_value(): raise ValueError(f"{value} is not a multiple of {unit}") return int(n) def post_order(path, payload, body, ts): # The typed payload IS the signing message — sign it directly, no prefix. sig = priv.sign(payload.encode()).hex() return call("POST", path, body, {"X-API-Key": api_key, "X-Timestamp": str(ts), "X-Signature": sig}) # A resting GTT buy ~5% below the BTC-USD oracle price, snapped to the tick. on_tick = round(float(mkt["oraclePrice"]) * 0.95 / float(TICK)) * float(TICK) price = round(on_tick, 10) good_til = int(time.time() * 1_000_000) + 40 * 86_400 * 1_000_000 # epoch µs, ≥1 month ahead ts = time.time_ns() payload = (f'{{"ad":"{addr}","ai":{account_index},"ct":{ts},"g":{good_til * 1000},' f'"m":1,"op":{OP_PLACE},"p":{to_int(price, TICK)},"q":{to_int("0.001", STEP)},' f'"r":0,"s":{SIDE["BUY"]},"t":{TIF["GTT"]},"v":1}}') placed = post_order("/v1/placeOrder", payload, { "address": wallet.address, "accountIndex": account_index, "marketId": 1, # BTC-USD "orderSide": "BUY", "orderType": "LIMIT", "quantity": "0.001", "price": str(price), "timeInForce": "GTT", "goodTilTime": str(good_til), "timestamp": ts}, ts) print("placed:", placed["orderId"], placed["status"]) # Cancel by orderId — cancelOrder signs its own payload (op 2; no g/p/q/s/t fields). ts = time.time_ns() payload = (f'{{"ad":"{addr}","ai":{account_index},"ct":{ts},' f'"id":"{placed["orderId"]}","m":1,"op":{OP_CANCEL},"v":1}}') canceled = post_order("/v1/cancelOrder", payload, { "address": wallet.address, "accountIndex": account_index, "marketId": 1, "kind": "orderId", "orderId": placed["orderId"], "timestamp": ts}, ts) print("canceled:", canceled["status"]) ``` `placeOrder`, `cancelOrder`, and `modifyOrder` use the typed payload above; `cancelAllOrders` and `setLeverage` use the legacy `timestamp + action + canonicalJSON(body)` message — see [Authentication](/api-reference/authentication). In the request **body**, `quantity` and `price` stay **decimal strings** (not floats). The integer `q`/`p` in the signed payload are derived from them via `tickSize` / `stepSize`; the two must agree, so reuse the same values. **Market orders** must use `timeInForce: IOC` (other values are rejected with `must be IOC for MARKET orders`) and still carry a `price` — it acts as a protective slippage bound that must be within **10%** of the current mark price (for TPSL market orders, within 10% of the trigger price), or the order is rejected. Because of this bound a `MARKET` order behaves like a marketable limit order, so many integrations simply send an aggressive `LIMIT` order instead. ## Step 4: Place a batch `batchPlaceOrders` signs **each order individually** with its own typed payload, all sharing one `X-Timestamp` (used as each element's `ct`). The `X-API-Key` + `X-Timestamp` headers authenticate the batch and each order carries its own `signature`. The `X-Signature` header must still be **present** — set it to any one element's signature (the batch is verified per element, not against this header). ```python theme={null} ts = time.time_ns() def batch_order(side, price, qty): payload = (f'{{"ad":"{addr}","ai":{account_index},"ct":{ts},"g":{good_til * 1000},' f'"m":1,"op":{OP_PLACE},"p":{to_int(price, TICK)},"q":{to_int(qty, STEP)},' f'"r":0,"s":{SIDE[side]},"t":{TIF["GTT"]},"v":1}}') return {"address": wallet.address, "accountIndex": account_index, "marketId": 1, "orderSide": side, "orderType": "LIMIT", "quantity": qty, "price": str(price), "timeInForce": "GTT", "goodTilTime": str(good_til), "timestamp": ts, "signature": priv.sign(payload.encode()).hex()} orders = [ batch_order("BUY", round(price * 0.99, 1), "0.001"), batch_order("BUY", round(price * 0.98, 1), "0.001"), ] # X-Signature must be present on a batch — set it to any element's signature. batch = call("POST", "/v1/batchPlaceOrders", {"orders": orders}, {"X-API-Key": api_key, "X-Timestamp": str(ts), "X-Signature": orders[0]["signature"]}) print("batch placed:", len(batch["responses"])) ``` `batchCancelOrders` works the same way — each element signs its own `cancelOrder` payload (`op` 2). ## Next steps Order placement is asynchronous — the response acknowledges receipt, and the order's lifecycle (fills, cancels) streams on the `orders` and `userFills` WebSocket channels. To follow orders in real time, move to the [WebSocket track](/guides/websocket-trading). # Trade over WebSocket Source: https://docs.arcus.xyz/guides/websocket-trading Register a key and place your first order over WebSocket — the primary trading interface WebSocket is the primary way to trade on Arcus: one connection multiplexes order routing and every data channel, and each request is signed on its own. This quickstart gets you from nothing to a live order on testnet in three steps. New here? The [REST track](/guides/rest-trading) is a gentler start. Requests target testnet (`https://api.testnet.arcus.xyz`); new accounts start empty — fund yours with the **Testnet Deposit** button (\~\$1,000), or an [on-chain deposit](/guides/fund-testnet-account) for more. See [Authentication](/api-reference/authentication) for signing. ## Step 1: Install Only published libraries — no Arcus package required. ```bash theme={null} pip install cryptography eth-account websockets ``` ## Step 2: Get an API key Your API key is an **Ed25519 keypair**; the server stores only the public half, and the key is registered to your wallet by a one-time REST call authenticated by an **EIP-712 typed-data signature** (`eth_signTypedData_v4`) — `createApiKey` is REST-only, so even socket-first clients register once over REST (the legacy EIP-191 scheme is deprecated but still accepted during the migration window). The Arcus web app does all of this for you and is the quickest way to get a key; you can also do it entirely in code. ### Option A — Generate on the web app (recommended) 1. Open the API Keys page — [testnet](https://testnet.arcus.xyz/api-keys), or [mainnet](https://app.arcus.xyz/api-keys) for production — and connect the wallet you'll trade from. 2. Enter a name and click **Generate**. The app creates the keypair in your browser. 3. **Copy the API Signing Key now** — it's the private half, shown once, and cannot be recovered. Pick a **Subaccount #** (default `0`) and **Days Valid** (up to 180), then click **Authorize** and confirm the signature in your wallet. That registers the key — nothing to run. Load the signing key you copied and skip to [Step 3](#step-3-place-an-order): ```python theme={null} # guide-test: skip — placeholder values; Option B is the runnable path. from cryptography.hazmat.primitives.asymmetric import ed25519 priv = ed25519.Ed25519PrivateKey.from_private_bytes(bytes.fromhex("")) api_key = priv.public_key().public_bytes_raw().hex() # matches the "API Key" the app shows account_index = 0 # the Subaccount # you selected wallet_address = "0x..." # the wallet you connected ``` The steps below read the master address as `wallet.address`; with a web-app key, use your `wallet_address` wherever you see it. ### Option B — Generate in code Generate a fresh keypair and register it with one `createApiKey` call — the same operation the web app performs: ```python theme={null} import json, time, urllib.request from cryptography.hazmat.primitives.asymmetric import ed25519 from eth_account import Account from eth_account.messages import encode_defunct BASE = "https://api.testnet.arcus.xyz" def canonical(obj): return json.dumps(obj, separators=(",", ":"), sort_keys=True).encode() def call(method, path, body=None, headers=None): data = canonical(body) if body is not None else None req = urllib.request.Request(BASE + path, data=data, method=method, headers={"Content-Type": "application/json", **(headers or {})}) with urllib.request.urlopen(req, timeout=10) as r: return json.loads(r.read() or b"{}") priv = ed25519.Ed25519PrivateKey.generate() api_key = priv.public_key().public_bytes_raw().hex() wallet = Account.create() # or Account.from_key("0x...") to reuse an address valid_until = int(time.time() * 1000) + 7 * 86_400_000 # within [now+1d, now+180d] msg = canonical({"apiWalletName": "quickstart", "apiWalletPublicKey": api_key, "validUntil": valid_until}) sig = wallet.sign_message(encode_defunct(primitive=msg)) created = call("POST", "/v1/createApiKey", { "address": wallet.address, "publicKey": api_key, "apiWalletName": "quickstart", "validUntil": valid_until, "signature": {"r": hex(sig.r), "s": hex(sig.s), "v": hex(sig.v)}}) account_index = created["accountIndex"] # A new key takes a moment to start authenticating — wait for it. for _ in range(60): if any(k["apiKey"] == api_key for k in call("GET", f"/v1/apiKeys?address={wallet.address}")["apiKeys"]): break time.sleep(1) print("key is live, accountIndex:", account_index) ``` ## Step 3: Place an order Open the socket, then send signed requests correlated to their responses by `id`. Trading methods sign the same **typed canonical payload** as REST: the compact JSON object **is** the signing message, and its `ct` field must equal the envelope `timestamp` (Unix **nanoseconds**). Price and size are signed as integer ticks/quantums — convert them with the market's `tickSize` / `stepSize`. See [Authentication](/api-reference/authentication) for the field reference. ```python theme={null} import asyncio, websockets from decimal import Decimal OP_PLACE, OP_CANCEL = 1, 2 SIDE = {"BUY": 0, "SELL": 1} TIF = {"GTT": 0, "FOK": 1, "IOC": 2, "ALO": 3} mkt = next(m for m in call("GET", "/v1/markets")["markets"] if m["marketId"] == 1) # BTC-USD TICK, STEP = mkt["tickSize"], mkt["stepSize"] addr = wallet.address.lower() # the signed payload uses the lowercase address def to_int(value, unit): # exact decimal → integer ticks/quantums n = Decimal(str(value)) / Decimal(unit) if n != n.to_integral_value(): raise ValueError(f"{value} is not a multiple of {unit}") return int(n) async def main(): async with websockets.connect("wss://api.testnet.arcus.xyz/v1/ws") as ws: async def send(req_id, method, payload, body, ts): # The envelope `signature` covers the typed payload, not the body. sig = priv.sign(payload.encode()).hex() await ws.send(json.dumps({"type": "post", "id": req_id, "request": { "type": method, "payload": body, "apiKey": api_key, "timestamp": str(ts), "signature": sig}})) while True: msg = json.loads(await asyncio.wait_for(ws.recv(), timeout=10)) if msg.get("id") == req_id: return msg # A resting GTT buy ~5% below the BTC-USD oracle price, snapped to the tick. price = round(round(float(mkt["oraclePrice"]) * 0.95 / float(TICK)) * float(TICK), 10) good_til = int(time.time() * 1_000_000) + 40 * 86_400 * 1_000_000 # epoch µs, ≥1 month ahead ts = time.time_ns() payload = (f'{{"ad":"{addr}","ai":{account_index},"ct":{ts},"g":{good_til * 1000},' f'"m":1,"op":{OP_PLACE},"p":{to_int(price, TICK)},"q":{to_int("0.001", STEP)},' f'"r":0,"s":{SIDE["BUY"]},"t":{TIF["GTT"]},"v":1}}') placed = await send(1, "placeOrder", payload, { "address": wallet.address, "accountIndex": account_index, "marketId": 1, "orderSide": "BUY", "orderType": "LIMIT", "quantity": "0.001", "price": str(price), "timeInForce": "GTT", "goodTilTime": str(good_til), "timestamp": ts}, ts) print("placed:", placed["result"]["orderId"], placed["status"]) ts = time.time_ns() payload = (f'{{"ad":"{addr}","ai":{account_index},"ct":{ts},' f'"id":"{placed["result"]["orderId"]}","m":1,"op":{OP_CANCEL},"v":1}}') canceled = await send(2, "cancelOrder", payload, { "address": wallet.address, "accountIndex": account_index, "marketId": 1, "kind": "orderId", "orderId": placed["result"]["orderId"], "timestamp": ts}, ts) print("canceled:", canceled["status"]) asyncio.run(main()) ``` A successful order returns a `202` acknowledgement; the order's lifecycle (fills, cancels) plays out on the `orders` and `userFills` channels. `placeOrder`, `cancelOrder`, and `modifyOrder` use the typed payload above; `cancelAllOrders` and `setLeverage` use the legacy `timestamp + action + canonicalJSON(payload)` message. A batch signs each order individually with its own typed payload under one shared `timestamp`, each embedding its own `signature` — see the [REST batch example](/guides/rest-trading#step-4-place-a-batch) for the exact headers. In the request **body**, `quantity` and `price` stay **decimal strings**. The integer `p`/`q` in the signed payload are derived from them via `tickSize` / `stepSize`; the two must agree. ## Next: stream your fills **Coming soon.** The [`userFills`](/api-reference/channels#userfills) and [`orders`](/api-reference/channels#orders) channels stream your fills and order updates in real time (subscribe by account address). A copy-paste example will be added here.