Skip to main content

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:

  1. API wallet: an address registered on the Account with hc_update_api_wallet (Account.isApiWalletActive). Signs orders and cancels.
  2. Manager: the Account owner (Account.manager). Signs API wallet changes, asset sends, and account-mode changes.
  3. 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):

BytesFieldMeaning
20accountThe Account address (address)
8nonceThe signer's nonce (uint64)
1versionAction family
3idAction ID within the family
restdataabi.encode of the inner action struct
VersionFamilyAction IDs
0HyperCall (HC)2 update API wallet. 1 (transfer option) is reserved and rejected.
1HyperLiquid (HL)1 limit order, 9 add API wallet, 10 cancel by oid, 11 cancel by cloid, 13 send asset, 16 set abstraction
255System1 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.

DomainnameversionSignerChainSeparator
API walletHypercallApiSign1API wallet9990x462ae8a3a67da57ddd088e60be368536f992b8bf369d13e64e620c51993e5ebb
API walletHypercallApiSign1API wallet9980x339bee4c00da9a5361f687797205c8d03f323659f5d9dc9e0505b9437ddee25e
ManagerHypercallManagerSign1Manager9990xc9b5b0b8b0d9d7ba716326acbf795cfe431ef960a25a8f958d6569bea2f1516e
ManagerHypercallManagerSign1Manager9980xd1f76b6138be892c14b71b0569bdb049cb44f239d34c78ef1ffaacd2466f9f18
RSMHypercallRsmSign1RSM signer9990x3d0cae2af623c614099dbadd67a1e1457fabde576aa270a70a57e39bf338a7be
RSMHypercallRsmSign1RSM signer9980x650b282053fb61d3fd477bdc28f6434311fe905e27cc4ca643e87e802c45938c

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​

MessageDomainSignerSubmit toExecutes
HLOrderAPIAPI walletPOST /v1/tee-perp/orders/submit, or POST /v1/actions/hl_limit_orderHyperliquid limit order
HLCancelAPIAPI walletPOST /v1/tee-perp/cancels/submit-by-oid, or POST /v1/actions/hl_cancel_by_oidCancel by oid
HLCancelByCloidAPIAPI walletPOST /v1/tee-perp/cancels/submit, or POST /v1/actions/hl_cancel_by_cloidCancel by cloid
HCPerpTriggerOrderAPIAPI walletPOST /v1/tee-perp/orders/submit-triggerNative trigger order (TEE only)
HCPerpOrderBatchAPIAPI walletPOST /v1/tee-perp/orders/submit-batchNative order batch (TEE only)
HCPerpLeverageChangeAPIAPI walletPOST /v1/tee-perp/leverage/submitLeverage update (TEE only)
HCPerpScheduleCancelAPIAPI walletPOST /v1/tee-perp/schedule-cancel/submitDead-man switch (TEE only)
HCUpdateApiWalletManagerManagerPOST /v1/actions/hc_update_api_walletAccount API wallet registry
HLAddApiWalletManagerManagerPOST /v1/actions/hl_add_api_walletHyperCore API wallet approval
HLSendAssetManagerManagerPOST /v1/actions/hl_send_assetHyperCore sendAsset from the Account
HLSetAbstractionManagerManagerPOST /v1/actions/hl_set_abstractionHyperCore account mode (unified only)
HCTransferOptionManagerManagerNot acceptedReserved, rejected on-chain
HLOrder, HLCancel, HLCancelByCloid, HLSendAssetRSMRSM signerSequencer onlyRecovery orders and sweeps via the Account
System*RSMRSM signerSequencer onlyCredits, 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​

  • nonce and every uint64, uint128, or uint256 field are JSON integers up to 9007199254740991, or decimal strings. Use strings above that.
  • signature is a 65-byte 0x hex string (r || s || v). v may be 0, 1, 27, or 28.
  • bytes32 fields are 0x-prefixed 32-byte hex. Addresses are checksummed or lowercase hex.
  • Perp prices and sizes are the HyperCore 1e8 integers, for example $50,000 is 5000000000000 and 0.001 coins is 100000.

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/markets lists the perp markets Hypercall accepts and their indices.
  • isBuy: true to buy, false to sell.
  • limitPx, sz: price and size times 1e8.
  • reduceOnly: true if 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):

StepValue
Typehash0xe6facb816c5f9c321054ef43c7d8fb1ec750f65d630ec39c5c18119a6f4707dd
Action struct hash0x0fd024c6bf8f132123e63a78d50008d8c39c52e5409ef6ca87a4eec8f6eabb41
Directive struct hash0xd32948ee1f249b96124a6acdb5845d09aad8228512098598de6858d5484fe703
Digest to sign0xc3c42699092fe20c7dbc4205a1027f55c28d33a682a64bd3f04e40f1d28693f5

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):

