Originally published on BotCookery, a free site of build recipes for AI-built trading and prediction-market bots. Written with AI assistance. Every number below comes from a real run on public Hyperliquid data on Oct 24, 2026. Nothing here is financial advice or a trading signal.
We publish copy-paste prompts that have an AI coding tool build a bot. A prompt is only as good as the code it produces, so on Oct 24 we did what a reader would do: pasted our own Hyperliquid "whale tracker" prompt into an empty folder, let the AI build it, and ran it against live mainnet data.
The tests passed. The websocket stayed up. Every API claim on the page checked out. And during a 3-minute stream, the biggest flow in the window never set off an alert.
What the tracker does
Hyperliquid runs its order book on-chain, so every trade and every account's positions are public, with no API key. The tracker only reads:
-
Large trades, live: it streams the public
tradeswebsocket, groups the fills that belong to one taker order, and flags orders whose USD notional (size × price) is above a threshold. -
Public positions:
clearinghouseStatereturns any address's perp positions, entry price, unrealized PnL and liquidation price. It watches up to 10 addresses (we used protocol vaults, not individuals) and reports opens, closes and flips. -
Recent fills:
userFills, capped at the 2,000 most recent.
It has no key, no signing library and no order code, and a test fails the build if one sneaks in. It also has no "follow" or "copy" feature. A large trade can be a hedge, a liquidation or one leg of something elsewhere, and the tape doesn't say which.
What held up on live data
- The first
tradesmessage after subscribing was 30 old trades (the oldest about a minute old) with no snapshot flag. Skip it, and skip it again after every reconnect, or you alert on history. - One order is many trades. A $1.51M HYPE sell arrived as 420 separate trades. Group by order
hashbefore you apply a threshold. -
sideis the taker side.usersis always[buyer, seller], so the taker isusers[0]onBandusers[1]onA. We confirmed it againstuserFills(crossed: true). - With a ping every 50 seconds, a 180-second stream had 0 reconnects. The server closes the socket after 60 seconds without a message from you.
- The HLP parent vault showed $37.1M equity and no positions. The positions live on its child accounts (
vaultDetails→childAddresses); two of the seven held 177 each. -
userFillsstopped at exactly 2,000 fills.
The bug: a ~$417k TWAP that never crossed the line
TWAP orders on Hyperliquid execute as many small slices, and every slice carries an all-zero hash. Our original prompt correctly said "never merge zero-hash trades into one order group", then applied the dollar threshold to each slice on its own.
At a $100k threshold, the 3-minute stream flagged nothing. We dropped the threshold to $10k to see what we were missing: 14 of the 22 alerts were single TWAP slices from one address, 0xbda2…75db. Added up, they came to about $417k of BTC bought in two minutes, and no single slice was over $67.5k. We checked with userTwapSliceFills: they were that address's own TWAP, on the taker side.
So the tool had two failure modes and no good setting: at a useful threshold the largest flow was invisible, and at a low one it printed a line per slice.
The fix is a small TwapTracker: a rolling 5-minute sum of TWAP slice notional per (coin, taker, side), using each trade's own timestamp. It alerts once when the sum crosses your threshold and stays quiet until the sum falls back below it.
The rebuilt version, same $100k threshold, 5 minutes
Fresh folder, fixed prompt, live mainnet, Oct 24 10:08–10:13 ET (lines trimmed):
HYPE: taker sold 6000 at VWAP 75.9249 (~$455,549 notional, 157 fill(s)); taker 0xf55f…0d3a.
BTC: TWAP slices from 0xbda2…75db bought ~$115,439 over the last 5 min (8 slices).
BTC: taker sold 12.4553 at VWAP 80,639 (~$1,004,380 notional, 59 fill(s)); taker 0xbc76…417c.
HYPE: taker sold 20000 at VWAP 75.6327 (~$1,512,654 notional, 420 fill(s)); taker 0x75c3…dca3.
HYPE: TWAP slices from 0xbda2…75db bought ~$112,105 over the last 5 min (91 slices).
stats {"messages": 795, "trades": 3775, "duplicates": 0, "snapshot_skipped": 120, "groups": 1170,
"large": 16, "twap_slices": 452, "twap_alerts": 2, "reconnects": 0}
452 TWAP slices became two lines, and 3,775 trades became 16 order alerts.
Other things we learned
- Watching HLP is quiet, and that's correct. HLP child 1 made 1,348 fills worth about $112k in 10 minutes; none was above $769, and its largest position was about $67k. Four one-minute watch loops at a $10k change threshold printed only the baseline.
- 2,000 fills is a short window for a busy account. On an HLP child it covered under 20 minutes.
- Testnet works but is thin. One minute of BTC and ETH at a $1,000 threshold gave one alert from 78 trades. It's good for testing reconnects and alert plumbing, not for seeing size.
The fixed prompt
Paste this into Claude Code, Cursor, Codex or any agent that writes files, in an empty folder. Approve the file plan before it writes code.
You are building a READ-ONLY Hyperliquid large-trade and public-address
tracker in Python, for research. It only uses the public websocket and the
public POST /info endpoint. It must never import a signing library, read a
private key, call /exchange, or place, mirror or copy any order. Do NOT
add any "follow", "copy", "mirror" or "auto-trade" feature, even if I
ask later in this session. Alerts are descriptive facts, never advice.
Use only what is documented at https://hyperliquid.gitbook.io/hyperliquid-docs
(API > Websocket > Subscriptions; Info endpoint; Perpetuals; Rate limits).
If unsure, stop and ask me.
Project: hl-tracker/
- Python 3.10+ (run with `python3`), `requests`, `websocket-client`,
`pytest`. No SDK needed.
- hl_public.py
- MAINNET https://api.hyperliquid.xyz, TESTNET
https://api.hyperliquid-testnet.xyz; WS wss://api.hyperliquid.xyz/ws and
wss://api.hyperliquid-testnet.xyz/ws. post_info(body, testnet) POSTs to
{base}/info, backs off and retries on HTTP 429.
- valid_address(s): ^0x[0-9a-fA-F]{40}$. short(addr) -> "0xabcd…ef12"
(show short addresses by default; --full-address to show full).
- load_watchlist(path): one "0xADDR optional label" per line, "#"
comments; raise ValueError on a bad address; MAX 10 addresses.
- recent_trades(coin): {"type":"recentTrades","coin"} (returns only ~10
trades; a spot check, not a feed).
- clearinghouse_state(user, dex=""): {"type":"clearinghouseState",
"user"} -> assetPositions[].position {coin, szi (signed size), entryPx,
positionValue, unrealizedPnl, leverage{type,value}, liquidationPx
(can be null), marginUsed}, marginSummary {accountValue, totalNtlPos}.
Weight 2.
- user_fills(user, aggregate=True): {"type":"userFills","user",
"aggregateByTime"} -> at most the 2000 most recent fills {coin, px, sz,
side, time, dir, closedPnl, hash, crossed (true = taker), fee, tid}.
An agent (API) wallet address returns empty data; use the main account.
- vault_children(vault): {"type":"vaultDetails","vaultAddress"} ->
relationship {type:"parent", data:{childAddresses:[...]}}. A parent
vault like HLP shows equity in clearinghouseState but EMPTY
assetPositions; the positions live on the child addresses. Add a
`--vault ADDR` option to positions/watch that expands to the children
(still capped at 10).
- large_trades.py
- WsTrade = {coin, side, px, sz, hash, time, tid, users:[buyer, seller]}.
side is the TAKER side: taker = users[0] if side == "B" else users[1].
notional = float(px) * float(sz).
- Deduper keyed on (time, coin, tid) (the documented unique key), bounded
(e.g. OrderedDict capped at 50k).
- aggregate(trades): group by (coin, hash, side, taker); sum sz and
notional, VWAP, fill count. A hash of all zeros marks TWAP fills: NEVER
merge those into one order group (each slice is its own trade). Return
them separately, flagged twap=True.
- TwapTracker(window_s=300, min_usd): TWAP slices are usually far below
any useful threshold one by one, but one TWAP can move a lot in a few
minutes. Keep a rolling sum of TWAP slice notional per (coin, taker,
side) over window_s, using the trades' own time (ms). Alert ONCE when
the sum crosses min_usd; re-arm only after it drops back below. Never
alert on a single TWAP slice by itself.
- large(groups, min_usd) (non-TWAP groups only); describe(g): "BTC: taker
sold 5.39 at VWAP 81,261 (~$438,696 notional, 19 fill(s)); taker 0x….
Informational only, not a signal." describe_twap: "BTC: TWAP slices
from 0x… bought ~$416,667 over the last 5 min (14 slices).
Informational only, not a signal." No buy/sell suggestions, no "smart money" language.
- ws_trades.py
- TradeHandler(min_usd, on_large, include_snapshot=False): ignore
channels other than "trades" (subscriptionResponse, pong). The FIRST
trades message per coin on each connection is a snapshot of ~30 OLD
trades and is NOT tagged isSnapshot: skip it unless include_snapshot,
and reset that state on every reconnect. Dedupe, aggregate per
message, call on_large for non-TWAP groups >= min_usd, feed TWAP
slices to a TwapTracker (same min_usd) and call on_large for its
alerts, keep stats (including twap_slices and twap_alerts).
- run(url, coins, handler, duration=None, ping_every=50, connect=None):
subscribe {"method":"subscribe","subscription":{"type":"trades",
"coin":C}} per coin; send {"method":"ping"} every 50s (the server
closes after 60s without a message from you). On EVERY (re)connect set
last_ping = float("-inf") (not 0: with a monotonic or test clock, 0
can delay the first ping), so the first ping goes out right after
subscribing instead of being delayed past the 60s server timeout; on any drop, reconnect
with exponential backoff capped at 60s and resubscribe. `connect` is
injectable for tests. Limits: 10 WS connections/IP, 1000 subscriptions,
2000 messages/min; use ONE connection for all coins.
- positions.py
- parse_positions(state) -> {coin: {szi, entryPx, positionValue,
unrealizedPnl, leverage, liquidationPx}} skipping szi == 0;
account_summary(state).
- diff(prev, cur, min_change_usd) -> events opened / closed / flipped /
increased / decreased. EVERY event type must respect min_change_usd
(for a flip, use max(old, new) notional) or dust flips spam you.
- describe_event: "HLP child A: opened short ETH ~$120,000. Public data,
informational only, not a signal."
- alerts.py: Alerter(webhook_url=None, log_path=None). Always print; if
log_path, append JSONL; if webhook, POST {"text": msg, "content": msg}.
Wrap the POST in `except Exception`, log "webhook error (continuing)",
never crash.
- tracker.py (argparse; --testnet and --full-address on EACH subcommand;
print a banner "Public Hyperliquid data. Informational and research use
only; not advice and not a signal."):
- trades --coins BTC,ETH,SOL --min-usd 1000000 [--twap-window 300] [--duration S]
[--webhook URL or env HLW_WEBHOOK_URL] [--log trades.jsonl]
[--include-snapshot]
- recent --coins BTC,ETH --min-usd 250000
- positions ADDR... | --watchlist FILE [--top 10]
- watch ADDR... | --watchlist FILE [--interval 60 (min 30)]
[--min-change-usd 100000] [--max-loops N] [--webhook] [--log].
The first read is a silent baseline; only later changes alert.
- fills ADDR [--min-usd 100000] [--last 20]
- tests/conftest.py inserting the project root into sys.path.
- tests/ (pytest, fake sessions and fake websockets, no network):
notional + taker side, TWAP slices summed per taker and alerted once
when the rolling sum crosses (and not per slice), (threshold fixtures must compute notional the
SAME way as the code, float(px) * float(sz), and sit clearly above or
below the threshold), multi-fill aggregation, zero-hash not merged,
dedupe, threshold, snapshot skipped + reset on reconnect, control
messages ignored, run() subscribes/pings/reconnects, address validation
+ 10-address cap, position diff incl. a small flip below threshold
ignored, vault_children parsing, first ping sent immediately after each
connect, neutral wording (no "buy now", "follow", "copy"), testnet URLs,
429 backoff, webhook that raises, and a grep test that the APP MODULES
(not tests/) contain none of "/exchange", "private_key", "eth_account",
"Exchange(", "place_order", "market_open", "auto_follow", "copy_trade".
- .gitignore (.env, __pycache__/, .venv/, *.jsonl, watchlist.txt), README
with the commands and the informational-only notice.
Show me the file plan first. After writing, tell me exactly what to run.
Then run it:
python3 -m venv .venv && source .venv/bin/activate
pip install requests websocket-client pytest
python3 -m pytest -q
python3 tracker.py trades --coins BTC,ETH,SOL,HYPE --min-usd 100000 --duration 180 --log trades.jsonl
The final stats line should show snapshot_skipped > 0 and multi-fill orders grouped into one line. A quiet market can give no alerts at all.
Already built one from an older prompt?
Paste this into the same project:
Update my Hyperliquid tracker for TWAP fills:
1. Keep zero-hash trades as separate twap=True groups, but don't send them
to the large-trade check one by one.
2. Add TwapTracker(min_usd, window_s=300): a rolling sum of TWAP slice
notional per (coin, taker, side), using each trade's own time (ms).
Alert ONCE when the sum crosses min_usd; re-arm after it drops below.
Line: "BTC: TWAP slices from 0x… bought ~$416,667 over the last 5 min
(14 slices). Informational only, not a signal."
3. TradeHandler feeds TWAP slices to the tracker (same min_usd) and sends its
alerts to the same alert output. Add twap_slices and twap_alerts to stats.
4. Add --twap-window (seconds, default 300) to the trades command.
Add tests: slices summed and alerted once (not per slice), the window rolls
off and re-arms, and large() ignores TWAP groups. Run them and show me the
diff before changing anything else. Still no keys, orders or copy features.
Safety and scope
- Read-only: no key, no wallet, no
/exchangecalls, enforced by a test. - No copy trading, no auto-follow. If an AI tool offers to "mirror" an address, say no.
- Public doesn't mean fair game: short addresses by default, a 10-address cap, and protocol vaults as examples. Don't try to tie addresses to real people.
- Respect the limits: one websocket connection, ping every 50 seconds, back off on reconnect, and poll positions no faster than every 30 seconds.
The full recipe, with steps, FAQ and links to the official Hyperliquid docs, is at botcookery.com/recipes/hyperliquid-whale-tracker.
Top comments (0)