Skip to main content

Agent Authorization

Multi-key signing support for market makers.

Overview

Agent authorization allows a dedicated signing key (agent) to place and cancel orders on behalf of a trading wallet. This is useful for:

  • Security: Keep trading wallet keys in cold storage, use hot keys for signing
  • Operational: Multiple team members can sign with different keys
  • Automation: Automated systems can sign with dedicated keys

Two Authorization Models

Hypercall has two separate delegation systems depending on the product:

ProductDelegation TypeStorageHow to Set Up
Options (REST API)Agent authorizationOff-chain (database)POST /approve-agent
Perps (on-chain directives)API walletOn-chain (Exchange contract)hc_update_api_wallet directive

These are independent systems. Approving an agent for options trading does not authorize it for perps, and vice versa.

Options: Agent Authorization (Off-Chain)

Agents sign PlaceOrder and CancelOrder EIP-712 messages on behalf of a trading wallet. The API server verifies authorization against its database before forwarding to the engine.

Perps: API Wallet (On-Chain)

API wallets sign perp directives using the HypercallAgentSign EIP-712 domain. The Exchange contract verifies authorization on-chain via isApiWalletActive(). See EIP-712 Signing for the full directive signing reference.

To add or remove an API wallet, submit an hc_update_api_wallet directive signed by the account's manager wallet.

This mirrors Hyperliquid's API wallet model. Hyperliquid documents these as API wallets, also called agent wallets, in Nonces and API wallets, and documents approveAgent in the Exchange endpoint.

Nonce Replay Protection

Nonce tracking follows the Hyperliquid nonce model. All signed actions (orders, agent approval/revocation, QP handshakes) share a per-signer nonce space. The engine stores the 100 highest nonces per signer address. A new nonce is accepted if:

  1. nonce > min(stored_set) -- must be greater than the smallest stored nonce
  2. !stored_set.contains(nonce) -- must not be a duplicate
  3. nonce is within (T - 2 days, T + 1 day) of the server timestamp

Out-of-order nonces are allowed (e.g., nonce 105 then 102 both work if both are above the set minimum). The set is bounded at 100 entries, so the oldest entries are evicted as new ones are added. This prevents account bricking from a single large nonce.

The nonce signer is the address that signed the EIP-712 message: the API wallet for orders, the recovered signer for agent auth, the QP wallet for handshakes. Clients should use Date.now() (millisecond epoch) as the nonce seed and increment monotonically.

Options Agent Authorization

Direct Signing

If signer == wallet, the order is always authorized (self-signing).

Agent Signing

If signer != wallet, the signer must be an authorized agent:

  • Agent must be present in engine-owned authorization state
  • The authorization must not be expired
  • Engine snapshots and the engine journal are the durable restart sources

Approve Agent

Endpoint: POST /approve-agent

Request: ApproveAgentRequest

{
"agent": "0x...",
"nonce": 1,
"signature": "0x..."
}

Signing:

  • Wallet owner signs the ApproveAgent message
  • Wallet is derived from recovered signature
  • Agent is the agent field in the request

EIP-712 struct:

struct ApproveAgent {
address agent;
uint64 nonce;
}

Response: ApproveAgentResponse

{
"success": true,
"error": null
}

Notes:

  • Agent authorization is persistent (stored in DB)
  • Authorization does not expire unless expires_at is set (not yet implemented)

Revoke Agent

Endpoint: DELETE /revoke-agent

Request: RevokeAgentRequest

{
"agent": "0x...",
"nonce": 2,
"signature": "0x..."
}

Signing:

  • Wallet owner signs the RevokeAgent message
  • Wallet is derived from recovered signature

EIP-712 struct:

struct RevokeAgent {
address agent;
uint64 nonce;
}

Response: RevokeAgentResponse

Notes:

  • Applies a journaled RevokeAgent command to engine-owned authorization state
  • Trading API authorization checks read backend state and enforce revocations for trading requests. Trollbox posting may cache positive agent authorization for longer, so a recently revoked agent may still be able to post until that cache expires or best-effort invalidation reaches the relevant edge location. This cache does not authorize trading API mutations.
  • Agent can no longer sign for the wallet after revocation

Get Authorized Agents

Endpoint: GET /authorized-agents?wallet=...

Query parameters:

  • wallet (required)

Response:

{
"agents": [
"0x...",
"0x..."
]
}

Notes:

  • Returns only active, non-expired agents
  • Ordered by created_at DESC

Agent Usage

Signing Orders with Agent

Once an agent is approved:

  1. Sign order with agent wallet: Use agent wallet to sign PlaceOrder / CancelOrder messages
  2. Set wallet field: Set wallet field to the trading wallet address (not agent address)
  3. Middleware verification: Middleware verifies agent is authorized for that wallet

Example:

// Agent wallet signs the order
const agentSigner = new ethers.Wallet(agentPrivateKey);

const message = {
wallet: "0x...", // Trading wallet (not agent)
symbol: "BTC-20250131-100000-C",
side: "Buy",
size: "0.1",
price: "100.0",
tif: "gtc",
clientId: "mm-1",
nonce: 1
};

// Sign with agent wallet
const signature = await agentSigner._signTypedData(domain, types, message);

// Send request
const response = await fetch('/order', {
method: 'POST',
body: JSON.stringify({
...message,
signature
})
});

Bulk Orders with Agent

Bulk endpoints verify agent authorization per-item:

  • Each order in POST /bulk_order can be signed by a different agent
  • Agent authorization is checked per-item
  • If any item fails agent auth, that item returns error in BulkOrderResult

Authorization Checks

Middleware (Single Orders)

Endpoints:

  • POST /order
  • DELETE /order
  • DELETE /order_cloid

Check: signature_and_agent_middleware verifies:

  1. Signature recovery succeeds
  2. Signer is authorized (signer == wallet OR agent authorized)

Handler (Bulk Orders)

Endpoints:

  • POST /bulk_order
  • DELETE /bulk_order
  • DELETE /bulk_order_cloid

Check: Per-item verification in handler:

  1. Signature recovery per-item
  2. Agent authorization check per-item

Authorization Storage

Agent authorization is engine-owned state. ApproveAgent and RevokeAgent commands are journaled, restored through engine replay, and exposed through the read snapshot for API checks.

Authorization Logic

Signer authorization is explicit OR logic:

  • Direct signing: signer == wallet.
  • Agent signing: the engine snapshot contains an authorization record for wallet_address == <wallet> and agent_address == <signer>, and expires_at is absent or in the future.

Expiration

expires_at is enforced by engine snapshot authorization checks. Agents with expires_at < NOW() are treated as unauthorized.

Security Considerations

  1. Agent key security: Protect agent private keys (use hardware wallets or secure key management)
  2. Regular audits: Review authorized agents via GET /authorized-agents regularly
  3. Revoke unused agents: Revoke agents that are no longer needed
  4. Expiration: Use expires_at for temporary agent authorizations when the approval path exposes a non-default expiry

Best Practices

  1. Use agents for automation: Keep trading wallet keys in cold storage, use agents for automated systems
  2. Limit agent scope: Only approve agents that need access
  3. Monitor agent usage: Track which agents are placing orders
  4. Revoke promptly: Revoke agents immediately when no longer needed

Common Issues

"Unauthorized: signer not authorized for wallet"

Cause: Agent not approved or authorization expired/revoked.

Solution: Approve agent via POST /approve-agent or sign with wallet directly.

Agent Authorization Not Working

Causes:

  • Agent not present in engine-owned authorization state
  • Authorization was revoked
  • Authorization is expired

Solution: Check agent status via GET /authorized-agents?wallet=....