
The Gap Between a Backtest and a Live Bot Is Mostly Plumbing: Building a Hyperliquid Trading Bot
The alpha is maybe 20% of a live bot. The other 80% is plumbing: authentication that does not leak keys, order handling that survives partial fills, a feed that stays fresh, and rate-limit discipline. A hands-on build guide against Hyperliquid's first-party Python SDK — read state, authenticate with an agent wallet, place and cancel orders, subscribe to a realtime feed, and contain the operational risks that actually break bots.

The gap between a backtest and a live bot is mostly plumbing
Every quant who has moved a strategy from research to production knows the uncomfortable truth: the alpha is maybe 20% of the work. The other 80% is authentication that does not leak keys, order plumbing that survives partial fills, a data feed that does not silently go stale, and rate-limit discipline that keeps you from being throttled at the worst possible moment. Hyperliquid is unusually friendly territory for this, because it ships a first-party, MIT-licensed Python SDK that wraps the same REST and WebSocket API the exchange itself uses. This guide walks through building a live trading bot against that API the way you would actually run it: read market state, authenticate with an API wallet, place and cancel orders, subscribe to a realtime feed, and handle the failure modes that break bots in production.
This is a technical build guide for developers running automated strategies. It assumes you are comfortable with Python, private-key handling, and perpetual-futures mechanics. Test everything on testnet before you route a single dollar of size through it.
Key points
The official hyperliquid-python-sdk (
pip install hyperliquid-python-sdk) exposes two core classes:Infofor read-only data andExchangefor signed actions.Sign with an API wallet (also called an agent wallet), generated at
app.hyperliquid.xyz/API— never with your master account's private key.A critical gotcha: you sign with the API wallet's key but query with the master account's public address. Using the agent address to query returns empty data.
REST is rate-limited to an aggregated weight of 1200 per minute per IP, plus an address-based limit of roughly 1 request per 1 USDC traded. Use WebSockets for low-latency realtime data.
The biggest operational risks are not strategy risk — they are key management, silent data staleness, and unhandled partial fills.
Should you use the REST API, WebSocket, or both?
Hyperliquid's API has two transports and you will almost certainly use both. The REST interface at https://api.hyperliquid.xyz/info and .../exchange is request/response: you POST a JSON body describing what you want. The info endpoint serves market and account state; the exchange endpoint accepts signed actions such as placing and cancelling orders. The WebSocket at wss://api.hyperliquid.xyz/ws is push-based — you subscribe once and the server streams updates.
The practical division of labor is straightforward. Use REST for anything you request on demand: startup snapshots, account state before sizing a position, and every order or cancel (all writes go through REST-style signed actions). Use WebSocket for anything you want continuously and quickly: the order book, trades, mids, and your own order/fill updates. Polling the book over REST is both slower and a fast way to burn through your rate budget. The SDK folds both transports behind the same Info object, so the switch is a single constructor flag.

