Nonces, expiry and API wallets
HyperCore keeps each signer's 100 highest nonces and accepts a new one only if unused, above the smallest, and within 2 days before and 1 day after block time.
A nonce stops a signed action from working twice, so a transfer’s receiver can’t replay it and drain the sender. Ethereum’s per-address counter (exactly the previous nonce plus one) would break an order book: thousands of orders and cancels a second can’t stay in sequence once two cross in flight.
HyperCore keeps recent nonces per signer instead: out of order is fine, twice is not. Say your bot signs three orders at nonces t, t + 1 and t + 2, where t is the current millisecond, and the third lands first. The other two still pass: unused, and above the smallest stored nonce. Resend t + 1 after it lands and it’s refused.
Each API wallet has its own set.
Three checks on every nonce
Section titled “Three checks on every nonce”HyperCore stores each signer’s 100 highest nonces. A new nonce must:
- be larger than the smallest of those 100,
- never have been used, and
- lie inside
(T - 2 days, T + 1 day), whereTis the block timestamp in milliseconds.
The signer is whoever’s key signed: the account, or its API wallet. Sub-accounts and vaults signed for by one API wallet share that wallet’s nonce set.
Rule 3 makes the docs’ recommended nonce, the current time in milliseconds, close to mandatory: fixtures with a fixed 1700000000000 produce correct hashes the exchange rejects. Rule 1 tolerates reordering: with steady millisecond nonces, a late action survives roughly until 100 newer ones land.
Generating nonces
Section titled “Generating nonces”| Design | What happens | Good for |
|---|---|---|
Date.now() on every call |
Two actions in the same millisecond get the same nonce; the second is refused | Nothing concurrent |
| Atomic counter: start at the current millisecond, add 1 per call, jump forward to the current time when it falls more than 300 ms behind | Unique and increasing across threads | One process, many threads |
max(now, last + 1) kept per signer and network, with a lock so requests leave in nonce order |
Unique per signer, and arrival order matches nonce order | One process, several signers |
| One API wallet per process | Separate signers, separate nonce sets, nothing to coordinate | Several processes |
Processes sharing one wallet collide: nonces are per signer, not per process. So does one API wallet trading two sub-accounts in parallel. The docs’ recipe for porting a strategy from a centralised exchange, robust to out-of-order arrival within 2 seconds:
- One API wallet per trading process; separate wallets for separate sub-accounts.
- Batch orders and cancels every 0.1 seconds, IOC and GTC apart from ALO: all-ALO batches are prioritised by the validators.
- The trading logic sends orders and cancels to the batching task.
- Each batch’s nonce comes from an atomic counter, fast-forwarded to the current millisecond when needed.
Killing an action in flight
Section titled “Killing an action in flight”{"type":"noop"} only marks its nonce used. Signed with an in-flight action’s nonce, it races it; if it lands first, the original is refused. The docs rate this above a cancel for in-flight orders.
Setting an expiry with expiresAfter
Section titled “Setting an expiry with expiresAfter”An L1 action may carry expiresAfter, a millisecond timestamp after which it is rejected; user-signed actions cannot. It is in the signed hash, and narrows the nonce window’s 2 days for orders priced for the moment of signing. An action cancelled as expired costs 5 times the usual address-based rate limit.
Approving an API wallet
Section titled “Approving an API wallet”The account’s own key approves one with the user-signed approveAgent:
{ "type": "approveAgent", "signatureChainId": "0xa4b1", "hyperliquidChain": "Mainnet", "agentAddress": "0x0000000000000000000000000000000000000001", "agentName": "bot1 valid_until 1724776000000", "nonce": 1717000000000}It is signed as HyperliquidTransaction:ApproveAgent(string hyperliquidChain,address agentAddress,string agentName,uint64 nonce). A wallet approved by a master account can sign for the master or any of its sub-accounts.
| Topic | Rule |
|---|---|
| Named and unnamed | Without agentName it is the unnamed API wallet, and the name signs as the empty string; with one it is named. Names are 1 to 16 characters. |
| Expiry | Append valid_until <timestamp in ms> to the name; at most 180 days in the future. |
| Replacement | A new unnamed wallet deregisters the old unnamed one; reusing a name deregisters its holder. There is no revoke action: replacement, expiry and the master running out of funds are how a wallet goes away. |
| Pruning and replay | A deregistered or expired wallet, or one whose master has no funds, may be pruned with its used-nonce state, after which its old signed actions could replay. Never re-approve an old address: generate a new key every time. |
| Reading | Info requests need the account’s address, not the wallet’s. userRole on a wallet returns {"role":"agent","data":{"user":"0x..."}}. The undocumented extraAgents lists {address, name, validUntil}, and returns null, not [], when there are none. |
| Per network | approveAgent signs hyperliquidChain, so a testnet approval is unknown on mainnet. |
What an API wallet can sign
Section titled “What an API wallet can sign”| Actions | |
|---|---|
| API wallet can sign | L1 actions: orders, cancels, modifies, scheduleCancel, updateLeverage, vaultTransfer, borrowLend, agentSetAbstraction, HIP-3 perpDeploy, HIP-4 deploy and settle actions, and agentSendAsset, whose destination must equal the source |
| Account key only | User-signed actions: usdSend, spotSend, sendAsset, withdraw3, usdClassTransfer, cDeposit, cWithdraw, tokenDelegate, approveAgent, approveBuilderFee, userSetAbstraction |
| Exception | activateOutcomeDeployer is an L1 action but rejects API wallet signatures, even from an approved wallet |
| Unresolved | Whether userOutcome split and merge accept an API wallet signature: two codebases sign them with one, a third refuses to |
Account modes show the split on the wire:
| Action | Signer | Values |
|---|---|---|
userSetAbstraction |
account | "disabled", "unifiedAccount", "portfolioMargin" |
agentSetAbstraction |
API wallet | "i", "u", "p", for the same three modes |
userAbstraction (reads it back) |
unsigned info request | "unifiedAccount", "portfolioMargin", "disabled", "default", "dexAbstraction" |
In a multi-sig action only the leader’s nonce set is checked and updated; if the leader is an authorised user’s API wallet, that wallet’s nonce is used.
The numbers
Section titled “The numbers”| Parameter | Value |
|---|---|
| Nonces kept per signer | the 100 highest |
| Nonce window | (T - 2 days, T + 1 day), T the block timestamp in ms |
| Recommended nonce | current time in milliseconds |
expiresAfter |
millisecond timestamp; an action rejected as stale costs 5x the address rate limit |
| API wallets per account, per the docs | 1 unnamed, up to 3 named, plus 2 named per sub-account |
| API wallet name | 1 to 16 characters, not counting the valid_until suffix |
| Longest approval | 180 days ahead |
| Batch cadence suggested by the docs | every 0.1 s |
Running bots without risking withdrawals
Section titled “Running bots without risking withdrawals”Give each bot process its own API wallet, never your funds key: a leaked API wallet can trade but not withdraw. Set a valid_until so a forgotten key expires.
For keeping API wallets safe, see API wallets and agents. For scheduleCancel, the dead man’s switch, see Orders.