Authentication & Signing
Every write to the Perps API is authorized by an EIP-712 signature carried in the request body. There is no API key and no bearer token — the server recovers the signer from the signature.
Signing is split across two keys so that a key used for order flow can never move funds:
- Your main wallet signs the custody-level actions — authorizing a trading key, revoking one, and withdrawing.
- A trading key signs everything else. It is a separate keypair you generate locally and your main wallet authorizes once; from then on it signs all order flow. If it leaks, the attacker can trade on your account but cannot withdraw — and you can revoke it instantly.
The typed data and request fields call the trading key an agent (AuthorizeAgent, agent_wallet_address). The two words mean the same key.
The EIP-712 domain
All typed data shares one domain — note there is no verifyingContract and no salt:
chainId is 999 on mainnet, 998 on testnet — it identifies the HyperFlow environment, not your connected wallet’s chain. Trading actions require the pinned value; wallet-signed actions accept any chainId > 0 as long as the body’s signature_chain_id carries the same number. Using 999 everywhere on mainnet is simplest.
Typed data that includes a hyperflowEnvironment field carries the literal string "Mainnet" or "Testnet" — a second replay guard across environments. There is no public testnet base URL today; every request goes to mainnet.
The signed actions
Seven actions are signed. nonce is always uint256 in the typed data; the column below is how it travels in the JSON body.
wallet and account_id are as defined in Account format.
Structs
Three members are not where the request body suggests:
Signing rules
Break any of these and the server recovers a different address — on trading-key actions that is a 403, not a validation error.
Numbers travel as strings, byte for byte
Prices, sizes, and amounts are typed as string in the typed data and hashed as UTF-8 — the server hashes what is in the body verbatim, so the string you sign must be byte-identical to the string you send.
Use canonical decimals: no trailing zeros, no exponent notation. "0.5" is correct; "0.50" and "5e-1" sign to different hashes. The app normalizes with BigNumber.toFixed().
One exception: Withdraw.amount is a uint256 in USDC base units — 25 USDC signs as 25000000 while the body sends "25". See Request a withdrawal.
Every nonce is a fresh millisecond timestamp
nonce is typed as uint256 and must be a Unix-millisecond timestamp, single-use per wallet. When you send a burst, increment it yourself so no two requests reuse a millisecond. A replayed nonce returns 409.
Signatures are 65-byte r || s || v
v is 27 or 28. Most endpoints take the whole thing as one hex signature string; /account/withdraw takes it split into { r, s, v }. The table above says which.
Signature failures
Wallet-signed actions report a bad signature explicitly: /auth/register and /auth/trading-keys/revoke return 400 INVALID_SIGNATURE, /account/withdraw returns 401 UNAUTHORIZED.
Trading-key actions do not: a bad signature recovers to an unknown address, and every trading route answers 403 FORBIDDEN — signature does not match an active trading key. The same message therefore means either no active key for this account or signed payload ≠ sent payload. GET /auth/trading-keys?wallet=… separates the two: if your agent is listed and unexpired, the payload is the problem.
Creating a trading key
Generate an agent keypair
Any secp256k1 keypair. Keep the private key; its address is agent_wallet_address.
Sign AuthorizeAgent with your main wallet
user is your wallet, agent the address from step 1, hyperflowEnvironment "Mainnet", expiresAt and nonce in Unix ms.
Register it
Optional: name (a label for the key, ≤64 chars) and key_hint (≤32 chars). An absent or 0 expires_at is rejected.
A success returns data.expires_at in Unix ms. Failures are all 400 with a specific code — INVALID_REQUEST, INVALID_ENVIRONMENT (wrong environment for this server), INVALID_SIGNATURE, INVALID_EXPIRES_AT (missing, past, or over 180 days), INVALID_SIGNATURE_CHAIN_ID — and 409 for a replayed nonce.
Managing trading keys
List active keys
Returns the wallet’s ACTIVE, unexpired keys, each with agent_wallet_address, status, created_at, and expires_at. It is an open read — no signature needed.
Revoke a key
Your main wallet signs:
The body is the /auth/register body without expires_at:
Revocation takes effect immediately — the key’s next order is rejected.
The app mints trading keys with a 7-day expiry; over the API you can sign an expiry of up to 180 days. Re-registering the same agent address refreshes its expiry rather than creating a second key.
Next: Orders