Authentication & Signing
EIP-712 signature requirements for write operations.
Overview
All write operations require EIP-712 typed data signatures. The signer must either:
- Be the wallet address itself (direct signing), OR
- Be an authorized agent for the wallet (see Agent auth)
EIP-712 Domain
Domain separator:
{
"name": "Hypertheta",
"version": "1",
"chainId": 998,
"verifyingContract": "0x0000000000000000000000000000000000000000"
}
Chain ID: 998 (Hyperliquid testnet)
Note: Mainnet chain ID TBD.
Message Types
PlaceOrder
Struct:
struct PlaceOrder {
address wallet;
string symbol;
string side;
string size;
string price;
string tif;
string clientId;
uint64 nonce;
}
Field requirements:
wallet: Wallet address (0x-prefixed hex string)symbol: Option symbol (e.g.,"BTC-20250131-100000-C")side:"Buy"or"Sell"(exact string match required)size: Size as string (e.g.,"0.1") - MUST match exactly what is sent in JSONprice: Price as string (e.g.,"100.0") - MUST match exactly what is sent in JSONtif: Time-in-force:"gtc","ioc", or"fok"(exact string match required)clientId: Client-provided order ID (string, can be empty"")nonce: Unique nonce (uint64) for replay protection
Critical: price and size MUST be strings in both the signed message and the JSON request body. String formatting must match exactly.
CancelOrder
Struct:
struct CancelOrder {
address wallet;
string orderId;
uint64 nonce;
}
Field requirements:
wallet: Wallet addressorderId: Order ID as string (e.g.,"123")nonce: Unique nonce
CancelOrderByClientId
Struct:
struct CancelOrderByClientId {
address wallet;
string clientId;
uint64 nonce;
}
Field requirements:
wallet: Wallet addressclientId: Client order ID (string)nonce: Unique nonce
ApproveAgent
Struct:
struct ApproveAgent {
address agent;
uint64 nonce;
}
Field requirements:
agent: Agent wallet address to authorizenonce: Unique nonce
Note: The wallet authorizing the agent is derived from the recovered signature.
RevokeAgent
Struct:
struct RevokeAgent {
address agent;
uint64 nonce;
}
Field requirements:
agent: Agent wallet address to revokenonce: Unique nonce
SetMmpConfig
Struct:
struct SetMmpConfig {
address wallet;
string currency;
uint64 intervalMs;
uint64 frozenTimeMs;
string qtyLimit;
string deltaLimit;
string vegaLimit;
bool enabled;
uint64 nonce;
}
Field requirements:
wallet: Wallet addresscurrency: Underlying currency (e.g.,"BTC","ETH")intervalMs: Rolling window length in millisecondsfrozenTimeMs: Freeze duration after trigger (milliseconds)qtyLimit: Optional quantity limit as string (e.g.,"1000000")deltaLimit: Optional delta limit as string (e.g.,"10.0")vegaLimit: Optional vega limit as string (e.g.,"5.0")enabled: Whether MMP is enabled for this wallet+currencynonce: Unique nonce
DeleteMmpConfig
Struct:
struct DeleteMmpConfig {
address wallet;
string currency;
uint64 nonce;
}
Field requirements:
wallet: Wallet addresscurrency: Underlying currency to delete config fornonce: Unique nonce
ResetMmp
Struct:
struct ResetMmp {
address wallet;
string currency;
uint64 nonce;
}
Field requirements:
wallet: Wallet addresscurrency: Underlying currency to reset MMP state fornonce: Unique nonce
Note: Resets MMP fill-window state (clears cumulative metrics, unfreezes currency).
Signing Process
Step 1: Construct Message
Example for PlaceOrder:
const message = {
wallet: "0x1111111111111111111111111111111111111111",
symbol: "BTC-20250131-100000-C",
side: "Buy",
size: "0.1", // MUST be string
price: "100.0", // MUST be string
tif: "gtc",
clientId: "mm-1",
nonce: 123
};
Step 2: Define Domain
const domain = {
name: "Hypertheta",
version: "1",
chainId: 998, // Hyperliquid testnet
verifyingContract: "0x0000000000000000000000000000000000000000"
};
Step 3: Sign Typed Data
Using ethers.js:
const signature = await signer._signTypedData(domain, {
PlaceOrder: [
{ name: "wallet", type: "address" },
{ name: "symbol", type: "string" },
{ name: "side", type: "string" },
{ name: "size", type: "string" },
{ name: "price", type: "string" },
{ name: "tif", type: "string" },
{ name: "clientId", type: "string" },
{ name: "nonce", type: "uint64" }
]
}, message);
Step 4: Send Request
Critical: The JSON request body must use the exact same string values for price and size:
{
"wallet": "0x1111111111111111111111111111111111111111",
"symbol": "BTC-20250131-100000-C",
"price": "100.0", // Same string as signed
"size": "0.1", // Same string as signed
"side": "Buy",
"tif": "gtc",
"client_id": "mm-1",
"nonce": 123,
"signature": "0x..."
}
Nonce Management
Requirements:
- Nonces MUST be unique per wallet
- Nonces SHOULD be incrementing (prevents replay attacks)
- Nonces are not validated for strict monotonicity (gaps allowed)
Best practices:
- Use a persistent counter per wallet
- Increment after each successful signature
- Handle nonce gaps gracefully (e.g., if a request fails, retry with same nonce)
Agent Authorization
If using an agent wallet to sign:
-
Approve agent (one-time):
POST /approve-agent
{
"agent": "0x...",
"nonce": 1,
"signature": "0x..." # Signed by wallet owner
} -
Sign orders with agent wallet:
- Use agent wallet to sign
PlaceOrder/CancelOrdermessages - Set
walletfield to the trading wallet address - Middleware verifies agent is authorized for that wallet
- Use agent wallet to sign
See Agent auth for full agent authorization details.
Perp Orders (Hyperliquid-Compatible)
Perp orders use a different EIP-712 domain and message format (Hyperliquid Core compatibility):
Domain:
{
"name": "Exchange",
"version": "1",
"chainId": 1337,
"verifyingContract": "0x0000000000000000000000000000000000000000"
}
Message: Agent struct with MessagePack-encoded order data.
See the current signature encoding guide for exact fields.
Common Errors
"Signature verification failed"
Causes:
priceorsizesent as number instead of string- String formatting changed between signing and sending (e.g.,
"100.0"vs"100") - Wrong nonce
- Wrong domain (chain ID, name, version)
- Agent not authorized for wallet
"Unauthorized: signer not authorized for wallet"
Cause: Signer is not the wallet itself and is not an authorized agent.
Solution: Approve agent via POST /approve-agent or sign with wallet directly.
Implementation Examples
Python (eth_account)
from eth_account import Account
from eth_account.messages import encode_structured_data
domain = {
"name": "Hypertheta",
"version": "1",
"chainId": 998,
"verifyingContract": "0x0000000000000000000000000000000000000000"
}
message = {
"wallet": "0x1111111111111111111111111111111111111111",
"symbol": "BTC-20250131-100000-C",
"side": "Buy",
"size": "0.1",
"price": "100.0",
"tif": "gtc",
"clientId": "mm-1",
"nonce": 123
}
types = {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"}
],
"PlaceOrder": [
{"name": "wallet", "type": "address"},
{"name": "symbol", "type": "string"},
{"name": "side", "type": "string"},
{"name": "size", "type": "string"},
{"name": "price", "type": "string"},
{"name": "tif", "type": "string"},
{"name": "clientId", "type": "string"},
{"name": "nonce", "type": "uint64"}
]
}
structured_msg = {
"types": types,
"domain": domain,
"primaryType": "PlaceOrder",
"message": message
}
encoded = encode_structured_data(structured_msg)
signed = Account.sign_message(encoded, private_key)
signature = signed.signature.hex()
JavaScript (ethers.js)
const { ethers } = require("ethers");
const domain = {
name: "Hypertheta",
version: "1",
chainId: 998,
verifyingContract: "0x0000000000000000000000000000000000000000"
};
const types = {
PlaceOrder: [
{ name: "wallet", type: "address" },
{ name: "symbol", type: "string" },
{ name: "side", type: "string" },
{ name: "size", type: "string" },
{ name: "price", type: "string" },
{ name: "tif", type: "string" },
{ name: "clientId", type: "string" },
{ name: "nonce", type: "uint64" }
]
};
const message = {
wallet: "0x1111111111111111111111111111111111111111",
symbol: "BTC-20250131-100000-C",
side: "Buy",
size: "0.1",
price: "100.0",
tif: "gtc",
clientId: "mm-1",
nonce: 123
};
const signature = await signer._signTypedData(domain, types, message);
Testing Signatures
Example for testing signature generation:
- Generate signature with your implementation
- Send test order via
POST /order - Verify response:
status="ACKED"orstatus="REJECTED"with reason - If
"signature_verification_failed", check:- String formatting of
priceandsize - Domain parameters (especially
chainId) - Nonce correctness
- String formatting of
Security Considerations
- Never expose private keys: Use hardware wallets or secure key management
- Nonce management: Use persistent, incrementing nonces per wallet
- Agent authorization: Regularly audit authorized agents via
GET /authorized-agents - String encoding: Ensure
priceandsizeare strings in both signing and request
References
- EIP-712: https://eips.ethereum.org/EIPS/eip-712
- Implementation: handled by the signature recovery and middleware stack.