Wallet liquidation-risk monitor

A warning when a watched position comes within 10 % or 3 % of its liquidation price.

Cheapest plan
Free
Time to build
15 min
Endpoints
3
/wallets/{address} · /whales · /positions/top
Languages
2
Node 18+, Python 3.9+, no installs

What you get

A monitor for a list of Hyperliquid wallets: for every open position it compares the mark with the liquidation price and warns once when the distance falls below your threshold, and again when it gets dangerous.

DANGER 0x6791…8bbc xyz:SP500 short $134.4K 50x: mark 7,728, liquidation 7,745 (0.2 %)
ok      0x5b5d…c060 ETH short $252.22M 5x: mark 2,370, liquidation 3,707 (56.4 %)
ok      0x5b5d…c060 BTC short $212.92M 5x: mark 81,476, liquidation 136,370 (67.4 %)
ok      0x5b5d…c060 SOL short $74.05M 7x: mark 102.87, liquidation 297.4 (189.1 %)
ok      0x5b5d…c060 ZEC short $53.70M 3x: mark 1,319, liquidation 4,601 (248.8 %)
ok      0x5b5d…c060 HYPE short $39.40M 5x: mark 82.23, liquidation 361.08 (339.1 %)
ok      0x5b5d…c060 xyz:GOLD short $27.97M 4x: mark 4,055, liquidation 24,953 (515.4 %)
ok      0x5b5d…c060 XRP short $17.93M 5x: mark 1.33, liquidation 11.76 (784.0 %)
ok      0x5b5d…c060 xyz:SILVER short $9.85M 5x: mark 53.86, liquidation 842.5 (1464.2 %)
ok      0x5b5d…c060 WLD short $6.36M 2x: mark 0.43862, liquidation 9.7 (2111.7 %)
ok      0x5b5d…c060 PUMP short $6.21M 5x: mark 0.004351, liquidation 0.098388 (2161.3 %)
ok      0x5b5d…c060 ENA short $5.85M 3x: mark 0.20768, liquidation 4.98 (2295.9 %)
ok      0x5b5d…c060 XPL short $5.45M 3x: mark 0.076145, liquidation 1.94 (2454.3 %)
… 13 more lines

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

Build it step by step

  1. Read one wallet

    Each open position has mark, liqPx and liqDistance = (liquidation price - mark) / mark. Negative for longs (price must fall), positive for shorts.

    curl -s -H "Authorization: Bearer $BLOCKMETRIC_API_KEY" \
      "$BLOCKMETRIC_BASE_URL/wallets/0xbc1bc64be47052fbab69d12fe5da1c3327788d1b"
  2. Choose two thresholds

    WARN_AT=0.10 warns within 10 %, DANGER_AT=0.03 within 3 %. The monitor alerts on each change of level only, not on every poll.

  3. List the wallets

    WALLETS=0x...,0x... Untracked wallets answer 404; the monitor says so and moves on.

  4. Send the warning somewhere

    Replace console.log with a message to your chat (see the alert bot recipe) or your paging tool.

The complete monitor

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
// Wallet liquidation-risk monitor: watches the open positions of Hyperliquid wallets and warns when the mark comes
// within a set distance of a position's liquidation price. Node 18+, no dependencies. One GET /wallets/{address} per
// wallet and poll (each answer has the positions with liqPx and the signed distance liqDistance).
//
//   export BLOCKMETRIC_API_KEY=bm_live_...            # optional: without a key it uses keyless public access
//   export BLOCKMETRIC_BASE_URL=https://<domain>/api/v1
//   export WALLETS=0xabc...,0xdef...                   # the wallets to watch (yours, or any tracked wallet)
//   node risk-monitor.mjs
//
// Options: WARN_AT (0.10 = warn within 10 %), DANGER_AT (0.03), POLL_SECONDS (60), ONCE=1 (one check, then exit).
// Distance = (liquidation price - mark) / mark, so -0.08 means the price may fall 8 % before a long is liquidated.

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 WALLETS = (process.env.WALLETS ?? '').split(',').map((a) => a.trim()).filter(Boolean)
if (!WALLETS.length) throw new Error('Set WALLETS=0x...,0x... (Hyperliquid addresses)')
const WARN_AT = Number(process.env.WARN_AT ?? 0.1)
const DANGER_AT = Number(process.env.DANGER_AT ?? 0.03)
const POLL_SECONDS = Number(process.env.POLL_SECONDS ?? 60)
const ONCE = process.env.ONCE === '1'
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))