How do you set up the SDK and read market state?
Install the package and start with a read-only client. The Info class needs no key at all, which makes it the right place to sanity-check connectivity before you touch signing. Pass skip_ws=True when you only want request/response calls.
pip install hyperliquid-python-sdkfrom hyperliquid.info import Info
from hyperliquid.utils import constants
# Read-only: no key required
info = Info(constants.MAINNET_API_URL, skip_ws=True)
# All mid prices, keyed by coin
mids = info.all_mids()
print("BTC mid:", mids["BTC"])
# Perp universe + context (funding, open interest, mark price, ...)
meta, asset_ctxs = info.meta_and_asset_ctxs()
# Full account state for any address (public address, not the agent)
user_state = info.user_state("0xcd5051944f780a621ee62e39e493c489668acf4d")
print("account value:", user_state["marginSummary"]["accountValue"])Note the two constants: constants.TESTNET_API_URL and constants.MAINNET_API_URL. The SDK's own examples default to testnet, and you should too until the bot is proven. Every method here is documented in the SDK's hyperliquid/info.py and mirrored in the examples/ directory of the repository.
How do you authenticate without leaking your keys?
This is the section to read twice. Hyperliquid separates your master account — the wallet that holds funds — from an API wallet (the docs also call it an agent wallet) that is authorized only to sign actions. You generate and approve an API wallet at https://app.hyperliquid.xyz/API. The API wallet can place and cancel orders on the master account's behalf, but it never holds custody and can be deregistered without moving funds. This is exactly the separation you want for an unattended bot: if the API key is compromised, the blast radius is trading actions, not withdrawal of your balance.
Two rules the official docs are explicit about:
Set the master account's public address as
account_address. A common pitfall is passing the agent wallet's address, which leads to empty query results.Treat API wallets as disposable. Once an agent is deregistered, its nonce state may be pruned, which can allow previously signed actions to be replayed. The docs strongly recommend generating a fresh agent wallet rather than reusing an address.
Never hardcode the secret. Load it from an environment variable (or a keystore) so the key never lands in source control or logs:
import os
import eth_account
from hyperliquid.exchange import Exchange
from hyperliquid.info import Info
from hyperliquid.utils import constants
# Secret lives in the environment, never in code
secret_key = os.environ["HL_API_SECRET_KEY"] # API wallet private key
account_address = os.environ["HL_ACCOUNT_ADDRESS"] # MASTER account public address
account = eth_account.Account.from_key(secret_key)
info = Info(constants.MAINNET_API_URL, skip_ws=True)
exchange = Exchange(
account,
constants.MAINNET_API_URL,
account_address=account_address, # query/target the master, sign with the agent
)The Exchange object now signs every action with the agent key while attributing it to your master account. Under the hood the SDK handles the EIP-712 signing and nonce management, so you rarely touch either directly — but it helps to know that nonces are tracked per signer, and that if you run multiple processes you should give each its own API wallet to avoid nonce collisions.
How do you place, check, and cancel an order?
The core method is exchange.order(name, is_buy, sz, limit_px, order_type). The following places a resting limit buy far below market so it will not fill, inspects the response, queries its status, then cancels it — the canonical round-trip from the SDK's basic_order.py example.
coin = "ETH"
# Resting GTC limit buy, 0.2 ETH at $1100 (well below market -> rests)
order_result = exchange.order(
coin, True, 0.2, 1100, {"limit": {"tif": "Gtc"}}
)
print(order_result)
if order_result["status"] == "ok":
status = order_result["response"]["data"]["statuses"][0]
if "resting" in status:
oid = status["resting"]["oid"]
# Confirm it is live on the book
print(info.query_order_by_oid(account_address, oid))
# Cancel by order id
print(exchange.cancel(coin, oid))A few things a veteran will want nailed down. The tif (time-in-force) field takes "Gtc" (good-til-cancelled, rests), "Ioc" (immediate-or-cancel), or "Alo" (add-liquidity-only / post-only, which the validators prioritize in ALO-only batches). The response is a nested structure: always drill into response.data.statuses and branch on the actual outcome — a status can come back as resting, filled, or error, and assuming success is how bots leak size.
For market orders the SDK provides market_open and market_close, which convert your intent into an aggressive IOC order bounded by a slippage tolerance:
# Market buy 0.05 ETH, price=None (use book), 1% max slippage
order_result = exchange.market_open("ETH", True, 0.05, None, 0.01)
for status in order_result["response"]["data"]["statuses"]:
if "filled" in status:
f = status["filled"]
print(f"filled {f['totalSz']} @ {f['avgPx']} (oid {f['oid']})")
elif "error" in status:
print("order error:", status["error"])
# Flatten the position later
exchange.market_close("ETH")That explicit slippage parameter is not decoration. On a thin book or during a volatility spike, a market order without a slippage bound is how you print a fill far from where you expected. If your strategy layers in advanced order types, the same order call carries trigger and TWAP variants — covered separately in advanced order types: TWAP, trailing, and conditional orders.
How do you subscribe to a realtime feed?
For anything latency-sensitive, drop the skip_ws flag and subscribe. The SDK's Info.subscribe(subscription, callback) registers a handler that fires on every message. You can subscribe to public feeds (order book, trades, mids, candles) and user-specific feeds (your fills, order updates) on the same connection.
info = Info(constants.MAINNET_API_URL) # WS enabled
def on_book(msg):
# L2 order book snapshot/update for ETH
levels = msg["data"]["levels"]
best_bid = levels[0][0]["px"]
best_ask = levels[1][0]["px"]
print("bid/ask:", best_bid, best_ask)
def on_fill(msg):
for fill in msg["data"]["fills"]:
print("FILL", fill["coin"], fill["sz"], "@", fill["px"])
info.subscribe({"type": "l2Book", "coin": "ETH"}, on_book)
info.subscribe({"type": "userFills", "user": account_address}, on_fill)Available subscription types include allMids, l2Book, trades, candle, bbo, userEvents, userFills, and orderUpdates, among others. The limits are per IP: up to 10 WebSocket connections, 1000 subscriptions, and 10 unique users across user-specific subscriptions. Some feeds only push on change, so do not treat silence as an error — treat it as "nothing happened." The same feeds are the backbone of on-chain monitoring strategies; for a data-only angle see tracking whale movements on-chain.
What does a minimal bot skeleton look like?
Putting it together: a bot is a loop (or an event handler) that reads state, decides, acts, and — crucially — handles errors on every network call. Below is a deliberately simple skeleton that checks a mid price and places one bounded order, wrapping each API call in error handling. It is a structural template, not a strategy.
import os
import time
import eth_account
from hyperliquid.exchange import Exchange
from hyperliquid.info import Info
from hyperliquid.utils import constants
def build_clients():
account = eth_account.Account.from_key(os.environ["HL_API_SECRET_KEY"])
addr = os.environ["HL_ACCOUNT_ADDRESS"]
info = Info(constants.TESTNET_API_URL, skip_ws=True) # start on TESTNET
exch = Exchange(account, constants.TESTNET_API_URL, account_address=addr)
return addr, info, exch
def run():
addr, info, exch = build_clients()
coin, target = "ETH", 1500.0
while True:
try:
mid = float(info.all_mids()[coin])
except Exception as e: # network / parse failure -> skip tick
print("data error, backing off:", e)
time.sleep(5)
continue
if mid <= target:
try:
res = exch.order(coin, True, 0.02, target, {"limit": {"tif": "Gtc"}})
statuses = res.get("response", {}).get("data", {}).get("statuses", [])
for s in statuses:
if "error" in s:
print("rejected:", s["error"]) # e.g. min size, insufficient margin
elif "resting" in s:
print("resting oid:", s["resting"]["oid"])
elif "filled" in s:
print("filled:", s["filled"])
except Exception as e:
print("order submit failed:", e) # DO NOT blindly retry
time.sleep(2) # respect rate limits; never hammer the endpoint
if __name__ == "__main__":
run()The shape matters more than the logic. Every external call is wrapped. A data failure skips the tick instead of crashing. A rejected order is inspected, not assumed away. And there is a deliberate sleep so the loop cannot spin into the rate limiter. Swap the if mid <= target block for real signals and you have the frame of a production bot. If your strategy is a symmetric grid rather than a single trigger, the same skeleton extends directly into the design in building a grid trading strategy on Hyperliquid.
Which mistakes break bots in production?
Querying with the agent address. The single most common SDK confusion: sign with the API wallet, but pass the master public address to
Infoqueries and asaccount_address. The agent address returns empty state.Assuming an order filled. An
okHTTP status only means the request was accepted. The real outcome lives inresponse.data.statusesand can beerror. Branch on it every time.Market orders with no slippage bound. Always pass a slippage tolerance to
market_open. Thin books and volatility spikes punish naked market orders.Ignoring the address-based rate limit. Beyond the per-IP weight budget, Hyperliquid allows roughly 1 request per 1 USDC of cumulative volume, with a 10,000-request starting buffer. A high-frequency cancel/replace loop on a tiny account will get throttled to one request every 10 seconds.
Reusing a deregistered API wallet. Pruned nonce state can allow replay of previously signed actions. Generate a fresh agent wallet rather than reusing one.
Testing on mainnet. Use
TESTNET_API_URLuntil the full order lifecycle — submit, partial fill, cancel, reconnect — is proven.
What are the real risks, and how do you contain them?
The dangerous risks in an automated setup are rarely the strategy. They are operational.
Key management is the top risk. An API wallet cannot withdraw funds, which is precisely why you should use one and never the master key. Even so, treat the agent secret like production credentials: load it from an environment variable or secrets manager, never commit it, never log it, and scope the host that holds it tightly. If a key is exposed, deregister that agent immediately and issue a new one — do not reuse the address.