StepValue
Typehash0x23c5529f34a875b6c0d11a6dfafdd945996dbdc5fbd1bdace2c2e0cf6a253a85
Action struct hash0xa34e7bb8af0d099f9c48095924e53067bcb9b542a816445b96abebbc5646b727
Directive struct hash0x69d560ba5ef3e88459bde7fb8252e55dbc33627373c8f0cfcf8ef0868f59e097
Digest to sign0x5046aba9fb33c55ce736faf63734ea66914f3f168ff0aff2229fa6f31fa9b330

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):

StepValue
Typehash0x10cbcfa56297fc737e4defee7b7937a52978ebbe5920e584317f7f25416836f5
Action struct hash0xfa4ab6440f946a6e279d90972d71223c768c01c8a91dc22703a6448dbcb966d0
Directive struct hash0x998efb01bb9c842eea80e8cc084b9e4e7ec13b2111790e1acbfec32e49060d9d
Digest to sign0x56af82a6aa7fa50907a23c6cfe3aa556af5aa087b1157163ebf6ea0240e8d29d

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):

StepValue
Typehash0x2d3104ab255275839aad254429e4be789f64a8d16b8e3e7d5f30a0db3c887b5c
Action struct hash0x285b3186be219b92d7159419288c0dce9bc4ad7bfc9536e009ebf24bd67f434c
Directive struct hash0xaa03e40b34b15e7e932ae827921930e7538387b596ef1040dbcc791e69021299
Digest to sign0xc46aadc390b66c235af15e199e188ab23fb7f7f1b9237f6c2849c264c28d94da

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):

StepValue
Typehash0x9d4f467a44f4b0d39f234f3b91888546ce31dcbb67ce791550901b9042c38f8b
Orders array hash0x13fc75fcc69bf3b4ef7cdc3a0872c3cc1cf59440c74d366055109b6bd5ea7c98
Directive struct hash0x69d0b1131e386769823fe1ab3facbe8e6fc013f86a117cf95cb52b8ce2f274e0
Digest to sign0x4b5cd34b1f254fdd9881b7674a3412807e16cade1f2ccc7a1e261482682fbd07

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):

StepValue
Typehash0xe13845f0e6c16a5e1fc56b2d11fb0972faa2ec30b01e94d58c72147fa9b6a463
Action struct hash0x0e6e271dd56192d7fb1f37ea2eaffecfbbe2d1f79cac78dce6a6930bb8cc253e
Directive struct hash0xe04b169bbb0e20042308e5aad1036ce60eda75f484a4d6093dc60f2c2231129e
Digest to sign0xb2a43487152a7cecc73d2b6b84d817a58dd02c559b05289a8d8e1b5c3d25e266

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):

StepValue
Typehash0xf21b6881e346bbf4b8d82e8ea507cf487e58dfc08776ad662591be7d6065bdfb
Action struct hash0x0a3ebfe48ef28a79beeee4d56e524f8a5280c38562efb697e7b1d63b3d475afc
Directive struct hash0xae64a13943a6a35b33aeb3095763b855c58c2119daf71e1e3228d8acf2645e8b
Digest to sign0xa01806f47299010b5963a516562be3c3b44fed4f4a9e2e56c4bb601630146cdc

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):

StepValue
Typehash0x8ebe7d19374200e5dc3ae838db74b370da95bd2b3a8aec9de85d2398dd4a9257
Action struct hash0x8b6c4a9db102e9d27c9a4069b39ba35e04948129359f0c15cd5103dd101ffce7
Directive struct hash0x830e291835ad182f290bc3d2bff09eaf9fd33acfbe44c6ad583218cd1a8232e5
Digest to sign0x06589077544af449314db1fa8c7aac2d277cce9e833a04992d88161ede24cbe8

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):

StepValue
Typehash0x71c0e0dfbddc3456015249946354324befde42b6851d1c9b26c6a29f2e0b1ac2
Action struct hash0xed32a441b0ee59c0252f5dff3411766289c9c35c19e3152b0b2b204176c2c7cd
Directive struct hash0xb990a3dfb4e8d96243539f8758b3a55249b217542bb4112f4f88ed67ec516ed9
Digest to sign0x584886d020316085de0e0dec16331b8e990558be3e8fd004c47200abbebed518

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):

StepValue
Typehash0x7feffdf7e7b191d5d27c2a209ca71c789289da73de203e99546abf9632860595
Action struct hash0x9b8c0290da19ea1069e14833023c4621a4f043354b36b4f17d4803b1fce6e656
Directive struct hash0xa0cb8a72188a61289fc2685c29a424605e0a2ee7b7af3e8cfee10e1c75153e08
Digest to sign0x7c7037f27c659a1885f744778480e6847c5d08e98fb22517265ba708b6597d10

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):

