Hyperliquid Paper Trading API: Complete Setup Guide
Walk-through of Hyperliquid's testnet API for paper trading perps, from key generation to order routing and P&L attribution. Includes a Python scaffold and the gotchas that differ from mainnet behaviour.
The Hyperliquid paper trading API is the testnet environment Hyperliquid exposes at api.hyperliquid-testnet.xyz — a near-identical replica of mainnet with simulated balances, real order-book mechanics and genuine WebSocket feeds. Before you risk a dollar running a Hyperliquid perps bot in production, you want your strategy logic battle-tested here. This guide walks through the full setup: key generation, authentication quirks, order routing, fill simulation differences and P&L attribution. Assume you're building in Python; the concepts translate directly to TypeScript or Rust.
Environment Setup and Key Generation
Paper trading uses the same EIP-712 signing scheme as mainnet, so every authenticated request is a typed-data signature over a structured payload. You'll need:
- A testnet wallet — generate a fresh key with
eth_accountor any EVM wallet. Never reuse a mainnet key on testnet; keep your surfaces clean. - An API wallet (optional but recommended) — Hyperliquid supports a sub-key delegation pattern. You register an API wallet address against your main testnet address via a signed
approveAgentaction. The API wallet can then sign orders independently without you ever exposing the main key to the bot process. - Testnet USDC — request from the faucet endpoint
POST /exchangewith action type"usdSend"from the deployer address, or use the testnet UI atapp.hyperliquid-testnet.xyz.
from eth_account import Account
from eth_account.messages import encode_typed_data
TESTNET_URL = "https://api.hyperliquid-testnet.xyz"
wallet = Account.from_key("0xYOUR_TESTNET_PRIVATE_KEY")
Keep the private key in an environment variable, not hardcoded. If you later wire this up into a running bot, a secrets manager is the right answer — not a .env file committed to the repo.
Authentication and the EIP-712 Signing Flow
Every mutating action — placing an order, cancelling, setting leverage — goes to POST /exchange as a JSON body:
{
"action": { ... },
"nonce": 1719000000000,
"signature": { "r": "0x...", "s": "0x...", "v": 28 }
}
The nonce must be a millisecond-precision Unix timestamp that is strictly greater than the last nonce Hyperliquid accepted for that wallet. Use int(time.time() * 1000) and never cache — if two requests land in the same millisecond the second will be rejected with a nonce error. This bites hard in fast loops.
The EIP-712 domain for testnet differs from mainnet only in chainId (421614 for testnet, 42161 for mainnet Arbitrum). If you hard-code the domain object in your signing helper and forget to switch it, your signatures will be silently invalid — you'll get an authentication error that looks exactly like a bad key.
import time, json, requests
from eth_account.structured_data import hash_domain, hash_message
def sign_action(wallet, action: dict) -> dict:
nonce = int(time.time() * 1000)
connection_id = b'\x00' * 32 # mainnet vault: use actual connection id
payload = {
"source": "a", # "a" = agent/API wallet; "b" = direct
"connectionId": connection_id.hex(),
}
# ... full EIP-712 construction here
return {"action": action, "nonce": nonce, "signature": sig}
The Hyperliquid Python SDK (hyperliquid-python-sdk on PyPI) handles all of this correctly and is worth using as a reference even if you write your own — the exchange.py module shows exactly which typed-data types map to which actions.
Placing and Cancelling Orders
Orders go through the "order" action type. A limit buy on ETH-USDC perp at 3,000 with 0.01 ETH size:
order_action = {
"type": "order",
"orders": [{
"a": 3, # asset index — ETH is index 3 on mainnet & testnet
"b": True, # True = buy
"p": "3000.0", # price as string
"s": "0.01", # size as string
"r": False, # reduce-only
"t": {"limit": {"tif": "Gtc"}}
}],
"grouping": "na"
}
Asset indices are not names — ETH is 3, BTC is 0, ARB is 2, and so on. The full current mapping comes from GET /info with {"type": "meta"}. Cache this at startup; it rarely changes but can when new markets list.
tif (time-in-force) options: "Gtc" (good-till-cancel), "Ioc" (immediate-or-cancel), "Alo" (add-liquidity-only / post-only). IOC on testnet behaves identically to mainnet — it either fills immediately against the book or cancels. Use "Alo" when you're testing a market-making strategy and want to ensure you never cross the spread.
Cancellations reference the oid (order ID) returned in the order response, not a client ID. Store every live oid in a local dictionary keyed by (asset, cloid) if you assign client order IDs.
WebSocket Feeds and Fill Events
Paper trading supports the same WebSocket endpoint as mainnet at wss://api.hyperliquid-testnet.xyz/ws. Subscribe to fills and order updates with:
subscribe_msg = {
"method": "subscribe",
"subscription": {
"type": "userFills",
"user": wallet.address
}
}
Fill events arrive as {"channel": "userFills", "data": [...]}. Each fill includes: coin, side, px (fill price), sz (fill size), fee, oid, tid (trade ID), and startPosition. The startPosition field is critical for P&L attribution — it tells you the position size before this fill landed, so you can compute realised P&L incrementally without holding full position state in memory.
One testnet-specific gotcha: fills on testnet can arrive out of order relative to the REST acknowledgement. On mainnet the latency is low enough that the WebSocket fill almost always arrives after your REST POST response. On testnet, especially under load, the fill WebSocket message sometimes precedes the REST response. Build your fill handler to be idempotent on tid and never assume ordering.
P&L Attribution and the Testnet/Mainnet Gap
Hyperliquid reports P&L in two ways: unrealized (mark price minus entry, times position size) and realized (booked on full or partial close). Query positions with POST /info and {"type": "clearinghouseState", "user": "0x..."} — the response includes each position's entryPx, positionValue, unrealizedPnl and returnOnEquity.
For accurate attribution of a multi-signal strategy, build your own ledger on top of fills rather than relying solely on the exchange's PnL numbers. The exchange figure is per-position; if you're running several strategies on the same asset you need to split fills by strategy tag using client order IDs and reconcile separately.
The critical mainnet gap for paper testing: testnet uses a simulated orderbook that matches your orders against a synthetic best-bid/ask rather than live liquidity. For large sizes the fill price may be better than what you'd get on mainnet because there's no real depth to absorb. If your strategy is sensitive to fill quality — market-making, liquidation hunting — add a simulated slippage layer in your backtest harness that degrades fill prices by 1–3 bps for realistic sizing. See our Hyperliquid market-making bot page for how we handle this in production.
Going to Mainnet
The transition checklist is short but each item has caused real losses for real operators:
- Switch
TESTNET_URLtohttps://api.hyperliquid.xyzand update the EIP-712chainIdfrom421614to42161. - Re-register your API wallet — testnet agent approvals do not carry over.
- Validate asset indices again — the mapping is the same but verify with a fresh
/infocall; never assume. - Set position limits before the first order — use the
updateLeverageand hard exposure caps in your risk layer, not after the first fill. - Test the kill-switch manually on mainnet with a 1-dollar position before running at size. The logic that works on testnet is valid, but network conditions differ.
Keep your testnet environment running in parallel for the first week on mainnet. Run the same strategy on both and diff the decisions — any divergence reveals a logic bug or a data-feed difference you missed.
If you want a production-grade Hyperliquid perps bot that already handles all of the above — signing, WebSocket reconnection, P&L ledgering, kill-switches and testnet-to-mainnet parity — get in touch and we can scope it out.
Need a bot like this built?
We design, build and run trading bots on Solana, Hyperliquid and Polymarket.
Start a projectMore from the blog
Public vs Paid RPC: How to Run a Fair Benchmark
A fair comparison of public and paid RPC endpoints must account for quotas, workload, freshness and support guarantees.
Read articleRPC Benchmark Percentiles Explained: p50, p95 and p99
Why averages hide the latency spikes that break trading bots, wallets and production blockchain applications.
Read articleHyperliquid REST vs WebSocket Benchmark: What to Measure
A benchmark plan for Hyperliquid market data that separates request latency from streaming freshness and recovery.
Read article