Software bugs move real size. A sign error, an off-by-one in position sizing, or a retry loop that resubmits a "failed" order that actually succeeded can all trade in ways you did not intend. Contain this with hard, code-level guardrails: a maximum order size, a maximum open-position check before every submit, and a kill switch that flattens and halts. Never build a blind retry-until-success loop around an order call — a timeout does not tell you whether the order landed.
Slippage and liquidity. Backtests assume fills; live books do not owe you one. Bound market orders with slippage, prefer resting limit or ALO orders where the strategy allows, and size against the visible book depth rather than the mid.
Rate limiting as a failure mode. Getting throttled mid-strategy — unable to cancel a stale order because you spent your budget polling — is a real risk. The docs give cancels a higher cumulative allowance precisely so you can still pull orders when limited, but design your request budget deliberately: WebSocket for data, batched actions where possible, and no busy-polling.
Where to go next
Clone the SDK repo, copy
examples/config.json.exampletoconfig.json, and runexamples/basic_order.pyagainst testnet to confirm your signing works end to end.Replace the polling loop with WebSocket subscriptions for your data path once the REST version is stable.
Add guardrails — max size, max position, kill switch — before you ever point the bot at mainnet.
Layer in richer execution with TWAP and conditional orders once the core lifecycle is bulletproof.
The API is generous and the SDK is well-worn. The discipline that separates a working bot from an expensive one is entirely in the plumbing: authenticate with an agent wallet, query the right address, inspect every response, bound your orders, and respect the limits.
Sources
Further Reading
Hyperliquid in 6 Minutes: The Trader's Cheat Sheet from CEX to On-Chain Perps
If you can read a Binance order book, you can already trade on Hyperliquid — but the account underneath looks nothing like one. Here is what changes, and what to check first.
HyperEVM Onboarding: Wallet, Gas, and Core-to-EVM Transfers
HyperEVM is not a separate chain you bridge to, it is the EVM half of Hyperliquid's single state. How to add the network, get HYPE for gas, and move assets between HyperCore and HyperEVM safely, including the one address that destroys your tokens.
Perpetual Futures: Long, Short, and Realized vs Unrealized PnL
Long or short, realized or unrealized PnL, mark price and funding, explained for your first Hyperliquid perpetual trade.