async function api(path) {
  for (let attempt = 1; ; attempt++) {
    const res = await fetch(BASE + path, { 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()
    return { status: res.status, body }
  }
}

const usd = (n) => (n >= 1e6 ? `$${(n / 1e6).toFixed(2)}M` : `$${(n / 1e3).toFixed(1)}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)}`

/** ok | warn | danger for one position (no liquidation price, e.g. fully collateralised: ok). */
export function level(p) {
  if (typeof p.liqDistance !== 'number') return 'ok'
  const d = Math.abs(p.liqDistance)
  return d <= DANGER_AT ? 'danger' : d <= WARN_AT ? 'warn' : 'ok'
}

const lastLevel = new Map() // wallet:coin -> level, so each change alerts once

async function check() {
  for (const address of WALLETS) {
    const { status, body } = await api(`/wallets/${address}`)
    if (status === 404) {
      console.log(`${short(address)}: not tracked by the API (404). For a wallet of your own, Guard on the site (Alerts > Guard) watches it live.`)
      continue
    }
    if (status !== 200) throw new Error(`${status} ${body.error?.code}: ${body.error?.message}`)
    const { wallet, positions } = body.data
    if (!positions.length) console.log(`${short(address)}: no open positions (account ${usd(wallet.accountValue ?? 0)})`)
    for (const p of positions) {
      const key = `${address}:${p.coin}`
      const now = level(p)
      const dist = typeof p.liqDistance === 'number' ? `${(p.liqDistance * 100).toFixed(1)} %` : 'no liquidation price'
      const text = `${short(address)} ${p.coin} ${p.side} ${usd(p.positionUsd)} ${p.leverage}x: mark ${px(p.mark)}, liquidation ${p.liqPx ? px(p.liqPx) : '-'} (${dist})`
      if (lastLevel.get(key) !== now && now !== 'ok') console.log(`${now === 'danger' ? 'DANGER' : 'WARNING'} ${text}`)
      else if (ONCE || !lastLevel.has(key)) console.log(`ok      ${text}`)
      lastLevel.set(key, now)
    }
  }
}

for (;;) {
  await check()
  if (ONCE) break
  await sleep(POLL_SECONDS * 1000)
}

Run it:

WALLETS=0x... node risk-monitor.mjs
WALLETS=0x... python risk_monitor.py
WALLETS=0x... ONCE=1 node risk-monitor.mjs   # one check

Download: risk-monitor.mjs · risk_monitor.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.

One wallet with its positions: GET /wallets/0x6791f31ebe6e6fc39a26c83160da54a175648bbc?limit=1
{
  "data": {
    "wallet": {
      "address": "0x6791f31ebe6e6fc39a26c83160da54a175648bbc",
      "label": null,
      "accountValue": 0,
      "pnlMonth": null,
      "vlmMonth": null,
      "source": "trades",
      "priority": 3,
      "firstSeen": "2026-10-27T06:27:31.649Z",
      "lastPolledAt": "2026-10-27T11:31:39.861Z",
      "positionCount": 1
    },
    "positions": [
      {
        "address": "0x6791f31ebe6e6fc39a26c83160da54a175648bbc",
        "label": null,
        "accountValue": 0,
        "coin": "xyz:SP500",
        "side": "short",
        "size": 17.469,
        "entryPx": 7671.055,
        "positionUsd": 134430.94,
        "leverage": 50,
        "leverageType": "cross",
        "liqPx": 7745.44511698,
        "liqDistance": 0.0023092703,
        "mark": 7727.6,
        "marginUsed": 2688.62,
        "unrealizedPnl": -425.28,
        "updatedAt": "2026-10-27T11:31:39.861Z"
      }
    ],
    "totals": {
      "positionUsd": 134430.94,
      "longUsd": 0,
      "shortUsd": 134430.94,
      "unrealizedPnl": -425.28,
      "unrealizedPnlMissing": 0
    }
  },
  "meta": {
    "updatedAt": "2026-10-27T11:31:39.861Z",
    "sources": [
      "hyperliquid"
    ],
    "plan": "api_dev",
    "delayMinutes": 0,
    "attribution": {
      "text": "Data by Blockmetric · Source: Hyperliquid",
      "url": "https://blockmetric.io/"
    },
    "license": {
      "id": "internal",
      "attributionRequired": false
    }
  }
}

Rate limits and pagination

  • One request per wallet per poll. A free key's 1,000 requests a day cover one wallet every 90 seconds, or 10 wallets every 15 minutes.
  • Positions are refreshed by our position polls; a check more often than once a minute rarely shows new numbers.
  • sort=near_liq on GET /whales lists the tracked wallets closest to liquidation right now: a quick way to find positions at risk.
  • Cross-margin positions share the account margin: a loss on one coin moves the liquidation price of the others.

Common pitfalls

  • A position without liqPx (for example fully collateralised) has no distance; treat it as safe, not as zero.
  • The mark moves between polls. For a position at 1 % distance a one-minute poll is too slow; use Guard or a shorter interval.

Who it is for

  • Traders who watch their own or a fund’s wallets from a script
  • Risk teams that want a log of near-liquidation moments
  • Builders of portfolio tools

Plan and limits

FreeCheapest
GET /wallets/{address} is in every plan. One request per wallet and poll.
10/min, 1,000/day, 30 days of history
Pro$29/mo
20 requests a minute: more wallets per minute, personal use.
20/min, 5,000/day, 365 days of history
API Dev$49/mo
Many wallets at a short interval, internal use.
60/min, 50,000/day, 365 days of history

Compare plans

Honest limits

  • The API answers for the wallets we track (404 otherwise). For a wallet of your own, Guard on the site (Alerts › Guard) watches it live without code.
  • Liquidation prices are the ones Hyperliquid reports for the position at our last poll.

Next steps

More recipes like this

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