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.
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.
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 personal_sign over the canonical message — see the POST /v1/createApiKey endpoint reference for the full signing recipe. 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:
ValueRESTWebSocket
API key (the public key)X-API-Key headerapiKey envelope field
Unix-nanosecond timestampX-Timestamp headertimestamp envelope field
Ed25519 signature (128 hex chars)X-Signature headersignature envelope field
There are two signing schemes. Pick by operation:
OperationScheme
placeOrder, cancelOrder, modifyOrderScheme 1 — typed payload
batchPlaceOrders, batchCancelOrders, batchModifyOrdersScheme 1, applied once per element (see Batches)
cancelAllOrders, setLeverage, WebSocket authenticateScheme 2 — legacy message
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) and client id (c) are lowercased.
OperationopCanonical payload
placeOrder1{"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}
cancelOrder2{"ad":"0x…","ai":N,"c":"…","ct":N,"id":"…","m":N,"op":2,"v":1}
modifyOrder3{"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
  • ggoodTilTime in nanoseconds (0 = no expiry) — 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 — the conversion must be exact (price ÷ tickSize with no remainder). Resting orders (t = 0 GTT or 3 ALO) require goodTilTime / g, and it must be at least one month in the future when the API handler processes the order — nearer or missing values are rejected. 1 FOK and 2 IOC never rest, so they use g = 0.

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, and WebSocket authenticate 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/cancelAllOrderscancelAllOrders); over WebSocket it’s the request type. 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 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. Once funded, the deposit appears as a DEPOSIT entry in GET /v1/accountTransferUpdates and your balance shows up on GET /v1/account:
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; subscribe to the markets WebSocket channel for live updates. The universe is the same for every API key.