Whale position tracker

One line every time a large wallet opens, adds, cuts, flips or closes a position.

Cheapest plan
Free
Time to build
15 min
Endpoints
4
/whales/feed · /whales · /positions/top
Languages
2
Node 18+, Python 3.9+, no installs

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,372

Real output of the program below, run against our API on 2026-10-28 10:38 UTC.

Build it step by step

  1. 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"
  2. 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"
  3. 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.

  4. 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.mjs

Download: 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

FreeCheapest
Every change of the last 30 days; a poll every 90 seconds stays within 1,000 requests a day.
10/min, 1,000/day, 30 days of history
Pro$29/mo
365 days of history and 5,000 requests a day, personal use.
20/min, 5,000/day, 365 days of history
API Dev$49/mo
Use it inside your organisation, poll every few seconds.
60/min, 50,000/day, 365 days of history

Compare plans

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

LiveHyperliquid markets 8sOperationalHyperliquid positions & whales 8sOperationalLiquidation heatmap & levels 18sOperationalLighter markets & liquidations 13sOperationalLighter positions & heatmap 12sOperationalLighter RH positions & heatmap 8sOperationalBinance markets 36sOperationalBybit markets 1mOperationalBinance liquidations 15sOperationalBybit liquidations 15sOperationaldYdX positions & heatmap 17sOperationalGMX positions & heatmap 16sOperationalLong/short & open interest history 15sOperationalOrder books & trade flow 8sOperationalOn-chain transfers 14sOperationalSecurity monitor 26sOperationalBlockmetric Sentiment 1mOperationalOfficial news 5mOperationalAlert delivery 8sOperational10:13:33 UTCData: Blockmetric Research · Binance · Bybit