Liquidation alert bot for Telegram

Every big liquidation in your chat seconds after it happens.

Cheapest plan
Free
Time to build
15 min
Endpoints
2
/liquidations · /me
Languages
2
Node 18+, Python 3.9+, no installs

What you get

A small bot that watches every liquidation we collect and posts the big ones to a Telegram chat or group. It polls forward from the last id it saw, so it never misses one and never sends one twice, and it picks its own interval from the daily quota of your key.

Watching liquidations of at least $250.0K.
ETH short liquidated: $1.93M at 2,346.3 on Hyperliquid (15:21 UTC)
ETH short liquidated: $1.20M at 2,346.3 on Hyperliquid (15:21 UTC)
BTC short liquidated: $1.57M at 81,222.6 on Hyperliquid (21:31 UTC)
ETH short liquidated: $1.55M at 2,357.8 on Hyperliquid (21:33 UTC)
xyz:HOOD long liquidated: $262.4K at 88.04733 on Hyperliquid (21:34 UTC)
BTC short liquidated: $782.7K at 81,202.1 on Hyperliquid (21:31 UTC)
kPEPE short liquidated: $309.7K at 0.00472 on Hyperliquid (01:41 UTC)
ETH short liquidated: $1.25M at 2,375.5 on Hyperliquid (08:26 UTC)
BTC short liquidated: $6.35M at 81,699.8 on Hyperliquid (08:35 UTC)
BTC short liquidated: $308.6K at 81,697.9 on Hyperliquid (08:36 UTC)
BTC short liquidated: $566.0K at 81,800.2 on Hyperliquid (08:36 UTC)
ETH short liquidated: $2.25M at 2,377.3 on Hyperliquid (08:35 UTC)
BTC short liquidated: $293.4K at 81,496.6 on Hyperliquid (08:35 UTC)

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

Build it step by step

  1. Get a key and set two variables

    Create a key on Account › API keys, then set BLOCKMETRIC_API_KEY and BLOCKMETRIC_BASE_URL in the shell that runs the bot. Without a key the bot still runs on keyless public access, with lower limits.

  2. Look at the data once

    Ask for the big liquidations of the last hour. Each row has time, venue, coin, the side of the liquidated position, price and size in USD. meta.nextAfter is the id to continue from.

    curl -s -H "Authorization: Bearer $BLOCKMETRIC_API_KEY" \
      "$BLOCKMETRIC_BASE_URL/liquidations?minUsd=250000&sinceMinutes=60&limit=5"
  3. Poll forward, oldest first

    Pass after=<the last id you saw> and from=<when the bot started>. The answer holds only newer rows, oldest first, and a new meta.nextAfter. That is the whole loop: nothing missed after a restart, nothing twice.

    curl -s -H "Authorization: Bearer $BLOCKMETRIC_API_KEY" \
      "$BLOCKMETRIC_BASE_URL/liquidations?minUsd=250000&from=2026-10-28T08:00:00Z&after=304070&limit=200"
  4. Connect Telegram

    Create a bot with @BotFather, add it to your chat and set TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID. Telegram answers 429 with retry_after; the bot waits as told.

  5. Run it and keep it running

    node alert-bot.mjs or python alert_bot.py. On a server, run it under systemd, pm2 or a container with restart on failure. ONCE=1 runs one poll and exits, which is handy for a first test.

The complete bot

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
// Liquidation alert bot: posts every big liquidation to Telegram as it happens.
// Node 18+, no dependencies. Polls GET /liquidations forward with ?after= (oldest first, nothing missed, nothing twice)
// and adapts its interval to the daily quota of your key (GET /me is free and never rate-limited).
//
//   export BLOCKMETRIC_API_KEY=bm_live_...            # optional: without a key the bot uses keyless public access
//   export BLOCKMETRIC_BASE_URL=https://<domain>/api/v1
//   export TELEGRAM_BOT_TOKEN=123456:ABC...  TELEGRAM_CHAT_ID=-1001234567890   # optional
//   node alert-bot.mjs
//
// Options (environment): MIN_USD (250000), COINS (all, e.g. BTC,ETH), POLL_SECONDS (auto from your quota),
// SINCE_MINUTES (0 = only new liquidations; 60 = start with the last hour), ONCE=1 (one poll, then exit).
// Licence: Free, Pro and Desk keys are for personal use (your own chat). A bot that posts to a public channel shows the
// data to others and needs API Pro (with the credit line from meta.attribution).

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 ?? 250_000)
const COINS = new Set((process.env.COINS ?? '').split(',').map((c) => c.trim().toUpperCase()).filter(Boolean))
const SINCE_MINUTES = Number(process.env.SINCE_MINUTES ?? 0)
const ONCE = process.env.ONCE === '1'
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))

