Whale position tracker
One line every time a large wallet opens, adds, cuts, flips or closes a position.
What you get
A tracker that follows the position changes of large tracked Hyperliquid wallets: who moved, which coin, how much, at what price, and the leverage and liquidation price after the move. Filter by size, coin or your own list of wallets.
Whale position changes of at least $1.00M.
09:39 UTC 0x9e2c…5508 closed BTC long: $4.07M at 81,398
09:39 UTC 0x9e2c…5508 opened xyz:CL long: $1.02M at 101.6 (now $1.02M, 20x)
09:40 UTC 0xf71a…7215 cut ETH short: $1.04M at 2,369 (now $1.04M, 25x, liq. 2,868)
09:42 UTC 0x9e2c…5508 added to xyz:CL long: $4.26M at 101.39 (now $5.27M, 20x, liq. 77.83)
09:42 UTC 0x354b…6425 added to xyz:CL short: $1.41M at 101.39 (now $4.50M, 8x, liq. 345.76)
09:42 UTC 0xbc1b…8d1b added to BTC long: $1.03M at 81,458 (now $1.75M, 3x)
09:44 UTC 0xcf3f…f95f added to BTC long: $2.54M at 81,486 (now $8.22M, 5x, liq. 14,441)
09:48 UTC 0xbc1b…8d1b added to BTC long: $3.43M at 81,476 (now $5.19M, 3x, liq. 31,981)
09:51 UTC 0xf71a…7215 closed ETH short: $1.04M at 2,372Real output of the program below, run against our API on 2026-10-28 10:38 UTC.
Build it step by step
See who the whales are
GET /whales is the leaderboard of tracked wallets by open notional, with a public label where a cited source names the wallet.
curl -s -H "Authorization: Bearer $BLOCKMETRIC_API_KEY" \ "$BLOCKMETRIC_BASE_URL/whales?sort=size&limit=20"Read the feed since a point in time
GET /whales/feed?since=<ISO time> returns the changes after that time, newest first. Each change has kind, side, the USD change, the price and, in data, leverage and liquidation price after the move.
curl -s -H "Authorization: Bearer $BLOCKMETRIC_API_KEY" \ "$BLOCKMETRIC_BASE_URL/whales/feed?minUsd=1000000&since=2026-10-28T09:00:00Z&limit=200"Poll and print
Collect every page back to since (meta.nextCursor), print oldest first, remember the newest time and ids. WATCH=0x...,0x... limits the output to your own list.
Go deeper on one wallet
GET /wallets/{address} has every open position of that wallet with entry, leverage, liquidation price and its latest changes.
The complete tracker
Set this once in the shell that runs the code:
export BLOCKMETRIC_API_KEY=bm_live_… # create one on /account/api
export BLOCKMETRIC_BASE_URL=https://api.blockmetric.io/v1// Whale position tracker: every time a large tracked Hyperliquid wallet opens, adds to, cuts, flips or closes a position,
// or gets liquidated. Node 18+, no dependencies. Polls GET /whales/feed with ?since= and prints one line per change.
//
// export BLOCKMETRIC_API_KEY=bm_live_... # optional: without a key it uses keyless public access
// export BLOCKMETRIC_BASE_URL=https://<domain>/api/v1
// node whale-tracker.mjs
//
// Options: MIN_USD (1000000 = changes of at least $1M), COIN (all), WATCH (only these addresses, comma-separated),
// POLL_SECONDS (60), SINCE_MINUTES (60 = start with the last hour), ONCE=1 (one poll, then exit).
const BASE = (process.env.BLOCKMETRIC_BASE_URL ?? '').replace(/\/+$/, '')
const KEY = process.env.BLOCKMETRIC_API_KEY
if (!BASE) throw new Error('Set BLOCKMETRIC_BASE_URL, e.g. https://<domain>/api/v1')
const MIN_USD = Number(process.env.MIN_USD ?? 1_000_000)
const COIN = process.env.COIN || undefined
const WATCH = new Set((process.env.WATCH ?? '').split(',').map((a) => a.trim().toLowerCase()).filter(Boolean))
const POLL_SECONDS = Number(process.env.POLL_SECONDS ?? 60)
const SINCE_MINUTES = Number(process.env.SINCE_MINUTES ?? 60)
const ONCE = process.env.ONCE === '1'
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
async function api(path, params = {}) {
const url = new URL(BASE + path)
for (const [k, v] of Object.entries(params)) if (v !== undefined && v !== null && v !== '') url.searchParams.set(k, String(v))
for (let attempt = 1; ; attempt++) {
const res = await fetch(url, { headers: KEY ? { Authorization: `Bearer ${KEY}` } : { 'X-Blockmetric-Access': 'public' }, signal: AbortSignal.timeout(30_000) })
if (res.status === 429 && attempt < 5) {
await sleep(Number(res.headers.get('Retry-After') ?? 5) * 1000)
continue
}
const body = await res.json()
if (!res.ok) throw new Error(`${res.status} ${body.error?.code}: ${body.error?.message}`)
return body
}
}
const usd = (n) => `$${Math.abs(n) >= 1e6 ? `${(Math.abs(n) / 1e6).toFixed(2)}M` : `${(Math.abs(n) / 1e3).toFixed(0)}K`}`
const px = (p) => p.toLocaleString('en-US', { maximumFractionDigits: p >= 1000 ? 0 : p >= 1 ? 2 : 6 })
const short = (a) => `${a.slice(0, 6)}…${a.slice(-4)}`
const VERB = { open: 'opened', increase: 'added to', decrease: 'cut', close: 'closed', flip: 'flipped to', liquidated: 'was liquidated on' }
/** One line per position change; the leverage and liquidation price come from the position poll. */
export function line(e) {
const who = e.label ?? short(e.address)
const d = e.data ?? {}
const after = d.positionUsd ? ` (now ${usd(d.positionUsd)}${d.leverage ? `, ${d.leverage}x` : ''}${d.liqPx ? `, liq. ${px(d.liqPx)}` : ''})` : ''
return `${e.ts.slice(11, 16)} UTC ${who} ${VERB[e.kind] ?? e.kind} ${e.coin} ${e.side}: ${usd(e.usdDelta)} at ${px(e.price)}${after}`
}
async function main() {
let since = new Date(Date.now() - SINCE_MINUTES * 60_000).toISOString()
const seen = new Set()
console.log(`Whale position changes of at least ${usd(MIN_USD)}${COIN ? ` in ${COIN}` : ''}${WATCH.size ? ` by ${WATCH.size} watched wallets` : ''}.`)
for (;;) {
// Newest first: collect every page back to `since`, then print oldest first.
const batch = []
let cursor
do {
const { data, meta } = await api('/whales/feed', { since, minUsd: MIN_USD, coin: COIN, limit: 200, cursor })
batch.push(...data)
cursor = meta.nextCursor ?? undefined
} while (cursor)
for (const e of batch.reverse()) {
if (seen.has(e.id) || (WATCH.size && !WATCH.has(e.address.toLowerCase()))) continue
seen.add(e.id)
console.log(line(e))
if (e.ts > since) since = e.ts
}
if (ONCE) break
await sleep(POLL_SECONDS * 1000)
}
}
main().catch((e) => {
console.error(e.message)
process.exit(1)
})
Run it:
node whale-tracker.mjs
python whale_tracker.py
COIN=ETH MIN_USD=5000000 node whale-tracker.mjsDownload: whale-tracker.mjs · whale_tracker.py · requests.sh. MIT-0: use it as you like.
Real responses
What the API answered when we ran this recipe, with long lists shortened.
Whale position changes of at least $1M: GET /whales/feed?minUsd=1000000&limit=1
{
"data": [
{
"id": 281668,
"ts": "2026-10-28T09:51:11.745Z",
"address": "0xf71ab4e4172730ed096b832f37982942935c7215",
"label": null,
"coin": "ETH",
"kind": "close",
"side": "short",
"sizeDelta": -440.5238,
"usdDelta": -1044746.24,
"price": 2371.6,
"data": {
"prevSize": 440.5238,
"newSize": 0,
"prevSide": "short",
"entryPx": 2374.61,
"liqPx": 2868.11269617,
"leverage": 25,
"leverageType": "cross",
"prevUnrealizedPnl": 2234.090471,
"prevPolledAt": "2026-10-28T09:40:54.097Z",
"polledAt": "2026-10-28T09:51:11.745Z",
"positionUsd": 0,
"prevPositionUsd": 1044746.24,
"markMin": 2369.3,
"markMax": 2371.8
}
}
],
"meta": {
"updatedAt": "2026-10-28T09:51:11.745Z",
"sources": [
"hyperliquid"
],
"plan": "api_dev",
"delayMinutes": 0,
"attribution": {
"text": "Data by Blockmetric · Source: Hyperliquid",
"url": "https://blockmetric.io/"
},
"nextCursor": "azN8MjAyNi0x…",
"license": {
"id": "internal",
"attributionRequired": false
}
}
}Rate limits and pagination
- A free key allows 1,000 requests a day: set POLL_SECONDS=90 or more there (the default 60 suits Pro and above).
- Every page of a poll is one request; a quiet minute is one page.
- Increases and decreases are kept 90 days; opens, closes, flips and liquidations 400 days.
- minUsd filters on the size of the change, not of the position: a $50M position that adds $200K is not a $1M change.
- Keep the ids you printed; since is exclusive, but two changes can share a timestamp.
Common pitfalls
- Labels are only shown when a source names the wallet; everything else is an address. Do not invent names.
- A change is seen between two position polls (or live from the trade stream), so its time can lag the trade by up to the poll interval.
Who it is for
- Traders who follow large wallets
- Researchers who log whale behaviour around big moves
- Bots that copy or fade positioning
Plan and limits
Honest limits
- The feed covers the Hyperliquid wallets we track, large wallets far better than small ones; it is not every trader on every venue.
- Lighter accounts are on GET /whales and /positions/top with venue=lighter; the change feed is Hyperliquid.
Next steps
More recipes like this
/liquidations/meCascade and cluster alertsLiquidation cascades pushed to your server, plus a warning before price hits a big cluster.API Dev30 min to buildwebhooks/radar/events+1Wallet liquidation-risk monitorA warning when a watched position comes within 10 % or 3 % of its liquidation price.Free15 min to build/wallets/{address}/whales+1