Skip to main content

Authentication & Signing

EIP-712 signature requirements for write operations.

Overview

All write operations require EIP-712 typed data signatures. The signer must either:

  1. Be the wallet address itself (direct signing), OR
  2. 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 JSON
  • price: Price as string (e.g., "100.0") - MUST match exactly what is sent in JSON
  • tif: 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 address
  • orderId: Order ID as string (e.g., "123")
  • nonce: Unique nonce

CancelOrderByClientId

Struct:

struct CancelOrderByClientId {
address wallet;
string clientId;
uint64 nonce;
}

Field requirements:

  • wallet: Wallet address
  • clientId: Client order ID (string)
  • nonce: Unique nonce

ApproveAgent

Struct:

struct ApproveAgent {
address agent;
uint64 nonce;
}

Field requirements:

  • agent: Agent wallet address to authorize
  • nonce: 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 revoke
  • nonce: 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 address
  • currency: Underlying currency (e.g., "BTC", "ETH")
  • intervalMs: Rolling window length in milliseconds
  • frozenTimeMs: 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+currency
  • nonce: Unique nonce

DeleteMmpConfig

Struct:

struct DeleteMmpConfig {
address wallet;
string currency;
uint64 nonce;
}

Field requirements:

  • wallet: Wallet address
  • currency: Underlying currency to delete config for
  • nonce: Unique nonce

ResetMmp

Struct:

struct ResetMmp {
address wallet;
string currency;
uint64 nonce;
}

Field requirements:

  • wallet: Wallet address
  • currency: Underlying currency to reset MMP state for
  • nonce: 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:

  1. Approve agent (one-time):

    POST /approve-agent
    {
    "agent": "0x...",
    "nonce": 1,
    "signature": "0x..." # Signed by wallet owner
    }
  2. Sign orders with agent wallet:

    • Use agent wallet to sign PlaceOrder / CancelOrder messages
    • Set wallet field to the trading wallet address
    • Middleware verifies agent is authorized for that wallet

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:

  1. price or size sent as number instead of string
  2. String formatting changed between signing and sending (e.g., "100.0" vs "100")
  3. Wrong nonce
  4. Wrong domain (chain ID, name, version)
  5. 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:

  1. Generate signature with your implementation
  2. Send test order via POST /order
  3. Verify response: status="ACKED" or status="REJECTED" with reason
  4. If "signature_verification_failed", check:
    • String formatting of price and size
    • Domain parameters (especially chainId)
    • Nonce correctness

Security Considerations

  1. Never expose private keys: Use hardware wallets or secure key management
  2. Nonce management: Use persistent, incrementing nonces per wallet
  3. Agent authorization: Regularly audit authorized agents via GET /authorized-agents
  4. String encoding: Ensure price and size are strings in both signing and request

References