/** GET an API path; retries 429 and 5xx with the server's Retry-After. */
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))
  const headers = KEY ? { Authorization: `Bearer ${KEY}` } : { 'X-Blockmetric-Access': 'public' }
  for (let attempt = 1; ; attempt++) {
    const res = await fetch(url, { headers, signal: AbortSignal.timeout(30_000) })
    if ((res.status === 429 || res.status >= 500) && attempt < 5) {
      await sleep(Number(res.headers.get('Retry-After') ?? 2 ** attempt) * 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) => (n >= 1e6 ? `$${(n / 1e6).toFixed(2)}M` : `$${(n / 1e3).toFixed(1)}K`)
const px = (p) => p.toLocaleString('en-US', { maximumFractionDigits: p >= 100 ? 1 : 5 })
const VENUE = { hyperliquid: 'Hyperliquid', lighter: 'Lighter', binance: 'Binance', bybit: 'Bybit' }

/** One message per liquidation. `side` is the side of the liquidated position. */
export const message = (l) => `${l.coin} ${l.side} liquidated: ${usd(l.usd)} at ${px(l.price)} on ${VENUE[l.venue] ?? l.venue} (${l.ts.slice(11, 16)} UTC)`

async function post(url, body) {
  for (let attempt = 1; attempt <= 3; attempt++) {
    const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), signal: AbortSignal.timeout(10_000) })
    if (res.status !== 429) return res.ok
    const j = await res.json().catch(() => ({}))
    await sleep(((j.parameters?.retry_after ?? j.retry_after ?? 2) + 0.5) * 1000) // Telegram says how long
  }
  return false
}

async function deliver(text) {
  console.log(text)
  const { TELEGRAM_BOT_TOKEN: tg, TELEGRAM_CHAT_ID: chat } = process.env
  if (tg && chat) await post(`https://api.telegram.org/bot${tg}/sendMessage`, { chat_id: chat, text, disable_web_page_preview: true })
}

/** Poll interval that keeps a 20 % reserve of the daily quota for everything else you run. */
async function pollSeconds() {
  if (process.env.POLL_SECONDS) return Number(process.env.POLL_SECONDS)
  const { data } = await api('/me')
  const perDay = data.limits?.perDay ?? 300
  return Math.max(10, Math.ceil(86_400 / (perDay * 0.8)))
}

async function main() {
  const every = ONCE ? 0 : await pollSeconds()
  const from = new Date(Date.now() - SINCE_MINUTES * 60_000).toISOString()
  let after = 0
  console.log(`Watching liquidations of at least ${usd(MIN_USD)}${COINS.size ? ` on ${[...COINS].join(', ')}` : ''}${every ? `, every ${every} s` : ''}.`)
  for (;;) {
    const { data, meta } = await api('/liquidations', { after, from, minUsd: MIN_USD, limit: 200 })
    for (const l of data) if (!COINS.size || COINS.has(l.coin.toUpperCase())) await deliver(message(l))
    after = meta.nextAfter ?? after
    if (ONCE) break
    if (data.length < 200) await sleep(every * 1000) // a full page: fetch the rest right away
  }
}

main().catch((e) => {
  console.error(e.message)
  process.exit(1)
})

Run it:

node alert-bot.mjs
python alert_bot.py
ONCE=1 SINCE_MINUTES=60 node alert-bot.mjs   # one poll over the last hour

Download: alert-bot.mjs · alert_bot.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.

Big liquidations, oldest first after an id: GET /liquidations?minUsd=250000&sinceMinutes=1440&after=0&limit=2
{
  "data": [
    {
      "id": 279889,
      "ts": "2026-10-27T15:21:36.580Z",
      "venue": "hyperliquid",
      "coin": "ETH",
      "side": "short",
      "price": 2346.31584836,
      "qty": 821.2615,
      "usd": 1926938.87
    },
    {
      "id": 280418,
      "ts": "2026-10-27T15:21:33.111Z",
      "venue": "hyperliquid",
      "coin": "ETH",
      "side": "short",
      "price": 2346.31479721,
      "qty": 512.1898,
      "usd": 1201758.51
    }
  ],
  "meta": {
    "updatedAt": "2026-10-27T15:21:36.580Z",
    "sources": [
      "hyperliquid"
    ],
    "plan": "api_dev",
    "delayMinutes": 0,
    "attribution": {
      "text": "Data by Blockmetric · Source: Hyperliquid",
      "url": "https://blockmetric.io/"
    },
    "nextCursor": null,
    "nextAfter": 280418,
    "restricted": [
      "binance",
      "bybit"
    ],
    "license": {
      "id": "internal",
      "attributionRequired": false
    }
  }
}

