Authentication & Signing

Every write to the Perps API is authorized by an EIP-712 signature carried in the request body. There are two signer roles:

SignerSignsWhy
Main walletAuthorizeAgent, RevokeAgent, WithdrawCustody-level actions stay with the key that owns the funds
Trading key (agent)Order, Cancel, Modify, UpdateIsolatedMarginA hot key for order flow that can never move funds out

A trading key is a separate keypair you generate locally. Your main wallet authorizes it once; after that, the agent key signs all trading actions. If it leaks, the attacker can trade on your account but cannot withdraw — and you can revoke it instantly.

The EIP-712 domain

All typed data shares one domain — note there is no verifyingContract and no salt:

1{
2 "name": "Hyperflow Perps",
3 "version": "2",
4 "chainId": 999
5}
  • For trading actions (Order, Cancel, Modify, UpdateIsolatedMargin), chainId is pinned per environment — 999 for mainnet, 998 for testnet — regardless of your connected wallet’s chain. For wallet-signed actions (AuthorizeAgent, RevokeAgent, Withdraw) the server is chain-agnostic: sign with any chainId > 0 and send the same value as signature_chain_id in the body. Using 999 everywhere on mainnet is simplest.
  • hyperflowEnvironment fields carry the literal string "Mainnet" or "Testnet" as a cross-environment replay guard.

Encoding rules

These rules must match exactly or signature recovery fails:

  1. Prices, sizes, and amounts are signed as string (hashed as UTF-8), not integers. The server hashes the body strings verbatim — so the string you sign must be byte-identical to the string you send. Use canonical decimals (no trailing zeros, no exponent: "0.5", never "0.50" or "5e-1") to avoid mismatches; the app normalizes with BigNumber.toFixed().
  2. nonce is a Unix-millisecond timestamp (uint256), single-use per wallet. Monotonically increase it when sending bursts. Its JSON carriage type varies per endpoint (a string in order/cancel/modify/withdraw bodies, a number in register/revoke/margin) — follow each endpoint’s example.
  3. account is bytes32 (the 32-byte account id) in Order, Modify, and UpdateIsolatedMargin — but address (the 20-byte wallet) in Cancel, AuthorizeAgent, RevokeAgent, and Withdraw.
  4. Signatures are 65-byte r || s || v hex (v = 27/28), sent either as one signature string or split {r, s, v} depending on the endpoint — each endpoint’s schema states which.

Creating a trading key

1

Generate an agent keypair

Any secp256k1 keypair works:

1import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
2
3const agentPrivateKey = generatePrivateKey();
4const agent = privateKeyToAccount(agentPrivateKey);
2

Sign AuthorizeAgent with your main wallet

Typed data — AuthorizeAgent(address user,address agent,string hyperflowEnvironment,uint256 expiresAt,uint256 nonce):

1const nonce = BigInt(Date.now());
2const expiresAt = BigInt(Date.now() + 7 * 24 * 3600 * 1000); // up to 180 days
3
4const signature = await mainWallet.signTypedData({
5 domain: { name: "Hyperflow Perps", version: "2", chainId: 999 },
6 types: {
7 AuthorizeAgent: [
8 { name: "user", type: "address" },
9 { name: "agent", type: "address" },
10 { name: "hyperflowEnvironment", type: "string" },
11 { name: "expiresAt", type: "uint256" },
12 { name: "nonce", type: "uint256" },
13 ],
14 },
15 primaryType: "AuthorizeAgent",
16 message: {
17 user: mainWallet.address,
18 agent: agent.address,
19 hyperflowEnvironment: "Mainnet",
20 expiresAt,
21 nonce,
22 },
23});
3

Register it

POST /auth/register
1{
2 "wallet_address": "0xYourMainWallet",
3 "agent_wallet_address": "0xYourAgentAddress",
4 "hyperflow_environment": "Mainnet",
5 "signature_chain_id": 999,
6 "expires_at": 1787672114951,
7 "nonce": 1787067314951,
8 "signature": "0x…65-byte r||s||v…"
9}

expires_at is required (absent or 0 is rejected) and must byte-match the signed expiresAt. Optional fields: name (a label, ≤64 chars) and key_hint (≤32 chars).

Returns data.expires_at (Unix ms). Responses: 400 bad body/env/chain/signature or missing expires_at; 403 the wallet is not on the alpha whitelist; 409 replayed nonce or the agent address is already in use.

Managing trading keys

EndpointWhat it does
GET /auth/trading-keys?wallet=0x…List the wallet’s ACTIVE, unexpired keys (agent_wallet_address, status, created_at, expires_at)
POST /auth/trading-keys/revokeRevoke a key — main wallet signs RevokeAgent(address user,address agent,string hyperflowEnvironment,uint256 nonce); body mirrors /auth/register without expires_at

The app mints trading keys with a 7-day expiry; the API accepts a signature-bound expiry of up to 180 days. Re-registering the same agent refreshes its expiry.

Next: Orders