How to Track Hyperliquid Whales in Real Time
Build a Hyperliquid whale tracker in Python. Stream large fills over gRPC, surface big resting orders, and read whale positions through Dwellir's read infra.
On a quiet afternoon in March, a single wallet opened a $200M+ long on ETH with 50x leverage. Anyone watching the chain saw it form block by block: the resting orders, the fills as they printed, the position swelling in the clearinghouse state. Hours later that same position got unwound, and the move was visible the entire way down. On a centralized exchange you would have learned about it from a screenshot after the fact. On Hyperliquid you can watch it happen.
That difference is the whole reason whale tracking works here. Hyperliquid runs its order book and matching engine on-chain, so every order, fill, and open position is public data keyed by wallet address. You do not need privileged exchange access or a leaked API. You need a fast read path to the L1 and a few size thresholds.
This guide builds that read path in Python. You will stream live fills over gRPC and flag the large ones, watch the order book for oversized resting orders, and pull a target wallet's positions and trade history on demand. All three reads run through Dwellir infrastructure: the L1 gRPC gateway, the Orderbook WebSocket, and the Info API proxy. Order placement is a separate concern that goes straight to Hyperliquid, and you will not need it here, because tracking whales is purely a read problem.
If you have already read our liquidation tracker guide, some of the gRPC plumbing will look familiar. That post filters the fill stream for forced liquidations. This one filters the same infrastructure for size, then widens out to the order book and per-wallet account state to show who is doing what.
Prerequisites
- Python 3.10 or newer.
- A Dwellir API key. The free tier covers 100,000 requests per day, which is enough to prototype every example here. Streaming connections (gRPC and the Orderbook WebSocket) bill differently from unary Info API calls, so check the Hyperliquid docs for current limits on each product.
- The gRPC proto files for the L1 gateway. These are not in PyPI; request them from support@dwellir.com or pull them from the Dwellir gRPC docs, then generate Python stubs with
grpcio-tools. - A few packages:
pip install grpcio grpcio-tools websockets httpx- Basic familiarity with async Python. The fill stream and order book both run as long-lived connections, so the examples use
asyncio.
Why whale tracking is tractable on Hyperliquid
A whale is just a wallet whose actions are large enough to matter. On most venues you can only infer those actions from price and aggregate volume. Hyperliquid removes the guesswork because the matching engine state is the chain state. Three properties make it work:
- Orders are on-chain. Resting limit orders sit in a public book. A $10M bid wall is visible before it ever fills.
- Fills are on-chain. Every execution carries the wallet address, coin, price, size, direction, and closed PnL. You can attribute a trade to an address the instant it prints.
- Positions are on-chain. A wallet's open perpetual positions, leverage, and liquidation prices live in its clearinghouse state and can be queried by anyone.
The catch is latency and throughput. The public Hyperliquid endpoints work, but they cap order book depth at 20 levels, are rolling out IP-based rate limits, and make you poll HTTP for fill history. Dwellir's gateway reads HyperCore data directly off disk and serves it over lower-latency transports: gRPC for the fill firehose, a WebSocket for up to 100 levels of book depth, and a filtering REST proxy for account queries. That is the difference between noticing a whale seconds late and catching the order as it rests.
Which endpoint serves which whale signal
Three distinct signals, three distinct transports. Match the data you want to the right read path before writing any code.
| Whale signal | What you watch for | Dwellir endpoint | Protocol |
|---|---|---|---|
| Large fills / trades | Single executions above a notional threshold, attributed to a wallet | L1 gRPC gateway (StreamFills) | gRPC server stream |
| Large resting orders | Oversized bids/asks parked in the book before they fill | Orderbook WebSocket (l2Book, l4Book) | WebSocket (WSS) |
| Large open positions / wallet activity | A specific wallet's positions, leverage, and fill history | Info API proxy (clearinghouseState, frontendOpenOrders) | REST (POST) |
A note on the read/write split, because it trips people up. Everything in that table is a read, and all of it runs through Dwellir. Placing or canceling orders is a write: it goes to Hyperliquid's native Exchange API at https://api.hyperliquid.xyz/exchange and requires an EIP-712 signature from your wallet. Dwellir does not proxy writes. A whale tracker never places an order, so you can ignore the write side entirely and keep your API key read-only.
Architecture
The tracker has three independent ingestion paths feeding one decision layer.
The Hyperliquid whale-tracking architecture shown in the image:
- Three ingestion paths feed one decision layer:
- gRPC fill stream - large fills as they happen.
- Order book WebSocket - resting size and walls.
- Info API - account positions and state.
- The decision layer thresholds, deduplicates, enriches, and alerts.
- All three feeds are reads through Dwellir; order placement stays native to Hyperliquid.
The first path is a gRPC client holding a long-lived StreamFills connection to the L1 gateway at api-hyperliquid-mainnet-grpc.n.dwellir.com:443. Fills arrive as they execute on-chain. Each one is checked against a per-coin notional threshold, and the ones that clear it become whale-fill events.
The second path is a WebSocket client connected to api-hyperliquid-mainnet-orderbook.n.dwellir.com, subscribed to the book for the coins you care about. On every book update it scans the levels and flags any single price level whose resting size exceeds your wall threshold.
The third path is on-demand rather than streaming. When a wallet shows up in the first two paths, or when you feed in a known address, you hit the Info API proxy at api-hyperliquid-mainnet-info.n.dwellir.com to pull that wallet's current positions and recent fills. This is the enrichment step that turns "a big order appeared" into "this specific wallet, which is already 30x long ETH, just added size."
Those three paths share nothing except the decision layer, where you apply thresholds, dedupe, and emit alerts. Keep them decoupled so a hiccup in one stream does not stall the others. Dwellir runs the streaming endpoints from dedicated edge servers in Singapore and Tokyo, so colocating your tracker near one region shaves real milliseconds off the fill path.
Step 1: Stream large fills over gRPC
The fill stream is your highest-signal source. Every execution on Hyperliquid passes through it, tagged with the wallet that traded. Filtering by notional size turns the firehose into a feed of whale activity.
The gRPC service is hyperliquid_l1_gateway.v1.HyperLiquidL1Gateway and the relevant method is StreamFills, a server-streaming RPC. Authentication is an x-api-key metadata header. Because the exact generated message and field names depend on the proto version you receive from Dwellir, the client below parses the JSON payload each fill response carries rather than reaching into proto fields directly. That keeps the example correct regardless of proto revision. Generate your stubs from the official proto (python -m grpc_tools.protoc ...) and import them as shown.
import grpc
import json
import os
# Generated from the Dwellir L1 gateway proto files.
# Request the proto from support@dwellir.com, then:
# python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. hyperliquid.proto
import hyperliquid_pb2
import hyperliquid_pb2_grpc
GRPC_ENDPOINT = "api-hyperliquid-mainnet-grpc.n.dwellir.com:443"
API_KEY = os.environ["DWELLIR_API_KEY"]
# Per-coin notional thresholds in USD. Anything at or above is a "whale" fill.
# Tune these to the liquidity of each market. A $250k BTC fill is routine;
# a $250k fill in a thin alt is not.
WHALE_THRESHOLDS = {
"BTC": 1_000_000,
"ETH": 500_000,
"HYPE": 250_000,
}
DEFAULT_THRESHOLD = 100_000
def notional(fill):
"""USD notional of a fill = price * size."""
return float(fill["px"]) * float(fill["sz"])
def is_whale_fill(fill):
coin = fill.get("coin", "")
threshold = WHALE_THRESHOLDS.get(coin, DEFAULT_THRESHOLD)
return notional(fill) >= threshold
def parse_fills(response):
"""StreamFills returns a batch with an events array.
Each event is a [user_address, fill_data] pair."""
batch = json.loads(response.data.decode("utf-8"))
fills = []
for event in batch.get("events", []):
if isinstance(event, list) and len(event) == 2:
user_address, fill = event
fill["user"] = user_address
fills.append(fill)
return fills
def stream_whale_fills():
credentials = grpc.ssl_channel_credentials()
options = [("grpc.max_receive_message_length", 150 * 1024 * 1024)]
metadata = [("x-api-key", API_KEY)]
with grpc.secure_channel(GRPC_ENDPOINT, credentials, options=options) as channel:
stub = hyperliquid_pb2_grpc.HyperliquidL1GatewayStub(channel)
request = hyperliquid_pb2.Position()
for response in stub.StreamFills(request, metadata=metadata):
for fill in parse_fills(response):
if is_whale_fill(fill):
usd = notional(fill)
print(
f"WHALE FILL {fill['coin']:>5} "
f"${usd:>14,.0f} "
f"{fill.get('dir', fill.get('side')):<12} "
f"@ {fill['px']} "
f"{fill['user']}"
)
if __name__ == "__main__":
stream_whale_fills()A few things worth calling out:
- Thresholds are per-coin and notional, not size. Ten BTC and ten thousand of a $2 token are wildly different bets. Always threshold on price times size so the filter means the same thing across markets.
- The 150MB receive limit matters. Busy blocks carry a lot of fills. Raising
grpc.max_receive_message_lengthprevents the stream from erroring out under load. This is the same setting the liquidation tracker uses. dirtells you intent. The direction field distinguishes "Open Long", "Close Short", and so on. A whale opening a fresh position is a different signal from one taking profit, and you usually want to treat them differently downstream.- Attribute, then enrich. Every whale fill carries a wallet address. Feed that address into Step 3 to see whether this is a one-off or part of a much larger position.
Why gRPC instead of polling userFills over HTTP? Polling forces you to choose a small set of wallets in advance and ask repeatedly. The gRPC stream gives you every fill on the exchange, push-based, so you discover whales you did not know to watch. That is the whole point of running the firehose.
Step 2: Surface large resting orders from the order book
Fills tell you what already happened. The order book tells you what a whale is about to do, because oversized limit orders rest in the book before they trade. A wall of bids 2% below the market is a whale signaling support; a stack of asks overhead is supply waiting to hit.
This is where the Orderbook WebSocket earns its place. It serves up to 100 levels per side, five times the depth of the public endpoint, which matters because big resting orders often sit several levels deep rather than at the top of book. Connect to wss://api-hyperliquid-mainnet-orderbook.n.dwellir.com/{API_KEY}/ws and subscribe to l2Book. The trailing /ws after the API key is mandatory; without it the connection drops on open.
import asyncio
import json
import os
import websockets
API_KEY = os.environ["DWELLIR_API_KEY"]
WS_URL = f"wss://api-hyperliquid-mainnet-orderbook.n.dwellir.com/{API_KEY}/ws"
COINS = ["BTC", "ETH", "HYPE"]
# Flag any single price level whose resting notional clears this much USD.
WALL_THRESHOLDS = {
"BTC": 2_000_000,
"ETH": 1_000_000,
"HYPE": 500_000,
}
DEFAULT_WALL = 250_000
def scan_for_walls(book):
coin = book["coin"]
threshold = WALL_THRESHOLDS.get(coin, DEFAULT_WALL)
bids, asks = book["levels"][0], book["levels"][1]
walls = []
for side, levels in (("bid", bids), ("ask", asks)):
for level in levels:
usd = float(level["px"]) * float(level["sz"])
if usd >= threshold:
walls.append((side, level["px"], level["sz"], level["n"], usd))
return walls
async def watch_order_book():
async with websockets.connect(WS_URL, max_size=None) as ws:
for coin in COINS:
await ws.send(json.dumps({
"method": "subscribe",
"subscription": {"type": "l2Book", "coin": coin, "nLevels": 100},
}))
async for raw in ws:
msg = json.loads(raw)
if msg.get("channel") != "l2Book":
continue
book = msg["data"]
for side, px, sz, n, usd in scan_for_walls(book):
print(
f"WALL {book['coin']:>5} {side:<3} "
f"${usd:>14,.0f} {sz} @ {px} ({n} orders)"
)
if __name__ == "__main__":
asyncio.run(watch_order_book())The book arrives as levels[0] for bids and levels[1] for asks, each level being { px, sz, n } where n is the number of distinct orders aggregated at that price. The n field is itself a clue. A level holding $3M across a single order (n: 1) is one player; the same notional spread over forty orders is a crowd. A lone, large n: 1 level is the cleaner whale signal.
For the sharpest read, subscribe to l4Book instead of l2Book. The L4 feed exposes individual order-level diffs rather than aggregated price levels, so you can track a specific large order as it is placed, modified, or pulled. Aggregated l2Book data cannot show you a whale yanking a $5M bid the instant before a drop; L4 can. The trade-off is volume: L4 carries every order event, so budget for the extra message rate (the L4 feed benchmarks around 9 messages per second per pair). For HIP-3 DEX markets and spot, use the same subscription with the appropriate coin format, for example "@107" for a spot token by index or "xyz:XYZ100" for a HIP-3 token.
One caveat: a resting order is not a commitment. Whales spoof, and walls vanish. Treat a large resting order as a hypothesis you confirm against the fill stream from Step 1. If the wall is real, you will see it trade.
Step 3: Read a wallet's positions and history
Steps 1 and 2 surface wallets. This step tells you who they are. Given an address, the Info API returns its open positions, leverage, liquidation prices, resting orders, and trade history. This is the enrichment layer that turns a flagged fill into a verdict.
Dwellir proxies a filtered subset of Hyperliquid's /info endpoint. Account-state queries like clearinghouseState and frontendOpenOrders run on the proxy. A few historical and aggregate types, including userFills, are not on the proxy yet and return HTTP 422; for those, fall back to the public endpoint at https://api.hyperliquid.xyz/info. The helper below routes each query to the right host automatically.
import os
import httpx
API_KEY = os.environ["DWELLIR_API_KEY"]
DWELLIR_INFO = "https://api-hyperliquid-mainnet-info.n.dwellir.com/info"
PUBLIC_INFO = "https://api.hyperliquid.xyz/info"
# Types not yet on the Dwellir proxy fall back to the public endpoint.
PUBLIC_ONLY = {"userFills", "userFillsByTime", "historicalOrders"}
def info(query):
qtype = query["type"]
if qtype in PUBLIC_ONLY:
resp = httpx.post(PUBLIC_INFO, json=query, timeout=10)
else:
resp = httpx.post(
DWELLIR_INFO, json=query,
headers={"X-Api-Key": API_KEY}, timeout=10,
)
resp.raise_for_status()
return resp.json()
def profile_wallet(address):
"""Summarize a wallet's perpetual positions and recent fills."""
state = info({"type": "clearinghouseState", "user": address})
summary = state["marginSummary"]
account_value = float(summary["accountValue"])
margin_used = float(summary["totalMarginUsed"])
print(f"\nWallet {address}")
print(f" Account value: ${account_value:,.0f}")
print(f" Margin used: {margin_used / account_value * 100:.1f}%")
for entry in state["assetPositions"]:
pos = entry["position"]
szi = float(pos["szi"]) # signed size: positive long, negative short
if szi == 0:
continue
side = "LONG" if szi > 0 else "SHORT"
notional_usd = abs(szi) * float(pos["entryPx"])
print(
f" {side} {pos['coin']}: {abs(szi)} "
f"(${notional_usd:,.0f}) "
f"lev {pos['leverage']['value']}x "
f"liq @ {pos['liquidationPx']}"
)
# userFills is not proxied yet; this call falls back to the public endpoint.
fills = info({"type": "userFills", "user": address})
print(f" Recent fills: {len(fills)} (showing last 5)")
for fill in fills[:5]:
usd = float(fill["px"]) * float(fill["sz"])
print(
f" {fill['dir']:<12} {fill['coin']:>5} "
f"${usd:>12,.0f} pnl {fill['closedPnl']}"
)
if __name__ == "__main__":
# Feed in an address surfaced by Step 1 or Step 2.
profile_wallet("0x0000000000000000000000000000000000000000")What you get back:
clearinghouseStategivesmarginSummary(account value, total notional position, margin used) andassetPositions. Each position carriesszi, the signed size, where the sign is the direction: positive is long, negative is short. It also carriesentryPx,leverage, andliquidationPx, which together tell you how big the bet is and where it breaks.frontendOpenOrders(not shown above, but the sameinfo()helper) returns the wallet's resting orders withcoin,side,limitPx,sz, andoid. Cross-reference these against the walls from Step 2 to confirm a flagged wall belongs to a wallet you are already tracking.userFillsreturns up to 2,000 recent fills withdir,closedPnl,fee, andtime. For deeper history, paginate withuserFillsByTimeusing the last timestamp you saw.
This is the step that separates a real whale from noise. A $500k fill from a wallet running $50M at 2x is a careful operator scaling in. The same fill from a wallet with $600k and 40x leverage is someone about to get liquidated. The position context changes the meaning entirely.
Production tips
Prototype code that prints to a console is one thing. A tracker you trust to wake you at 3am is another. A few things to get right before you depend on it.
Reconnect, always. Both the gRPC stream and the WebSocket are long-lived connections that will eventually drop, on a deploy, a network blip, or a server rotation between edge regions. Wrap each in a supervisor loop with exponential backoff, and re-subscribe to your coins on every WebSocket reconnect. Never assume a stream stays up for days untouched.
Cache metadata; do not refetch it per fill. Coin metadata (meta, szDecimals, asset indices) changes rarely. Fetch it once at startup, cache it, and refresh on an interval measured in minutes, not on every event. The fill stream can move hundreds of messages a second, and re-resolving metadata inline will throttle you fast.
Batch your account reads. When you are profiling several wallets, do not fire one HTTP request per wallet in a tight loop. clearinghouseState is per-address, but you can pull broad market context in one shot with metaAndAssetCtxs, which returns open interest, volume, and funding for the whole universe at once. That one call replaces dozens of per-coin queries when you are sizing thresholds against current liquidity. Note that metaAndAssetCtxs is not on the Dwellir proxy yet, so route it to the public endpoint.
Respect the limits, and know when to upgrade. The free tier's 100,000 requests per day is generous for unary Info API calls, but a hot whale tracker that re-profiles wallets aggressively can burn through it. Streaming connections are billed separately from unary calls. If you are running the full firehose plus order books across many coins, or you need uncapped throughput with no rate ceiling, a dedicated node (Dwellir runs these out of Tokyo) gives you headroom the shared tier cannot. See Hyperliquid endpoint options beyond public RPC for how the access tiers stack up.
Reject stale data. Query exchangeStatus periodically and check the L1 timestamp. If a stream silently falls behind, you want to know before you act on data that is thirty seconds old. The gRPC gateway retains 24 hours of history, so a brief gap is recoverable, but a tracker acting on stale book state will hand you bad signals.
From data to signal
Three streams of flagged events are raw material, not an alert. The last mile is deciding what is worth surfacing to a human.
Start with deduplication. A single whale entry can throw off a dozen fills as it sweeps the book; collapse those into one "whale opened ~$X position in COIN" event keyed by wallet and a short time window. Then layer context from Step 3: an alert that says "0xabc just opened a $5M ETH long at 25x, now its third-largest position, account is 60% margined" is actionable. "Big ETH fill" is not.
From there it is plumbing. Push notable events to a Telegram or Discord webhook, write them to a time-series store for backtesting, or fan them out to subscribers over your own WebSocket exactly as the liquidation tracker does. The same wallet-attributed fills also power copy-trading and flow-following strategies. And if you want to connect whale positioning to funding dynamics, who is paying to hold a crowded trade, pair this with our Hyperliquid funding rates guide.
Wrapping up
Whale tracking on Hyperliquid comes down to three reads pointed at one decision layer: the gRPC fill stream for what is trading, the Orderbook WebSocket for what is resting, and the Info API for who is holding. Because the exchange runs on-chain, all three are public and all three run read-only through Dwellir, with order placement staying native to Hyperliquid where it belongs.
Get a free API key at dwellir.com, request the gRPC proto from support@dwellir.com, and start with the fill stream in Step 1. Once whale fills are printing to your console, layer in the order book and wallet enrichment, and you have a tracker that sees the market's biggest players before most people know they moved.
References
- Dwellir Hyperliquid documentation
- Dwellir gRPC API reference
- Dwellir Order Book Server docs
- Dwellir Info API docs
- Hyperliquid API documentation
Trading cryptocurrencies involves significant risk. This content is for educational purposes only and does not constitute financial advice.
Hyperliquid Funding Rates: How They Work and How to Fetch Them
Learn how Hyperliquid funding rates work, the hourly premium-plus-interest formula, and how to fetch current, predicted, and historical funding via API.
Polymarket API: The Complete Developer Guide
A developer's guide to the Polymarket API: discover markets with Gamma, read prices and place orders on the CLOB, stream fills, and read positions on Polygon.