Generate an Ed25519 key pair
The public key becomes yourapiKey. 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.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
ForplaceOrder, 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:
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, lowercase0x-hexai— account (subaccount) indexc— client id; omitted entirely when emptyct— client timestamp in Unix nanoseconds; equals theX-Timestampyou sendg—goodTilTimein nanoseconds (the request’s microsecondgoodTilTime× 1000; required on every order — see below) —placeOrderandmodifyOrderid— serverorderId. Required formodifyOrder; forcancelOrderit is omitted when canceling by client idm— market idop— operation:1place,2cancel,3modify (4= untriggered TPSL — same fields as place)p— price in integer ticks =price ÷ market tickSizeq— quantity in integer quantums =size ÷ market stepSizer— reduce-only,0or1(integer, not a boolean) —placeOrderandmodifyOrders— side:0buy,1sell —placeOrderandmodifyOrdert— time-in-force:0GTT,1FOK,2IOC,3ALO —placeOrderandmodifyOrderv— payload version (currently1)
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, domainArcus Transfer) with the wallet that ownsethereumAddress, and send the(r, s, v)triple in the body’ssignaturefield. 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-Signaturetriple and omit the bodysignature.X-Signatureis an Ed25519 signature over theTransferV1typed payload, which follows the same compact, key-sorted convention as orders:
ad— master Ethereum address (ethereumAddress), lowercase0x-hexaf/at— source (fromAccountIndex) and destination (toAccountIndex) subaccount indexes; both are signed, so the destination can’t be swapped after signingct— equals theX-Timestampyou send (Unix nanoseconds)n— the bodynonce, signed verbatimop— always6(transfer). An order or withdrawal signature can never authorize a transfer, and a transfer signature authorizes nothing elseq—amountas an integer number of quote quantums, unquotedv— payload version (currently1)
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:
GET /v1/markets; subscribe to the markets WebSocket channel for live updates. The universe is the same for every API key.