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:
| Product | Delegation Type | Storage | How to Set Up |
|---|---|---|---|
| Options (REST API) | Agent authorization | Off-chain (database) | POST /approve-agent |
| Perps (on-chain directives) | API wallet | On-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:
nonce > min(stored_set)-- must be greater than the smallest stored nonce!stored_set.contains(nonce)-- must not be a duplicatenonceis 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
ApproveAgentmessage - Wallet is derived from recovered signature
- Agent is the
agentfield 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_atis set (not yet implemented)
Revoke Agent
Endpoint: DELETE /revoke-agent
Request: RevokeAgentRequest
{
"agent": "0x...",
"nonce": 2,
"signature": "0x..."
}
Signing:
- Wallet owner signs the
RevokeAgentmessage - Wallet is derived from recovered signature
EIP-712 struct:
struct RevokeAgent {
address agent;
uint64 nonce;
}
Response: RevokeAgentResponse
Notes:
- Applies a journaled
RevokeAgentcommand 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:
- Sign order with agent wallet: Use agent wallet to sign
PlaceOrder/CancelOrdermessages - Set wallet field: Set
walletfield to the trading wallet address (not agent address) - 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_ordercan 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 /orderDELETE /orderDELETE /order_cloid
Check: signature_and_agent_middleware verifies:
- Signature recovery succeeds
- Signer is authorized (signer == wallet OR agent authorized)
Handler (Bulk Orders)
Endpoints:
POST /bulk_orderDELETE /bulk_orderDELETE /bulk_order_cloid
Check: Per-item verification in handler:
- Signature recovery per-item
- 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>andagent_address == <signer>, andexpires_atis 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
- Agent key security: Protect agent private keys (use hardware wallets or secure key management)
- Regular audits: Review authorized agents via
GET /authorized-agentsregularly - Revoke unused agents: Revoke agents that are no longer needed
- Expiration: Use
expires_atfor temporary agent authorizations when the approval path exposes a non-default expiry
Best Practices
- Use agents for automation: Keep trading wallet keys in cold storage, use agents for automated systems
- Limit agent scope: Only approve agents that need access
- Monitor agent usage: Track which agents are placing orders
- 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=....