EIP-712 Signing
This page is the field-level reference for every EIP-712 message Hypercall verifies: the three directive signing domains (API wallet, Manager, RSM), the off-chain-only perp messages that share the API wallet domain, and the standalone Physical Covered Call Quote domain. Options orders on the REST API use a different domain, Hypercall; see Authentication & Signing.
Everything here was checked against the contracts deployed on HyperEVM mainnet (chain ID 999): Exchange 0.6.1 at 0xE6b7FaA216cabb2b38B392f577C9c826378b2240 and Account 0.2.4. The domain separators and typehashes below are embedded in that deployed bytecode.
Overview
A directive is one action against one Account, signed off-chain and submitted on-chain by Hypercall's sequencer through Exchange.executeUserDirective or Exchange.executeRsmDirective. The Account recovers the signer from the EIP-712 digest and checks authorization:
- API wallet: an address registered on the Account with
hc_update_api_wallet(Account.isApiWalletActive). Signs orders and cancels. - Manager: the Account owner (
Account.manager). Signs API wallet changes, asset sends, and account-mode changes. - RSM signer: protocol-controlled (
Exchange.rsmSigner). Signs credits, withdrawals, liquidation directives, and reduce-only recovery orders. Users never sign these.
Directive Encoding
On-chain, a directive is abi.encodePacked(account, nonce, version, id, data):
| Bytes | Field | Meaning |
|---|---|---|
| 20 | account | The Account address (address) |
| 8 | nonce | The signer's nonce (uint64) |
| 1 | version | Action family |
| 3 | id | Action ID within the family |
| rest | data | abi.encode of the inner action struct |
| Version | Family | Action IDs |
|---|---|---|
0 | HyperCall (HC) | 2 update API wallet. 1 (transfer option) is reserved and rejected. |
1 | HyperLiquid (HL) | 1 limit order, 9 add API wallet, 10 cancel by oid, 11 cancel by cloid, 13 send asset, 16 set abstraction |
255 | System | 1 credit token, 2 credit option, 3 start liquidation, 4 stop liquidation, 5 withdraw token |
You do not build this byte string yourself. Sign the typed data, then submit account, nonce, action, and signature to the API, which encodes the directive.
Typed Data Shape
Every directive has the same outer shape: the Account address, the signer's nonce, and one inner action struct.
<Outer>(address account,uint64 nonce,<Inner> action)<Inner>(...)
The outer type string appends the inner type definition, so the typehash covers both. In EIP-712 terms, primaryType is the outer name, types has both entries, and message is { account, nonce, action }. Field order is part of the type string and must match exactly.
Unsigned Funding Entrypoints
Not every funding call is an EIP-712 action. These methods are direct transactions sent by the paying wallet or router:
function depositUsdcFor(address account, uint256 amount) external;
function depositOption(address account, address token, uint256 amount) external;
depositUsdcFor is intentionally unsigned because msg.sender is only the USDC payer. The credited Hypercall account is the explicit account argument and the UsdcDeposit.account event field. Routers and zaps may call this method, so indexers and backend services must not use msg.sender for credit attribution.
depositOption burns option tokens from msg.sender and emits Deposit(account, msg.sender, token, amount) for the RSM indexer. The option credit path is event-driven and does not use a manager, agent, or RSM signature from the depositor.
Standalone Domain: Physical Covered Call Quotes
Physical covered-call quotes are verified by the factory deployed on the selected chain. They do not use the Exchange verifier or its zero-address domain.
Domain:
{
"name": "Hypercall Physical Covered Call",
"version": "1",
"chainId": "<selected chain id>",
"verifyingContract": "<exact PhysicalCoveredCallFactory address>"
}
Signer: the market maker encoded in PhysicalCoveredCallQuote.marketMaker.
Canonical nested types:
AssetTerms(address underlyingWrapper)
OptionTerms(address seller,uint256 underlyingAmount,uint64 strikeX8,uint64 expiry)
PhysicalCoveredCallRequest(bytes32 rfqId,AssetTerms asset,OptionTerms option)AssetTerms(address underlyingWrapper)OptionTerms(address seller,uint256 underlyingAmount,uint64 strikeX8,uint64 expiry)
PhysicalCoveredCallQuote(bytes32 requestHash,bytes32 quoteId,address marketMaker,uint256 premiumSettlement,uint64 quoteDeadline,uint64 nonce)
The factory recomputes requestHash from the complete nested request before recovering the quote signer. The signature therefore binds the seller, underlying wrapper, wrapper-share amount, strike, expiry, premium, quote deadline, nonce, chain id, and factory address. Settlement token and decimals are authoritative factory configuration, not separately signed values. Pyth is absent from this oracleless generation. Both the quote digest and the maker nonce are consumed when the seller funds a vault.
Only the seller encoded in the request can submit the signed quote. Funding mints the writer-claim NFT to the seller and the long-option NFT to the quoted market maker, provided that maker is an eligible recipient while the optional buyer allowlist is enabled. A valid signature authorizes funding, not exercise.
Only the current long-option NFT owner may call exercise. This owner-only rule prevents an ERC-721 operator from consuming the owner's settlement-token allowance. An approved operator may still transfer the NFT under standard ERC-721 rules, which transfers the economic right to an eligible recipient while the buyer gate is enabled. Successful exercise pulls strike cash from the caller-owner, delivers underlying to that owner, and burns the long-option NFT.
EIP-712 Domain Separators
The three directive domains use the same structure and differ only in name. The verifyingContract is the zero address, so the same signature is valid for any Account on the chain, and the Account address inside the message is what binds it to one Account.
{
"name": "<DomainName>",
"version": "1",
"chainId": <chainId>,
"verifyingContract": "0x0000000000000000000000000000000000000000"
}
The domain type is EIP712Domain(string name,string version,uint256 chainId,address verifyingContract).
Chain IDs: mainnet 999, testnet 998. The Account selects the separator from the chain it runs on, so a testnet signature never verifies on mainnet.
| Domain | name | version | Signer | Chain | Separator |
|---|---|---|---|---|---|
| API wallet | HypercallApiSign | 1 | API wallet | 999 | 0x462ae8a3a67da57ddd088e60be368536f992b8bf369d13e64e620c51993e5ebb |
| API wallet | HypercallApiSign | 1 | API wallet | 998 | 0x339bee4c00da9a5361f687797205c8d03f323659f5d9dc9e0505b9437ddee25e |
| Manager | HypercallManagerSign | 1 | Manager | 999 | 0xc9b5b0b8b0d9d7ba716326acbf795cfe431ef960a25a8f958d6569bea2f1516e |
| Manager | HypercallManagerSign | 1 | Manager | 998 | 0xd1f76b6138be892c14b71b0569bdb049cb44f239d34c78ef1ffaacd2466f9f18 |
| RSM | HypercallRsmSign | 1 | RSM signer | 999 | 0x3d0cae2af623c614099dbadd67a1e1457fabde576aa270a70a57e39bf338a7be |
| RSM | HypercallRsmSign | 1 | RSM signer | 998 | 0x650b282053fb61d3fd477bdc28f6434311fe905e27cc4ca643e87e802c45938c |
Compute the separator yourself and compare before signing anything:
import { ethers } from "ethers";
const separator = ethers.TypedDataEncoder.hashDomain({
name: "HypercallApiSign",
version: "1",
chainId: 999,
verifyingContract: ethers.ZeroAddress
});
// 0x462ae8a3a67da57ddd088e60be368536f992b8bf369d13e64e620c51993e5ebb
Message Reference
Signer and Endpoint Matrix
| Message | Domain | Signer | Submit to | Executes |
|---|---|---|---|---|
HLOrder | API | API wallet | POST /v1/tee-perp/orders/submit, or POST /v1/actions/hl_limit_order | Hyperliquid limit order |
HLCancel | API | API wallet | POST /v1/tee-perp/cancels/submit-by-oid, or POST /v1/actions/hl_cancel_by_oid | Cancel by oid |
HLCancelByCloid | API | API wallet | POST /v1/tee-perp/cancels/submit, or POST /v1/actions/hl_cancel_by_cloid | Cancel by cloid |
HCPerpTriggerOrder | API | API wallet | POST /v1/tee-perp/orders/submit-trigger | Native trigger order (TEE only) |
HCPerpOrderBatch | API | API wallet | POST /v1/tee-perp/orders/submit-batch | Native order batch (TEE only) |
HCPerpLeverageChange | API | API wallet | POST /v1/tee-perp/leverage/submit | Leverage update (TEE only) |
HCPerpScheduleCancel | API | API wallet | POST /v1/tee-perp/schedule-cancel/submit | Dead-man switch (TEE only) |
HCUpdateApiWallet | Manager | Manager | POST /v1/actions/hc_update_api_wallet | Account API wallet registry |
HLAddApiWallet | Manager | Manager | POST /v1/actions/hl_add_api_wallet | HyperCore API wallet approval |
HLSendAsset | Manager | Manager | POST /v1/actions/hl_send_asset | HyperCore sendAsset from the Account |
HLSetAbstraction | Manager | Manager | POST /v1/actions/hl_set_abstraction | HyperCore account mode (unified only) |
HCTransferOption | Manager | Manager | Not accepted | Reserved, rejected on-chain |
HLOrder, HLCancel, HLCancelByCloid, HLSendAsset | RSM | RSM signer | Sequencer only | Recovery orders and sweeps via the Account |
System* | RSM | RSM signer | Sequencer only | Credits, withdrawals, liquidation via the Exchange |
All /v1/actions/{action} routes take account, nonce, action, and signature. POST /v1/actions/{action}/typed-data returns the exact typed data to sign for that request, which is the easiest way to confirm your encoder. GET /v1/directives/{directive_id} reports the on-chain result. Request schemas live in the API Reference.
Wire Conventions
nonceand everyuint64,uint128, oruint256field are JSON integers up to9007199254740991, or decimal strings. Use strings above that.signatureis a 65-byte0xhex string (r || s || v).vmay be0,1,27, or28.bytes32fields are0x-prefixed 32-byte hex. Addresses are checksummed or lowercase hex.- Perp prices and sizes are the HyperCore
1e8integers, for example$50,000is5000000000000and0.001coins is100000.
API Wallet Directives (HypercallApiSign)
Signed by an active API wallet of the Account. Use chain ID 999, name HypercallApiSign.
Test vectors use a placeholder account and a fixed nonce so you can check your encoder byte for byte. The digest is keccak256(0x1901 || domainSeparator || directiveStructHash).
HLOrder
One Hyperliquid limit order for the Account.
Struct:
struct LimitOrder {
uint32 asset;
bool isBuy;
uint64 limitPx;
uint64 sz;
bool reduceOnly;
uint8 encodedTif;
uint128 cloid;
}
struct HLOrder {
address account;
uint64 nonce;
LimitOrder action;
}
Canonical EIP-712 type:
HLOrder(address account,uint64 nonce,LimitOrder action)LimitOrder(uint32 asset,bool isBuy,uint64 limitPx,uint64 sz,bool reduceOnly,uint8 encodedTif,uint128 cloid)
Typehash: 0xe6facb816c5f9c321054ef43c7d8fb1ec750f65d630ec39c5c18119a6f4707dd. Signer: API wallet. Action: version 1, id 1.
asset: the Hyperliquid asset index.GET /v1/tee-perp/marketslists the perp markets Hypercall accepts and their indices.isBuy:trueto buy,falseto sell.limitPx,sz: price and size times1e8.reduceOnly:trueif the order may only reduce a position.encodedTif: ALO =1, GTC =2, IOC =3. Any other value is rejected.cloid: client order ID, nonzero. You use it later to cancel by cloid.
Example (ethers.js):
const domain = {
name: "HypercallApiSign",
version: "1",
chainId: 999,
verifyingContract: ethers.ZeroAddress
};
const types = {
LimitOrder: [
{ name: "asset", type: "uint32" },
{ name: "isBuy", type: "bool" },
{ name: "limitPx", type: "uint64" },
{ name: "sz", type: "uint64" },
{ name: "reduceOnly", type: "bool" },
{ name: "encodedTif", type: "uint8" },
{ name: "cloid", type: "uint128" }
],
HLOrder: [
{ name: "account", type: "address" },
{ name: "nonce", type: "uint64" },
{ name: "action", type: "LimitOrder" }
]
};
const message = {
account: managedAccount,
nonce: selectedNonce,
action: {
asset: 0,
isBuy: true,
limitPx: 5000000000000n,
sz: 100000n,
reduceOnly: false,
encodedTif: 2,
cloid: selectedNonce
}
};
const signature = await apiWalletSigner.signTypedData(domain, types, message);
Test vector (account 0x1111111111111111111111111111111111111111, nonce 1760000000000, chain 999):
| Step | Value |
|---|---|
| Typehash | 0xe6facb816c5f9c321054ef43c7d8fb1ec750f65d630ec39c5c18119a6f4707dd |
| Action struct hash | 0x0fd024c6bf8f132123e63a78d50008d8c39c52e5409ef6ca87a4eec8f6eabb41 |
| Directive struct hash | 0xd32948ee1f249b96124a6acdb5845d09aad8228512098598de6858d5484fe703 |
| Digest to sign | 0xc3c42699092fe20c7dbc4205a1027f55c28d33a682a64bd3f04e40f1d28693f5 |
The test vector uses isBuy: true, limitPx: 5000000000000, sz: 100000, reduceOnly: false, encodedTif: 2, asset: 0, and cloid equal to the nonce.
Submit: POST /v1/tee-perp/orders/submit with account, nonce, action, and signature. Hypercall runs the margin check, then a secure enclave signs and sends the native Hyperliquid order with the Hypercall builder code attached. The Account manager must first approve that builder with Account.approveZeroFeeBuilder; see Onboarding. See Perps for the full flow.
{
"account": "0x1111111111111111111111111111111111111111",
"nonce": 1760000000000,
"action": {
"asset": 0,
"isBuy": true,
"limitPx": "5000000000000",
"sz": "100000",
"reduceOnly": false,
"encodedTif": 2,
"cloid": "1760000000000"
},
"signature": "0x<65 byte signature>"
}
The same signed message is also accepted at POST /v1/actions/hl_limit_order, which encodes it as an on-chain directive executed through Exchange.executeUserDirective and Account.performCoreAction.
HLCancel
Cancels one order by Hyperliquid order ID. The wrapper is HLCancel and the inner struct is CancelOrderByOid.
struct CancelOrderByOid {
uint32 asset;
uint64 oid; // Order ID assigned by HyperCore
}
struct HLCancel {
address account;
uint64 nonce;
CancelOrderByOid action;
}
HLCancel(address account,uint64 nonce,CancelOrderByOid action)CancelOrderByOid(uint32 asset,uint64 oid)
Typehash: 0x23c5529f34a875b6c0d11a6dfafdd945996dbdc5fbd1bdace2c2e0cf6a253a85. Signer: API wallet. Action: version 1, id 10. Submit: POST /v1/tee-perp/cancels/submit-by-oid, or POST /v1/actions/hl_cancel_by_oid.
const types = {
CancelOrderByOid: [
{ name: "asset", type: "uint32" },
{ name: "oid", type: "uint64" }
],
HLCancel: [
{ name: "account", type: "address" },
{ name: "nonce", type: "uint64" },
{ name: "action", type: "CancelOrderByOid" }
]
};
const message = {
account: managedAccount,
nonce: selectedNonce,
action: { asset: 0, oid: 123456789n }
};
const signature = await apiWalletSigner.signTypedData(domain, types, message);
Test vector (account 0x1111111111111111111111111111111111111111, nonce 1760000000000, chain 999):
| Step | Value |
|---|---|
| Typehash | 0x23c5529f34a875b6c0d11a6dfafdd945996dbdc5fbd1bdace2c2e0cf6a253a85 |
| Action struct hash | 0xa34e7bb8af0d099f9c48095924e53067bcb9b542a816445b96abebbc5646b727 |
| Directive struct hash | 0x69d560ba5ef3e88459bde7fb8252e55dbc33627373c8f0cfcf8ef0868f59e097 |
| Digest to sign | 0x5046aba9fb33c55ce736faf63734ea66914f3f168ff0aff2229fa6f31fa9b330 |
The test vector uses asset: 0 and oid: 123456789.
HLCancelByCloid
Cancels one order by the client order ID you set on HLOrder.
struct CancelOrderByCloid {
uint32 asset;
uint128 cloid; // Client order ID
}
struct HLCancelByCloid {
address account;
uint64 nonce;
CancelOrderByCloid action;
}
HLCancelByCloid(address account,uint64 nonce,CancelOrderByCloid action)CancelOrderByCloid(uint32 asset,uint128 cloid)
Typehash: 0x10cbcfa56297fc737e4defee7b7937a52978ebbe5920e584317f7f25416836f5. Signer: API wallet. Action: version 1, id 11. Submit: POST /v1/tee-perp/cancels/submit, or POST /v1/actions/hl_cancel_by_cloid.
const types = {
CancelOrderByCloid: [
{ name: "asset", type: "uint32" },
{ name: "cloid", type: "uint128" }
],
HLCancelByCloid: [
{ name: "account", type: "address" },
{ name: "nonce", type: "uint64" },
{ name: "action", type: "CancelOrderByCloid" }
]
};
const message = {
account: managedAccount,
nonce: selectedNonce,
action: { asset: 0, cloid: 1760000000000n }
};
const signature = await apiWalletSigner.signTypedData(domain, types, message);
Test vector (account 0x1111111111111111111111111111111111111111, nonce 1760000000000, chain 999):
| Step | Value |
|---|---|
| Typehash | 0x10cbcfa56297fc737e4defee7b7937a52978ebbe5920e584317f7f25416836f5 |
| Action struct hash | 0xfa4ab6440f946a6e279d90972d71223c768c01c8a91dc22703a6448dbcb966d0 |
| Directive struct hash | 0x998efb01bb9c842eea80e8cc084b9e4e7ec13b2111790e1acbfec32e49060d9d |
| Digest to sign | 0x56af82a6aa7fa50907a23c6cfe3aa556af5aa087b1157163ebf6ea0240e8d29d |
The test vector uses asset: 0 and cloid: 1760000000000.
TEE-Only Perp Messages
These four messages are verified by the Hypercall API, not by the Account contract, because the enclave executes them natively on Hyperliquid and no on-chain directive exists for them. They use the same HypercallApiSign domain and the same API wallet authorization as HLOrder (the signer must be an active API wallet of the Account, and the Account must be portfolio margin). Their typehashes are not in the contracts. Nonces are Unix time in milliseconds.
PerpOrderTerms
HCPerpTriggerOrder and HCPerpOrderBatch both embed this struct. A limit child signs its TIF byte with triggerPx = 0, isMarket = false, tpsl = 0. A trigger child signs encodedTif = 0, a positive triggerPx, and tpsl of 1 (take profit) or 2 (stop loss).
struct PerpOrderTerms {
uint32 asset;
bool isBuy;
uint64 limitPx;
uint64 sz;
bool reduceOnly;
uint8 encodedTif;
uint64 triggerPx;
bool isMarket;
uint8 tpsl;
uint128 cloid;
}
HCPerpTriggerOrder
One stop-loss or take-profit order, submitted to POST /v1/tee-perp/orders/submit-trigger. The request action.orderType must be a trigger.
struct HCPerpTriggerOrder {
address account;
uint64 nonce;
PerpOrderTerms order;
}
HCPerpTriggerOrder(address account,uint64 nonce,PerpOrderTerms action)PerpOrderTerms(uint32 asset,bool isBuy,uint64 limitPx,uint64 sz,bool reduceOnly,uint8 encodedTif,uint64 triggerPx,bool isMarket,uint8 tpsl,uint128 cloid)
Typehash: 0x2d3104ab255275839aad254429e4be789f64a8d16b8e3e7d5f30a0db3c887b5c
Test vector (account 0x1111111111111111111111111111111111111111, nonce 1760000000000, chain 999):
| Step | Value |
|---|---|
| Typehash | 0x2d3104ab255275839aad254429e4be789f64a8d16b8e3e7d5f30a0db3c887b5c |
| Action struct hash | 0x285b3186be219b92d7159419288c0dce9bc4ad7bfc9536e009ebf24bd67f434c |
| Directive struct hash | 0xaa03e40b34b15e7e932ae827921930e7538387b596ef1040dbcc791e69021299 |
| Digest to sign | 0xc46aadc390b66c235af15e199e188ab23fb7f7f1b9237f6c2849c264c28d94da |
The test vector is a reduce-only market stop-loss sell: asset: 0, isBuy: false, limitPx: 4800000000000 (worst price), sz: 100000, reduceOnly: true, encodedTif: 0, triggerPx: 4900000000000, isMarket: true, tpsl: 2, cloid: 1760000000001.
HCPerpOrderBatch
Up to 100 orders under one grouping, submitted to POST /v1/tee-perp/orders/submit-batch. Every order must use a distinct cloid. grouping is 0 (no grouping), 1 (normalTpsl), or 2 (positionTpsl). A batch counts as one order placement against the rate limit.
struct HCPerpOrderBatch {
address account;
uint64 nonce;
uint8 grouping;
PerpOrderTerms[] orders;
}
HCPerpOrderBatch(address account,uint64 nonce,uint8 grouping,PerpOrderTerms[] orders)PerpOrderTerms(uint32 asset,bool isBuy,uint64 limitPx,uint64 sz,bool reduceOnly,uint8 encodedTif,uint64 triggerPx,bool isMarket,uint8 tpsl,uint128 cloid)
Typehash: 0x9d4f467a44f4b0d39f234f3b91888546ce31dcbb67ce791550901b9042c38f8b. The orders array encodes as keccak256 of the concatenated PerpOrderTerms struct hashes, in submission order.
Test vector (account 0x1111111111111111111111111111111111111111, nonce 1760000000000, chain 999):
| Step | Value |
|---|---|
| Typehash | 0x9d4f467a44f4b0d39f234f3b91888546ce31dcbb67ce791550901b9042c38f8b |
| Orders array hash | 0x13fc75fcc69bf3b4ef7cdc3a0872c3cc1cf59440c74d366055109b6bd5ea7c98 |
| Directive struct hash | 0x69d0b1131e386769823fe1ab3facbe8e6fc013f86a117cf95cb52b8ce2f274e0 |
| Digest to sign | 0x4b5cd34b1f254fdd9881b7674a3412807e16cade1f2ccc7a1e261482682fbd07 |
The test vector is grouping: 1 with two orders: a GTC limit buy (limitPx: 5000000000000, sz: 100000, cloid: 1760000000000) followed by the stop-loss from the trigger example.
HCPerpLeverageChange
Sets leverage for one asset, submitted to POST /v1/tee-perp/leverage/submit. leverage must be greater than zero.
struct PerpLeverageChange {
uint32 asset;
bool isCross;
uint32 leverage;
}
struct HCPerpLeverageChange {
address account;
uint64 nonce;
PerpLeverageChange action;
}
HCPerpLeverageChange(address account,uint64 nonce,PerpLeverageChange action)PerpLeverageChange(uint32 asset,bool isCross,uint32 leverage)
Typehash: 0xe13845f0e6c16a5e1fc56b2d11fb0972faa2ec30b01e94d58c72147fa9b6a463
Test vector (account 0x1111111111111111111111111111111111111111, nonce 1760000000000, chain 999):
| Step | Value |
|---|---|
| Typehash | 0xe13845f0e6c16a5e1fc56b2d11fb0972faa2ec30b01e94d58c72147fa9b6a463 |
| Action struct hash | 0x0e6e271dd56192d7fb1f37ea2eaffecfbbe2d1f79cac78dce6a6930bb8cc253e |
| Directive struct hash | 0xe04b169bbb0e20042308e5aad1036ce60eda75f484a4d6093dc60f2c2231129e |
| Digest to sign | 0xb2a43487152a7cecc73d2b6b84d817a58dd02c559b05289a8d8e1b5c3d25e266 |
The test vector sets cross leverage to 5x on asset: 0.
HCPerpScheduleCancel
Hyperliquid's dead-man switch: cancel every open order at time (Unix milliseconds, at least seven seconds ahead). Submitted to POST /v1/tee-perp/schedule-cancel/submit. To clear a scheduled cancel, omit time in the request and sign time = 0. Sending an explicit 0 is rejected.
struct PerpScheduleCancel {
uint64 time;
}
struct HCPerpScheduleCancel {
address account;
uint64 nonce;
PerpScheduleCancel action;
}
HCPerpScheduleCancel(address account,uint64 nonce,PerpScheduleCancel action)PerpScheduleCancel(uint64 time)
Typehash: 0xf21b6881e346bbf4b8d82e8ea507cf487e58dfc08776ad662591be7d6065bdfb
Test vector (account 0x1111111111111111111111111111111111111111, nonce 1760000000000, chain 999):
| Step | Value |
|---|---|
| Typehash | 0xf21b6881e346bbf4b8d82e8ea507cf487e58dfc08776ad662591be7d6065bdfb |
| Action struct hash | 0x0a3ebfe48ef28a79beeee4d56e524f8a5280c38562efb697e7b1d63b3d475afc |
| Directive struct hash | 0xae64a13943a6a35b33aeb3095763b855c58c2119daf71e1e3228d8acf2645e8b |
| Digest to sign | 0xa01806f47299010b5963a516562be3c3b44fed4f4a9e2e56c4bb601630146cdc |
The test vector schedules a cancel at time: 1760000060000.
Manager-Signed Prepared Cancel
POST /v1/tee-perp/cancels/submit-prepared is the manager-signed cancel path. It does not use a typed-data struct. The manager signs the digest of a prepared native Hyperliquid cancel, supplied as preparedRequest (base64) with managerSignature in canonical low-s form, plus a timestamped authenticationSignature that authenticates the manager wallet. See the API Reference for the request schema.
Manager Directives (HypercallManagerSign)
Signed by the Account manager. Use chain ID 999, name HypercallManagerSign. The Account requires signer == manager. Submit with POST /v1/actions/{action}.
HCUpdateApiWallet
Adds, replaces, or removes (addr = 0x0) the API wallet stored under name on the Account.
struct UpdateApiWallet {
bytes32 name; // Wallet label, ASCII left-aligned and zero padded
address addr; // API wallet address, zero to remove
}
struct HCUpdateApiWallet {
address account;
uint64 nonce;
UpdateApiWallet action;
}
HCUpdateApiWallet(address account,uint64 nonce,UpdateApiWallet action)UpdateApiWallet(bytes32 name,address addr)
Typehash: 0x8ebe7d19374200e5dc3ae838db74b370da95bd2b3a8aec9de85d2398dd4a9257. Action: version 0, id 2. Submit: POST /v1/actions/hc_update_api_wallet with action of { "name": "0x...", "addr": "0x..." }.
const domain = {
name: "HypercallManagerSign",
version: "1",
chainId: 999,
verifyingContract: ethers.ZeroAddress
};
const types = {
UpdateApiWallet: [
{ name: "name", type: "bytes32" },
{ name: "addr", type: "address" }
],
HCUpdateApiWallet: [
{ name: "account", type: "address" },
{ name: "nonce", type: "uint64" },
{ name: "action", type: "UpdateApiWallet" }
]
};
const message = {
account: accountAddress,
nonce: selectedNonce,
action: {
name: ethers.encodeBytes32String("trading-bot"),
addr: apiWalletAddress
}
};
const signature = await managerSigner.signTypedData(domain, types, message);
Test vector (account 0x1111111111111111111111111111111111111111, nonce 1760000000000, chain 999):
| Step | Value |
|---|---|
| Typehash | 0x8ebe7d19374200e5dc3ae838db74b370da95bd2b3a8aec9de85d2398dd4a9257 |
| Action struct hash | 0x8b6c4a9db102e9d27c9a4069b39ba35e04948129359f0c15cd5103dd101ffce7 |
| Directive struct hash | 0x830e291835ad182f290bc3d2bff09eaf9fd33acfbe44c6ad583218cd1a8232e5 |
| Digest to sign | 0x06589077544af449314db1fa8c7aac2d277cce9e833a04992d88161ede24cbe8 |
The test vector uses name trading-bot (0x74726164696e672d626f74 right-padded with zeros) and addr 0x2222222222222222222222222222222222222222. See Onboarding for the full API wallet flow. The Account emits ApiWalletUpdated(name, addr).
HLAddApiWallet
Approves a Hyperliquid API wallet for the Account on HyperCore.
struct AddApiWallet {
address apiWalletAddress;
string apiWalletName; // An empty name replaces the main API wallet
}
struct HLAddApiWallet {
address account;
uint64 nonce;
AddApiWallet action;
}
HLAddApiWallet(address account,uint64 nonce,AddApiWallet action)AddApiWallet(address apiWalletAddress,string apiWalletName)
Typehash: 0x71c0e0dfbddc3456015249946354324befde42b6851d1c9b26c6a29f2e0b1ac2. Action: version 1, id 9. Submit: POST /v1/actions/hl_add_api_wallet.
apiWalletName is a string, so its struct field is keccak256(bytes(apiWalletName)). Ethers and viem do this for you.
Test vector (account 0x1111111111111111111111111111111111111111, nonce 1760000000000, chain 999):
| Step | Value |
|---|---|
| Typehash | 0x71c0e0dfbddc3456015249946354324befde42b6851d1c9b26c6a29f2e0b1ac2 |
| Action struct hash | 0xed32a441b0ee59c0252f5dff3411766289c9c35c19e3152b0b2b204176c2c7cd |
| Directive struct hash | 0xb990a3dfb4e8d96243539f8758b3a55249b217542bb4112f4f88ed67ec516ed9 |
| Digest to sign | 0x584886d020316085de0e0dec16331b8e990558be3e8fd004c47200abbebed518 |
The test vector uses apiWalletAddress 0x2222222222222222222222222222222222222222 and apiWalletName hl-bot.
HLSendAsset
Sends assets from the Account on HyperCore to a destination.
struct SendAsset {
address destination; // Recipient
address subAccount; // Recipient sub-account, zero for main
uint32 srcDex; // Source dex: 0 = perps, 4294967295 = spot
uint32 dstDex; // Destination dex: 0 = perps, 4294967295 = spot
uint64 token; // HyperCore token index (0 = USDC)
uint64 amountWei; // Amount in HyperCore wei (USDC: 1e8 per dollar)
}
struct HLSendAsset {
address account;
uint64 nonce;
SendAsset action;
}
HLSendAsset(address account,uint64 nonce,SendAsset action)SendAsset(address destination,address subAccount,uint32 srcDex,uint32 dstDex,uint64 token,uint64 amountWei)
Typehash: 0x7feffdf7e7b191d5d27c2a209ca71c789289da73de203e99546abf9632860595. Action: version 1, id 13. Submit: POST /v1/actions/hl_send_asset.
const types = {
SendAsset: [
{ name: "destination", type: "address" },
{ name: "subAccount", type: "address" },
{ name: "srcDex", type: "uint32" },
{ name: "dstDex", type: "uint32" },
{ name: "token", type: "uint64" },
{ name: "amountWei", type: "uint64" }
],
HLSendAsset: [
{ name: "account", type: "address" },
{ name: "nonce", type: "uint64" },
{ name: "action", type: "SendAsset" }
]
};
const message = {
account: accountAddress,
nonce: selectedNonce,
action: {
destination: recipientAddress,
subAccount: ethers.ZeroAddress,
srcDex: 0,
dstDex: 0xFFFFFFFF, // spot
token: 0, // USDC
amountWei: 500000000n // 5 USDC
}
};
const signature = await managerSigner.signTypedData(domain, types, message);
Test vector (account 0x1111111111111111111111111111111111111111, nonce 1760000000000, chain 999):
| Step | Value |
|---|---|
| Typehash | 0x7feffdf7e7b191d5d27c2a209ca71c789289da73de203e99546abf9632860595 |
| Action struct hash | 0x9b8c0290da19ea1069e14833023c4621a4f043354b36b4f17d4803b1fce6e656 |
| Directive struct hash | 0xa0cb8a72188a61289fc2685c29a424605e0a2ee7b7af3e8cfee10e1c75153e08 |
| Digest to sign | 0x7c7037f27c659a1885f744778480e6847c5d08e98fb22517265ba708b6597d10 |
The test vector sends 5 USDC (amountWei: 500000000) from perps (srcDex: 0) to spot (dstDex: 4294967295) with destination the production Exchange 0xE6b7FaA216cabb2b38B392f577C9c826378b2240.
HLSetAbstraction
Sets the Account's HyperCore account mode. The Account only accepts user equal to itself and abstraction = 2 (unified account); anything else reverts. Portfolio Margin requires it.
struct SetAbstraction {
address user; // Must equal the Account
uint8 abstraction; // Must be 2 (unified account)
}
struct HLSetAbstraction {
address account;
uint64 nonce;
SetAbstraction action;
}
HLSetAbstraction(address account,uint64 nonce,SetAbstraction action)SetAbstraction(address user,uint8 abstraction)
Typehash: 0xcf5fddf081e5f2a9a45aa87cb1a3c42e5aa20164566b1ad2369b6b1318eac786. Action: version 1, id 16. Submit: POST /v1/actions/hl_set_abstraction.
Test vector (account 0x1111111111111111111111111111111111111111, nonce 1760000000000, chain 999):
| Step | Value |
|---|---|
| Typehash | 0xcf5fddf081e5f2a9a45aa87cb1a3c42e5aa20164566b1ad2369b6b1318eac786 |
| Action struct hash | 0xef6c70a3c14b9e77c710c6b3b3ee681ca270ef884bd8a64fa4064a96f3dda84e |
| Directive struct hash | 0xdda9d0fe82b63c728c4ac42585d5998a04cabaed0f957a2cafdf31a754829c3d |
| Digest to sign | 0xf49d7d8d74596e503d6a1bdfb26653533a12a5ba3816acd992d0bd4ef378f59d |
The test vector sets user to the Account and abstraction to 2.
HCTransferOption (reserved)
The contracts define HCTransferOption(address account,uint64 nonce,TransferOption action)TransferOption(address recipient,address option,uint256 amountWei) (typehash 0xa218e1158788b23987f954edc7a07659a03a03fe5ba981a5bbe44d7a146e0819, action version 0, id 1), but no signer type is assigned to it. The Account reverts with Account__UserDirectiveNotSupported, and the API rejects hc_transfer_option. Do not sign it. Move option tokens with depositOption and withdrawals instead.
Manager Transactions Without a Signature
Some manager actions are direct Account calls, not EIP-712 messages: Account.approveZeroFeeBuilder(builder) approves a Hyperliquid builder at a 0% maximum fee and is required once before the first perp order (see Onboarding). Account.transferTo(recipient, token, amount) moves tokens out of the Account on HyperEVM. Both require msg.sender == manager and revert while the Account is in liquidation.
RSM Directives (HypercallRsmSign)
Signed by the RSM signer and submitted by the sequencer. They credit deposits, process withdrawals, start and stop liquidation, and place reduce-only IOC recovery orders. Market makers and users do not sign or submit them; they are documented so you can verify what the protocol may do to an Account. Use chain ID 999, name HypercallRsmSign. The Exchange checks signer == rsmSigner (currently 0x502E7fAbF34E84EeB115C34FE947b79A94dF886E on mainnet) and the caller must hold SEQUENCER_ROLE.
RSM Orders and Sweeps (executed by the Account)
These reuse the HL structs under the RSM domain, so the typehashes equal the API and manager ones and only the domain separator differs. The Account enforces extra constraints and runs them even during liquidation.
| Message | Typehash | Constraint enforced by the Account |
|---|---|---|
HLOrder | 0xe6facb816c5f9c321054ef43c7d8fb1ec750f65d630ec39c5c18119a6f4707dd | encodedTif must be IOC (3), and the order must be reduceOnly, or a spot sell into USDC (spot asset ids 10000 to 99999) limited no more than 5% below the HyperCore spot price |
HLCancel | 0x23c5529f34a875b6c0d11a6dfafdd945996dbdc5fbd1bdace2c2e0cf6a253a85 | None |
HLCancelByCloid | 0x10cbcfa56297fc737e4defee7b7937a52978ebbe5920e584317f7f25416836f5 | None |
HLSendAsset | 0x7feffdf7e7b191d5d27c2a209ca71c789289da73de203e99546abf9632860595 | destination must be the Exchange |
RSM HLOrder test vector (account 0x1111111111111111111111111111111111111111, nonce 1760000000000, chain 999):
| Step | Value |
|---|---|
| Typehash | 0xe6facb816c5f9c321054ef43c7d8fb1ec750f65d630ec39c5c18119a6f4707dd |
| Action struct hash | 0x39b4e0699f382aaad23f04740034e7c5581ba918139d2db63fbf586cc447e7db |
| Directive struct hash | 0xe6dde5ad6d310b5ebb83c09ec28ee5568700473b885bb483b7437dca202af4f3 |
| Digest to sign | 0x57afa1e810804e768c05a467aa88ede15fdceedae5aaa840adff61a613aca469 |
The RSM HLOrder vector is a reduce-only IOC sell (isBuy: false, limitPx: 4800000000000, sz: 100000, reduceOnly: true, encodedTif: 3, cloid equal to the nonce). Exchange.executeRsmDirective verifies the signature and forwards the action to the Account.
System Actions (executed by the Exchange)
Version 255. Each wrapper is System<Action>(address account,uint64 nonce,<Action> action)<Action>(...).
| Wrapper | Inner type | Typehash |
|---|---|---|
SystemCreditToken | CreditToken(uint32 srcDex,uint32 dstDex,uint64 token,uint64 amountWei) | 0xe845f2049264182802994dae939dc8ac42d3ae543a7eeb530dd021428743f052 |
SystemCreditOption | CreditOption(bytes32 underlying,uint256 expiry,uint256 strike,bool isCall,uint256 amountWei) | 0x65aedd5af06882e1e62d752b43b72ec62a85cacf2ca59e8ec2affc57eed9d8ae |
SystemStartLiquidation | StartLiquidation(int256 equity,uint256 marginNeeded) | 0x92b6a135a656e489c24fb5511684842b0ab3895c22b51e4910dee90a7d23511e |
SystemStopLiquidation | StopLiquidation(uint256 startTime) | 0xe1af98778e9cfd1d1a679bb0b64b6dc8aa3fb152ff6469d0a81a1c7eb87a4bd3 |
SystemWithdrawToken | WithdrawToken(address destination,address subAccount,uint32 srcDex,uint32 dstDex,uint64 token,uint64 amountWei) | 0x634b866e9a922bb846e297973c7a456e641b88f4e97493da9b222084d799c883 |
Full outer type strings:
SystemCreditToken(address account,uint64 nonce,CreditToken action)CreditToken(uint32 srcDex,uint32 dstDex,uint64 token,uint64 amountWei)
SystemCreditOption(address account,uint64 nonce,CreditOption action)CreditOption(bytes32 underlying,uint256 expiry,uint256 strike,bool isCall,uint256 amountWei)
SystemStartLiquidation(address account,uint64 nonce,StartLiquidation action)StartLiquidation(int256 equity,uint256 marginNeeded)
SystemStopLiquidation(address account,uint64 nonce,StopLiquidation action)StopLiquidation(uint256 startTime)
SystemWithdrawToken(address account,uint64 nonce,WithdrawToken action)WithdrawToken(address destination,address subAccount,uint32 srcDex,uint32 dstDex,uint64 token,uint64 amountWei)
- SystemCreditToken credits spot or perp tokens from HyperCore to the Account.
amountWeimust be nonzero and the Account must be activated on HyperCore. - SystemCreditOption mints option tokens to the Account, deploying the option pair if needed.
underlyingis an OpenZeppelinShortStringword: ASCII left-aligned with the byte length in the last byte (BTCis0x42544300...03). Non-canonical encodings revert. - SystemStartLiquidation records the auction parameters and locks the Account.
marginNeededmust be nonzero. - SystemStopLiquidation requires
startTimeto equal the recorded start and returns the Account to its manager. - SystemWithdrawToken queues Standard USDC delivery on HyperCore from the Exchange's own balance. It requires
subAccount = 0,srcDex = 0,dstDexof0or4294967295,token = 0(USDC), and anamountWeithat is a whole multiple of the EVM unit. A HyperEVM receipt alone is not proof of delivery on HyperCore.
SystemCreditToken test vector (account 0x1111111111111111111111111111111111111111, nonce 1760000000000, chain 999):
| Step | Value |
|---|---|
| Typehash | 0xe845f2049264182802994dae939dc8ac42d3ae543a7eeb530dd021428743f052 |
| Action struct hash | 0xc69d7c6e5c36c9789d990c6316eaff3be43980583806f41f8a8e6461b7114011 |
| Directive struct hash | 0x7ffa37cef7e08d3a2dd5501a8648899d37bc09ebb25d148c124e12c5047476e1 |
| Digest to sign | 0x95f75e5ddb4837de3f578b8aa0d714d4b02672b18c4929e3d2dda4a266de8254 |
The vector credits 5 USDC (srcDex and dstDex of 4294967295, token: 0, amountWei: 500000000).
SystemCreditOption test vector (account 0x1111111111111111111111111111111111111111, nonce 1760000000000, chain 999):
| Step | Value |
|---|---|
| Typehash | 0x65aedd5af06882e1e62d752b43b72ec62a85cacf2ca59e8ec2affc57eed9d8ae |
| Action struct hash | 0x4a8b563fc94d26c1da376145d5aa5d05f996232fcf9739ed0a662650e91861b6 |
| Directive struct hash | 0x190b638f1a462469bf0d070248acfc86bbbb95ea8a9d3d97d33581c7d6bb5554 |
| Digest to sign | 0x86fb7c53bb7e139a4d5df7db2feae40e634efe09ea7509165d3cece75d75fce2 |
The vector credits one BTC call (underlying: 0x42544300...03, expiry: 1761811200, strike: 80000, isCall: true, amountWei: 1000000000000000000).
SystemStartLiquidation test vector (account 0x1111111111111111111111111111111111111111, nonce 1760000000000, chain 999):
| Step | Value |
|---|---|
| Typehash | 0x92b6a135a656e489c24fb5511684842b0ab3895c22b51e4910dee90a7d23511e |
| Action struct hash | 0x4cd6a7f0997eba84ef82eabd429a0d9ca80e55bd6d89d2e67eda9970d87ec803 |
| Directive struct hash | 0x84f18dc2577978d7890c9eb269fd787d99de37039b3a08e24745398feffbd0c5 |
| Digest to sign | 0xcc72c0e7829938c9ca0a4515d759f94cc96d2f24cd51a134a430f215ed5f5d4b |
The vector uses equity: -1500000000 and marginNeeded: 20000000000 (both six-decimal USDC).
SystemStopLiquidation test vector (account 0x1111111111111111111111111111111111111111, nonce 1760000000000, chain 999):
| Step | Value |
|---|---|
| Typehash | 0xe1af98778e9cfd1d1a679bb0b64b6dc8aa3fb152ff6469d0a81a1c7eb87a4bd3 |
| Action struct hash | 0x1d900a795b328bd5fa734bd6d7f74634b98ca66eb5e45322fb1ba6400564b62b |
| Directive struct hash | 0x977b9becdd26830f2cdf70ab40f72f24490fe6ed45822d1fc8c84120e9a46f40 |
| Digest to sign | 0xbe65f82782b14dba42e334e32dd7dcc6bf14c96824523723a186a7381f8f12af |
The vector uses startTime: 1760000000.
SystemWithdrawToken test vector (account 0x1111111111111111111111111111111111111111, nonce 1760000000000, chain 999):
| Step | Value |
|---|---|
| Typehash | 0x634b866e9a922bb846e297973c7a456e641b88f4e97493da9b222084d799c883 |
| Action struct hash | 0x10bd8d4372a09e613a6b4fb7018fcccde857ebf6cc3327d6c9a3c0f81515a076 |
| Directive struct hash | 0x86680427965513c65a7a170730b9f4fb2e2b40e22cd18338cb004b6a7872ada2 |
| Digest to sign | 0x65cc392038decb9ccdc23b0a4a1cfbe657d3417f8cd9ebf2319b3172fb7d885c |
The vector withdraws 5 USDC (amountWei: 500000000) to destination 0x1111111111111111111111111111111111111111 with srcDex: 0, dstDex: 0, token: 0.
Nonces
Each signer has an independent nonce space, tracked in three places.
- Account contract: a bitmap per signer.
Account.isNonceUsed(signer, nonce)returns whether a nonce was consumed. A user directive reverts withAccount__NonceAlreadyUsedon reuse. There is nonextNonce; gaps are allowed and order does not matter on-chain. - Exchange contract: a bitmap for the RSM signer, read with
Exchange.isNonceUsed(signer, nonce). - Engine: keeps the 100 highest nonces per signer. A new nonce must not be in the set, must exceed the smallest stored nonce once the set is full, and must be strictly within two days before and one day after the server time in milliseconds.
Use the current Unix time in milliseconds as the nonce seed and increment per signer. Two requests signed by the same wallet in the same millisecond need distinct nonces, so keep a counter. For the TEE perp routes the nonce is also the request's time anchor, so use real millisecond timestamps there rather than small integers.
function isNonceUsed(address signer, uint256 nonce) external view returns (bool);
Signature Verification Flow
- Off-chain: the signer builds the EIP-712 digest (
0x1901 || domainSeparator || structHash) and signs it. - API: Hypercall recovers the signer, checks it is an active API wallet or the manager, runs the engine margin check for orders, then enqueues the directive. The response reports the honest stage, rejection details, and transaction hash.
- Sequencer: submits
Exchange.executeUserDirective(directive, signature). The Exchange checks the Account exists and forwards toAccount.executeUserDirective. - Account: rebuilds the digest from its own address, the nonce, and the action, recovers the signer with the API or manager separator for that action, checks authorization, consumes the nonce, then executes. HL actions go to the HyperCore CoreWriter. HC actions update the API wallet registry.
- Client: polls
GET /v1/directives/{directive_id}for the final status.
The TEE perp routes stop after the API step: the enclave, not the Account, executes the order on Hyperliquid.
Example (HLOrder):
1. API wallet signs HLOrder with the managed account, a unique nonce, and one LimitOrder action
2. Client posts account, nonce, action, and signature to /v1/tee-perp/orders/submit
3. Hypercall recovers the API wallet and applies portfolio-margin admission
4. The enclave signs and sends the native Hyperliquid order with the Hypercall builder code
5. Client reads the stage from the response and polls for subsequent status
Deprecated and Removed
These pieces appeared in earlier versions of this page and are no longer part of the deployed system. They are listed so integrations built against them can be migrated.
- Processor and ActionCaster encoding. The
Processorcontract and ActionCaster message encoding were removed in Exchange0.2.0(February 2026). Account directives now go throughExchange.executeUserDirectiveand CoreWriter. The deployed Exchange0.6.1has none of the old entrypoints. - Exchange entrypoints with their own structs.
hlRequestOrder,hlRequestCancel,hlRequestCancelByCloid,hlActionSendAsset,hcActionWithdrawToken,hcActionWithdrawOption,rsmCommandRebalance, andrsmCommandRepayare gone. Their replacements:
| Removed message | Replacement |
|---|---|
HLRequestCancel (batch of HLCancel{asset,oid} plus nonce) | HLCancel with one CancelOrderByOid per directive |
HLRequestCancelByCloid | HLCancelByCloid with one CancelOrderByCloid per directive |
HLActionSendAsset (account, nonce, destination, srcDex, dstDex, token, amountWei) | HLSendAsset with SendAsset, which adds subAccount |
HCActionWithdrawToken | RSM-signed SystemWithdrawToken, executed by the sequencer after the engine approves the withdrawal |
HCActionWithdrawOption | Option credits are RSM SystemCreditOption; deposits use depositOption |
RsmCommandRebalance | RSM-signed HLOrder (reduce-only IOC) |
RsmCommandRepay | RSM-signed HLSendAsset to the Exchange |
- Legacy order functions.
placeCoreOrders,cancelCoreOrders, andcancelCoreOrdersByCloidused a MsgPack encoding and theCoreSignaturesdomain ("Exchange", chain ID1337). They are removed. nextNonce. The on-chainnextNonce[signer]counter no longer exists. Only the used-nonce bitmap (isNonceUsed) remains.- Mainnet separators marked TBD. Earlier text listed the manager and RSM mainnet separators as TBD. All three are fixed and listed above.
HCTransferOption. Defined but never enabled; see above.
Security Considerations
- Keys: keep the manager key on a hardware wallet. API wallets only sign orders and cancels; use separate keys per system.
- Nonce replay: never reuse a nonce. Track nonces off-chain and increment atomically.
- Domain: always sign with chain ID
999on mainnet (998on testnet), and check your computed separator against the table above. - Verify what you sign: call
POST /v1/actions/{action}/typed-dataand compare it with your own encoder before signing with a hardware wallet. - On-chain verification is final: the API pre-checks signatures, but the Account re-verifies every directive on-chain. Do not treat an API acknowledgement as execution.
- Manager vs API wallet: managers control ownership, API wallets, and asset movement. API wallets only sign trading messages. Use separate keys.
References
- Onboarding for Account creation and API wallet setup
- Contracts overview for deployed addresses and directive types
- Perps for the perp order flow
- Authentication & Signing for options order signing