Skip to main content
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 — mainnet or testnet — can generate and register a key for you in a few clicks — the easiest path, and where the trading guides start; the manual steps below are for managing keys yourself.

Register the key (REST)

The registration request must be ECDSA-signed by the Ethereum address that owns the key.
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 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: There are two signing schemes. Pick by operation: 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. POST /v1/transfer and POST /v1/withdraw don’t need an API key: by default the wallet signs them instead. See Internal transfers and Withdrawals.

Scheme 1 — typed payload

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:
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. 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 gives the order 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 — 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.

Internal transfers

POST /v1/transfer moves collateral between two subaccounts of the same wallet. It accepts two authentication modes, and the presence of the X-API-Key header selects which one applies:
  • Wallet-signed (default, no X-API-Key). Sign the transfer as EIP-712 typed data (eth_signTypedData_v4, domain Arcus Transfer) with the wallet that owns ethereumAddress, and send the (r, s, v) triple in the body’s signature field. No API-key headers are needed. The domain, types and signing examples are on the endpoint page.
  • API-key-signed. Send the usual X-API-Key / X-Timestamp / X-Signature triple and omit the body signature. X-Signature is an Ed25519 signature over the TransferV1 typed payload, which follows the same compact, key-sorted convention as orders:
  • ad — master Ethereum address (ethereumAddress), lowercase 0x-hex
  • af / at — source (fromAccountIndex) and destination (toAccountIndex) subaccount indexes; both are signed, so the destination can’t be swapped after signing
  • ct — equals the X-Timestamp you send (Unix nanoseconds)
  • n — the body nonce, signed verbatim
  • op — always 6 (transfer). An order or withdrawal signature can never authorize a transfer, and a transfer signature authorizes nothing else
  • q — amount as an integer number of quote quantums, unquoted
  • v — payload version (currently 1)
The key must belong to ethereumAddress. A key scoped to one subaccount can only transfer out of that subaccount; a key scoped to all of the wallet’s subaccounts can use any of them as the source. No extra key permission is needed: moving funds between your own subaccounts carries the same authority as trading them.

Withdrawals

POST /v1/withdraw is wallet-signed (EIP-712) by default. Its API-key mode signs the WithdrawV1 payload (op 5), and the key must be bound to that address and subaccount and carry the operator-granted withdraw permission; keys from POST /v1/createApiKey get 403. The payload and signing details are on the endpoint page.

Scheme 2 — legacy message (everything else)

cancelAllOrders, scheduleCancel, setLeverage, settleLoan, WebSocket authenticate, and the referral endpoints sign the timestamp, then the action, then the canonical JSON body — concatenated with no delimiters:
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 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 in USDG 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. Once funded, the deposit appears as a DEPOSIT entry in GET /v1/accountTransferUpdates and your balance shows up on GET /v1/account:
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; subscribe to the markets WebSocket channel for live updates. The universe is the same for every API key.