Skip to navigation

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:

{
"name": "Hyperflow Perps",
"version": "2",
"chainId": 999
}

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.

ActionSigned byaccount fieldnonce in JSONSignature in body
AuthorizeAgentMain walletaddress (wallet)numbersignature string
RevokeAgentMain walletaddress (wallet)numbersignature string
WithdrawMain walletaddress (wallet)string{ r, s, v } object
OrderTrading keybytes32 (account_id)stringsignature string
CancelTrading keyaddress (wallet)stringsignature string
ModifyTrading keybytes32 (account_id)stringsignature string
UpdateIsolatedMarginTrading keybytes32 (account_id)numbersignature string

wallet and account_id are as defined in Account format.

Structs

AuthorizeAgent(address user,address agent,string hyperflowEnvironment,uint256 expiresAt,uint256 nonce)
RevokeAgent(address user,address agent,string hyperflowEnvironment,uint256 nonce)
Withdraw(address user,address destination,uint256 amount,string hyperflowEnvironment,uint256 nonce)
Order(bytes32 account,string market,string side,string orderType,string size,string limitPrice,string timeInForce,bool reduceOnly,uint256 leverage,uint256 nonce,string triggerPrice,bool isMarket,string tpsl)
Cancel(address account,string orderId,string orderType,uint256 nonce)
Modify(bytes32 account,string orderId,string clientOrderId,string limitPrice,string size,string triggerPrice,uint256 nonce)
UpdateIsolatedMargin(bytes32 account,string market,string amount,uint256 nonce)

Three members are not where the request body suggests:

MemberValue comes from
Cancel.orderId, Modify.orderIdThe URL path — it is not in the body
Modify.clientOrderIdThe body’s client_order_id. Signed here; not a member of Order at all. parent_client_order_id on a bracket leg is not signed either
Order.triggerPrice, isMarket, tpslEvery order signs them — a plain order signs "0", false, ""

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

1

Generate an agent keypair

Any secp256k1 keypair. Keep the private key; its address is agent_wallet_address.

2

Sign AuthorizeAgent with your main wallet

user is your wallet, agent the address from step 1, hyperflowEnvironment "Mainnet", expiresAt and nonce in Unix ms.

3

Register it

POST /auth/register
{
"wallet_address": "0xYourMainWallet",
"agent_wallet_address": "0xYourAgentAddress",
"hyperflow_environment": "Mainnet",
"signature_chain_id": 999,
"expires_at": 1787672114951,
"nonce": 1787067314951,
"signature": "0x…65-byte r||s||v…"
}
FieldMeaning
wallet_addressThe main wallet that signed — the account the key will trade for
agent_wallet_addressThe public address of the keypair you generated in step 1
hyperflow_environment"Mainnet" or "Testnet", matching what you signed
signature_chain_idThe chainId you used in the signing domain
expires_atWhen the key stops working, Unix ms. Required, and must match the signed expiresAt exactly
nonceThe same Unix-millisecond timestamp you signed. It makes this request single-use: replaying it returns 409
signatureThe signature from step 2

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.

4

Verify it

GET /auth/trading-keys?wallet=0xYourMainWallet

Your agent should be listed as ACTIVE with a future expires_at. An empty list means you registered against a different wallet or environment, and every order will return 403. The wallet here must be the one your account_id derives from.

Managing trading keys

List active keys

GET /auth/trading-keys?wallet=0x…

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

POST /auth/trading-keys/revoke

Your main wallet signs:

RevokeAgent(address user, address agent, string hyperflowEnvironment, uint256 nonce)

The body is the /auth/register body without expires_at:

{
"wallet_address": "0xYourMainWallet",
"agent_wallet_address": "0xYourAgentAddress",
"hyperflow_environment": "Mainnet",
"signature_chain_id": 999,
"nonce": 1787067314958,
"signature": "0x…"
}

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