Liquidation alert bot for Telegram
Every big liquidation in your chat seconds after it happens.
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
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.
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"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"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.
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 hourDownload: 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.
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
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
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.
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).
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.
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
webhooks/radar/events+1Whale position trackerOne line every time a large wallet opens, adds, cuts, flips or closes a position.Free15 min to build/whales/feed/whales+2Wallet 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