Faster than polling: stream or webhooks

  • With API Pro the live stream (GET /api/v1/stream, Server-Sent Events) pushes radar events such as big liquidations and cascades as soon as they are stored; reconnect with Last-Event-ID and you get what you missed in the last 15 minutes.
  • With API Dev or API Pro, signed webhooks push the same events to your server. The webhook recipe shows the receiver with signature verification.

Webhooks guide · Live stream

Rate limits and pagination

  • Free: 10/min, 1,000/day, 30 days of history; the bot keeps 20 % of the day for everything else you run.
  • One request per poll, whatever the number of liquidations (up to 200 per page; a full page is fetched again at once).
  • GET /me is never rate-limited, so reading your quota at start costs nothing.
  • Filter on the server: minUsd is applied before paging, so a quiet hour costs one small request.
  • Keep after in a file or database if the bot restarts often; from alone would resend the rows since the start time.
  • A 429 carries Retry-After in seconds. The bot waits exactly that long instead of guessing.

Common pitfalls

  • side is the side of the liquidated position: "short liquidated" means shorts were forced to buy.
  • Do not page back with cursor for a live bot; cursor pages go back in time, after goes forward.
  • Telegram allows about one message per second per chat. Batch into one message when many liquidations arrive at once.

Who it is for

  • Traders who want a ping for big liquidations without keeping a chart open
  • Community admins who run a Telegram group or channel
  • Anyone starting with the API: one endpoint, about 80 lines

Plan and limits

FreeCheapest
A free key polls every 108 s within 1,000 requests a day. Personal use: your own chat.
10/min, 1,000/day, 30 days of history
API Dev$49/mo
50,000 requests a day (poll every few seconds), webhooks instead of polling, internal use in your organisation.
60/min, 50,000/day, 365 days of history
API Pro$249/mo
The live stream and the display licence: post to a public channel or group, with the credit line.
300/min, 500,000/day, 1095 days of history

Compare plans

Honest limits

  • Hyperliquid liquidations are confirmed by their fills; Lighter liquidations are trades the exchange flags. Binance and Bybit single liquidations are not served through the API while their licence review is pending (403 source_restricted); totals include them.
  • Exchanges report liquidations incompletely (one order per symbol and second on some venues), so exchange totals are a lower bound.
  • Every plan is real time; the delay is the poll interval you choose.

Example projects

telegram-alert-bot

Posts Blockmetric radar events to a Telegram chat: big liquidations, liquidation cascades, whales liquidated or close to liquidation (configurable). With an API Pro key it uses the live stream (GET /api/v1/stream, reconnects with Last-Event-ID); with other keys it polls GET /api/v1/radar/events.

README.md · bot.mjs · package.json

liquidation-alert-bot

Alerts on big liquidations and liquidation cascades as they happen, and warns once per cluster when the price comes close to a large liquidation cluster of the heatmap (built from real tracked Hyperliquid positions). Alerts go to the console and, optionally, to any JSON webhook (ALERT_WEBHOOK_URL, e.g. a Slack incoming webhook).

README.md · bot.mjs · package.json

python-liquidation-alert-bot

The Python twin of liquidation-alert-bot: alerts on big liquidations and liquidation cascades, and warns once per cluster when the price comes close to a large liquidation cluster of the heatmap. Alerts go to stdout and, optionally, to any JSON webhook (ALERT_WEBHOOK_URL, e.g. a Slack incoming webhook). Standard library plus the blockmetric SDK.

README.md · bot.py

These projects use the official SDKs, which are being prepared for release. Until then the dependency-free program above runs as it is.

Next steps

More recipes like this

LiveHyperliquid markets 28sOperationalHyperliquid positions & whales 10sOperationalLiquidation heatmap & levels 19sOperationalLighter markets & liquidations 20sOperationalLighter positions & heatmap 12sOperationalLighter RH positions & heatmap 10sOperationalBinance markets 21sOperationalBybit markets 12sOperationalBinance liquidations 20sOperationalBybit liquidations 20sOperationaldYdX positions & heatmap 12sOperationalGMX positions & heatmap 22sOperationalLong/short & open interest history 1mOperationalOrder books & trade flow 12sOperationalOn-chain transfers 19sOperationalSecurity monitor 32sOperationalBlockmetric Sentiment 9mOperationalOfficial news 8mOperationalAlert delivery 11sOperational10:36:49 UTCData: Blockmetric Research · Binance · Bybit