StepValue
Typehash0xcf5fddf081e5f2a9a45aa87cb1a3c42e5aa20164566b1ad2369b6b1318eac786
Action struct hash0xef6c70a3c14b9e77c710c6b3b3ee681ca270ef884bd8a64fa4064a96f3dda84e
Directive struct hash0xdda9d0fe82b63c728c4ac42585d5998a04cabaed0f957a2cafdf31a754829c3d
Digest to sign0xf49d7d8d74596e503d6a1bdfb26653533a12a5ba3816acd992d0bd4ef378f59d

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.

MessageTypehashConstraint enforced by the Account
HLOrder0xe6facb816c5f9c321054ef43c7d8fb1ec750f65d630ec39c5c18119a6f4707ddencodedTif 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
HLCancel0x23c5529f34a875b6c0d11a6dfafdd945996dbdc5fbd1bdace2c2e0cf6a253a85None
HLCancelByCloid0x10cbcfa56297fc737e4defee7b7937a52978ebbe5920e584317f7f25416836f5None
HLSendAsset0x7feffdf7e7b191d5d27c2a209ca71c789289da73de203e99546abf9632860595destination must be the Exchange

RSM HLOrder test vector (account 0x1111111111111111111111111111111111111111, nonce 1760000000000, chain 999):

StepValue
Typehash0xe6facb816c5f9c321054ef43c7d8fb1ec750f65d630ec39c5c18119a6f4707dd
Action struct hash0x39b4e0699f382aaad23f04740034e7c5581ba918139d2db63fbf586cc447e7db
Directive struct hash0xe6dde5ad6d310b5ebb83c09ec28ee5568700473b885bb483b7437dca202af4f3
Digest to sign0x57afa1e810804e768c05a467aa88ede15fdceedae5aaa840adff61a613aca469

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>(...).

WrapperInner typeTypehash
SystemCreditTokenCreditToken(uint32 srcDex,uint32 dstDex,uint64 token,uint64 amountWei)0xe845f2049264182802994dae939dc8ac42d3ae543a7eeb530dd021428743f052
SystemCreditOptionCreditOption(bytes32 underlying,uint256 expiry,uint256 strike,bool isCall,uint256 amountWei)0x65aedd5af06882e1e62d752b43b72ec62a85cacf2ca59e8ec2affc57eed9d8ae
SystemStartLiquidationStartLiquidation(int256 equity,uint256 marginNeeded)0x92b6a135a656e489c24fb5511684842b0ab3895c22b51e4910dee90a7d23511e
SystemStopLiquidationStopLiquidation(uint256 startTime)0xe1af98778e9cfd1d1a679bb0b64b6dc8aa3fb152ff6469d0a81a1c7eb87a4bd3
SystemWithdrawTokenWithdrawToken(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. amountWei must be nonzero and the Account must be activated on HyperCore.
  • SystemCreditOption mints option tokens to the Account, deploying the option pair if needed. underlying is an OpenZeppelin ShortString word: ASCII left-aligned with the byte length in the last byte (BTC is 0x42544300...03). Non-canonical encodings revert.
  • SystemStartLiquidation records the auction parameters and locks the Account. marginNeeded must be nonzero.
  • SystemStopLiquidation requires startTime to 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, dstDex of 0 or 4294967295, token = 0 (USDC), and an amountWei that 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):

StepValue
Typehash0xe845f2049264182802994dae939dc8ac42d3ae543a7eeb530dd021428743f052
Action struct hash0xc69d7c6e5c36c9789d990c6316eaff3be43980583806f41f8a8e6461b7114011
Directive struct hash0x7ffa37cef7e08d3a2dd5501a8648899d37bc09ebb25d148c124e12c5047476e1
Digest to sign0x95f75e5ddb4837de3f578b8aa0d714d4b02672b18c4929e3d2dda4a266de8254

The vector credits 5 USDC (srcDex and dstDex of 4294967295, token: 0, amountWei: 500000000).

SystemCreditOption test vector (account 0x1111111111111111111111111111111111111111, nonce 1760000000000, chain 999):

StepValue
Typehash0x65aedd5af06882e1e62d752b43b72ec62a85cacf2ca59e8ec2affc57eed9d8ae
Action struct hash0x4a8b563fc94d26c1da376145d5aa5d05f996232fcf9739ed0a662650e91861b6
Directive struct hash0x190b638f1a462469bf0d070248acfc86bbbb95ea8a9d3d97d33581c7d6bb5554
Digest to sign0x86fb7c53bb7e139a4d5df7db2feae40e634efe09ea7509165d3cece75d75fce2

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):

