Technical overview
L1 actions use a phantom agent construction - a temporary signing identity created from your action’s hash. This provides privacy by hashing the actual data before signing.
The Hyperliquid Python SDK v0.18.0+ handles phantom agent construction internally. You don’t need to implement this complexity yourself.
Key characteristics
| Property | Value |
|---|
| Chain ID | 1337 (NOT Arbitrum’s 42161) |
| Domain name | ”Exchange” |
| Version | ”1” |
| Serialization | Msgpack binary format |
| Agent type | Phantom agent from action hash |
| Source | ”a” for mainnet, “b” for testnet |
What is a phantom agent?
A phantom agent is a cryptographic construct that provides privacy for trading operations:
Serialize action
Action is serialized using msgpack binary format
Append metadata
Nonce and vault address (if applicable) are appended
Hash the data
Complete data is hashed with keccak256
Create agent object
Temporary “agent” object created with hash as connectionId
Sign via EIP-712
This phantom agent is signed using EIP-712 typed data
Supported L1 actions
All L1 actions use the same signing method sign_l1_action() - only the action payload changes:
Trading actions
order — Place new orders
cancel — Cancel orders by order ID
cancelByCloid — Cancel orders by client order ID
modify — Modify existing orders
batchModify — Modify multiple orders
Position management
updateLeverage — Adjust leverage for positions
updateIsolatedMargin — Manage isolated margin
Transfers
vaultTransfer — Transfer between vault accounts
subAccountTransfer — Transfer between sub-accounts
Utility
scheduleCancel — Schedule order cancellation
noop — No operation (for testing)
Complete implementation example
#!/usr/bin/env python3
"""
L1 Action Signing Example - Demonstrates phantom agent construction
SDK v0.18.0+
"""
from hyperliquid.exchange import Exchange
from hyperliquid.utils import constants
from hyperliquid.utils.signing import sign_l1_action, get_timestamp_ms
from eth_account import Account
import json
def main():
# Load configuration
with open('config.json') as f:
config = json.load(f)
wallet = Account.from_key(config['private_key'])
print(f"Account: {wallet.address}")
# Initialize exchange for sending signed actions
exchange = Exchange(
wallet=wallet,
base_url=constants.MAINNET_API_URL
)
# Example 1: Place an order (SDK handles signing internally)
order_result = exchange.order(
coin="BTC",
is_buy=True,
sz=0.01,
limit_px=50000,
order_type={"limit": {"tif": "Gtc"}},
reduce_only=False
)
print(f"Order result: {order_result}")
# Example 2: Manual signing for understanding
timestamp = get_timestamp_ms()
# Create a minimal L1 action
action = {"type": "noop"}
# Sign with phantom agent construction
signature = sign_l1_action(
wallet,
action,
None, # vault_address
timestamp,
None, # expires_after
True # is_mainnet
)
print(f"\nPhantom agent signature generated:")
print(f" r: {signature['r']}")
print(f" s: {signature['s']}")
print(f" v: {signature['v']}")
# Send the signed action to Hyperliquid
result = exchange._post_action(action, signature, timestamp)
print(f"\nResult: {result}")
if __name__ == "__main__":
main()
Action payload examples
Place order
action = {
"type": "order",
"orders": [{
"a": 0, # Asset index (0 = BTC)
"b": True, # Buy = True, Sell = False
"p": "50000", # Price
"s": "0.01", # Size
"r": False, # Reduce-only
"t": {"limit": {"tif": "Gtc"}} # Order type
}],
"grouping": "na"
}
Cancel order
action = {
"type": "cancel",
"cancels": [{
"a": 0, # Asset index
"o": 123456 # Order ID
}]
}
Modify order
action = {
"type": "batchModify",
"modifies": [{
"oid": 123456, # Order ID
"order": {
"a": 0,
"b": True,
"p": "51000", # New price
"s": "0.01",
"r": False,
"t": {"limit": {"tif": "Gtc"}}
}
}]
}
Update leverage
action = {
"type": "updateLeverage",
"asset": 0,
"isCross": True,
"leverage": 10
}
Technical implementation details
Phantom agent construction
def construct_phantom_agent(hash, is_mainnet):
return {
"source": "a" if is_mainnet else "b",
"connectionId": hash
}
EIP-712 domain
domain = {
"name": "Exchange",
"version": "1",
"chainId": 1337,
"verifyingContract": "0x0000000000000000000000000000000000000000"
}
Type definition
types = {
"Agent": [
{"name": "source", "type": "string"},
{"name": "connectionId", "type": "bytes32"}
]
}
TypeScript implementation
import { ethers } from 'ethers';
import msgpack from 'msgpack-lite';
import { keccak256 } from 'ethers/lib/utils';
async function signL1Action(
privateKey: string,
action: any,
vaultAddress: string | null,
nonce: number,
isMainnet: boolean
): Promise<{ r: string; s: string; v: number }> {
// Serialize action with msgpack
const actionBytes = msgpack.encode(action);
// Append vault address and nonce
const data = Buffer.concat([
actionBytes,
vaultAddress ? Buffer.from(vaultAddress.slice(2), 'hex') : Buffer.alloc(20),
Buffer.from(nonce.toString(16).padStart(16, '0'), 'hex')
]);
// Hash to create connectionId
const hash = keccak256(data);
// EIP-712 domain
const domain = {
name: 'Exchange',
version: '1',
chainId: 1337, // L1 actions always use 1337
verifyingContract: '0x0000000000000000000000000000000000000000'
};
const types = {
Agent: [
{ name: 'source', type: 'string' },
{ name: 'connectionId', type: 'bytes32' }
]
};
const value = {
source: isMainnet ? 'a' : 'b',
connectionId: hash
};
const wallet = new ethers.Wallet(privateKey);
const signature = await wallet._signTypedData(domain, types, value);
return ethers.utils.splitSignature(signature);
}
Configuration file
Create a config.json file:
{
"private_key": "0x...",
"account_address": "0x..."
}
Common errors and solutions
Wrong chain ID
Never use Arbitrum’s chain ID (42161) for L1 actions. Always use chain ID 1337.
# CORRECT
domain = {"chainId": 1337, ...}
# INCORRECT
domain = {"chainId": 42161, ...} # This will fail!
Ensure action payloads match the expected structure:
# CORRECT - proper structure
action = {
"type": "order",
"orders": [...],
"grouping": "na"
}
# INCORRECT - missing required fields
action = {
"type": "order",
"orders": [...]
# Missing "grouping" field
}
Nonce issues
Always use current timestamp in milliseconds:
from hyperliquid.utils.signing import get_timestamp_ms
# CORRECT
nonce = get_timestamp_ms()
# Also correct
import time
nonce = int(time.time() * 1000)
# INCORRECT
nonce = int(time.time()) # Missing milliseconds
Testing your implementation
Start with noop
Test with a noop action first - it validates signing without any side effects
Verify signatures
Check that signatures have proper r, s, v components
Monitor responses
Successful actions return {"status": "ok"} or order details
Best practices
DO
- Use the official SDK when possible
- Keep private keys secure (use environment variables)
- Test thoroughly on testnet first
- Handle errors gracefully
- Use proper nonce management
DON’T
- Hardcode private keys in code
- Mix up chain IDs (1337 vs 0x66eee)
- Reuse nonces across requests
- Skip error handling
- Use outdated SDK versions
Summary
L1 actions use phantom agent construction for privacy - the actual data is hashed before signing. The SDK’s sign_l1_action handles all complexity internally, making it easy to implement trading operations securely.