StepValue
Typehash0x92b6a135a656e489c24fb5511684842b0ab3895c22b51e4910dee90a7d23511e
Action struct hash0x4cd6a7f0997eba84ef82eabd429a0d9ca80e55bd6d89d2e67eda9970d87ec803
Directive struct hash0x84f18dc2577978d7890c9eb269fd787d99de37039b3a08e24745398feffbd0c5
Digest to sign0xcc72c0e7829938c9ca0a4515d759f94cc96d2f24cd51a134a430f215ed5f5d4b

The vector uses equity: -1500000000 and marginNeeded: 20000000000 (both six-decimal USDC).

SystemStopLiquidation test vector (account 0x1111111111111111111111111111111111111111, nonce 1760000000000, chain 999):

StepValue
Typehash0xe1af98778e9cfd1d1a679bb0b64b6dc8aa3fb152ff6469d0a81a1c7eb87a4bd3
Action struct hash0x1d900a795b328bd5fa734bd6d7f74634b98ca66eb5e45322fb1ba6400564b62b
Directive struct hash0x977b9becdd26830f2cdf70ab40f72f24490fe6ed45822d1fc8c84120e9a46f40
Digest to sign0xbe65f82782b14dba42e334e32dd7dcc6bf14c96824523723a186a7381f8f12af

The vector uses startTime: 1760000000.

SystemWithdrawToken test vector (account 0x1111111111111111111111111111111111111111, nonce 1760000000000, chain 999):

StepValue
Typehash0x634b866e9a922bb846e297973c7a456e641b88f4e97493da9b222084d799c883
Action struct hash0x10bd8d4372a09e613a6b4fb7018fcccde857ebf6cc3327d6c9a3c0f81515a076
Directive struct hash0x86680427965513c65a7a170730b9f4fb2e2b40e22cd18338cb004b6a7872ada2
Digest to sign0x65cc392038decb9ccdc23b0a4a1cfbe657d3417f8cd9ebf2319b3172fb7d885c

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 with Account__NonceAlreadyUsed on reuse. There is no nextNonce; 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​

  1. Off-chain: the signer builds the EIP-712 digest (0x1901 || domainSeparator || structHash) and signs it.
  2. 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.
  3. Sequencer: submits Exchange.executeUserDirective(directive, signature). The Exchange checks the Account exists and forwards to Account.executeUserDirective.
  4. 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.
  5. 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 Processor contract and ActionCaster message encoding were removed in Exchange 0.2.0 (February 2026). Account directives now go through Exchange.executeUserDirective and CoreWriter. The deployed Exchange 0.6.1 has none of the old entrypoints.
  • Exchange entrypoints with their own structs. hlRequestOrder, hlRequestCancel, hlRequestCancelByCloid, hlActionSendAsset, hcActionWithdrawToken, hcActionWithdrawOption, rsmCommandRebalance, and rsmCommandRepay are gone. Their replacements:
Removed messageReplacement
HLRequestCancel (batch of HLCancel{asset,oid} plus nonce)HLCancel with one CancelOrderByOid per directive
HLRequestCancelByCloidHLCancelByCloid with one CancelOrderByCloid per directive
HLActionSendAsset (account, nonce, destination, srcDex, dstDex, token, amountWei)HLSendAsset with SendAsset, which adds subAccount
HCActionWithdrawTokenRSM-signed SystemWithdrawToken, executed by the sequencer after the engine approves the withdrawal
HCActionWithdrawOptionOption credits are RSM SystemCreditOption; deposits use depositOption
RsmCommandRebalanceRSM-signed HLOrder (reduce-only IOC)
RsmCommandRepayRSM-signed HLSendAsset to the Exchange
  • Legacy order functions. placeCoreOrders, cancelCoreOrders, and cancelCoreOrdersByCloid used a MsgPack encoding and the CoreSignatures domain ("Exchange", chain ID 1337). They are removed.
  • nextNonce. The on-chain nextNonce[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​

  1. Keys: keep the manager key on a hardware wallet. API wallets only sign orders and cancels; use separate keys per system.
  2. Nonce replay: never reuse a nonce. Track nonces off-chain and increment atomically.
  3. Domain: always sign with chain ID 999 on mainnet (998 on testnet), and check your computed separator against the table above.
  4. Verify what you sign: call POST /v1/actions/{action}/typed-data and compare it with your own encoder before signing with a hardware wallet.
  5. 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.
  6. Manager vs API wallet: managers control ownership, API wallets, and asset movement. API wallets only sign trading messages. Use separate keys.

References​