// Blockmetric → Telegram: posts radar events (big liquidations, cascades, whale moves, …) to a Telegram chat. // Uses the live stream (API Pro); with other keys it polls GET /radar/events instead. // // BLOCKMETRIC_API_KEY=bm_live_… BLOCKMETRIC_BASE_URL=https:///api/v1 \ // TELEGRAM_BOT_TOKEN=123:abc TELEGRAM_CHAT_ID=-100123 node bot.mjs import { Blockmetric, BlockmetricError } from 'blockmetric' const env = (name, fallback) => { const v = process.env[name] ?? fallback if (v === undefined || v === '') { console.error(`Missing ${name} (see README.md)`) process.exit(1) } return v } const TOKEN = env('TELEGRAM_BOT_TOKEN') const CHAT_ID = env('TELEGRAM_CHAT_ID') // Only for tests against a local fake; the real API is https://api.telegram.org. const TELEGRAM_API = env('TELEGRAM_API_BASE', 'https://api.telegram.org').replace(/\/+$/, '') const KINDS = env('BLOCKMETRIC_KINDS', 'big_liquidation,liq_cascade,whale_liquidated,whale_near_liq') const MIN_SEVERITY = Number(env('BLOCKMETRIC_MIN_SEVERITY', '2')) const POLL_SECONDS = Number(env('POLL_SECONDS', '20')) const client = new Blockmetric() // BLOCKMETRIC_API_KEY, BLOCKMETRIC_BASE_URL const ctrl = new AbortController() process.once('SIGINT', () => ctrl.abort()) process.once('SIGTERM', () => ctrl.abort()) const SEV = { 1: 'ℹ️', 2: '🟡', 3: '🟠', 4: '🔴' } const escape = (s) => String(s ?? '').replace(/[&<>]/g, (c) => ({ '&': '&', '<': '<', '>': '>' })[c]) function format(ev) { const d = ev.data const lines = [`${SEV[d.severity] ?? ''} ${escape(d.title)}`] if (d.body) lines.push(escape(d.body)) lines.push(`${escape(d.kind)}${d.coin ? ` · ${escape(d.coin)}` : ''} · ${d.ts.slice(0, 16).replace('T', ' ')} UTC`) lines.push('Data: Blockmetric (public sources). Not investment advice.') return lines.join('\n') } const sleep = (ms) => new Promise((r) => setTimeout(r, ms)) /** sendMessage with Telegram's flood control (429 + retry_after) and at most one message per second. */ async function send(text) { for (let attempt = 0; attempt < 5; attempt++) { const res = await fetch(`${TELEGRAM_API}/bot${TOKEN}/sendMessage`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ chat_id: CHAT_ID, text, parse_mode: 'HTML', disable_web_page_preview: true }), }).catch((e) => ({ ok: false, status: 0, json: async () => ({ description: e.message }) })) const body = await res.json().catch(() => ({})) if (res.ok && body.ok !== false) return true if (res.status === 429) { await sleep(((body.parameters?.retry_after ?? 5) + 1) * 1000) continue } console.error(`telegram: HTTP ${res.status} ${body.description ?? ''}`) return false } return false } const envelope = (e) => ({ id: `evt_${e.id}`, type: e.kind, created: Math.floor(Date.parse(e.ts) / 1000), data: e }) /** Live events: the stream for API Pro keys, polling otherwise. */ async function* events() { const filter = { kinds: KINDS, minSeverity: MIN_SEVERITY } try { yield* client.stream(filter, { signal: ctrl.signal, onReady: () => console.log('connected to the live stream'), onReconnect: (r) => console.log(`reconnecting in ${r.delayMs} ms (${r.reason})`) }) return } catch (e) { if (!(e instanceof BlockmetricError) || e.code !== 'plan_required') throw e console.log(`live stream needs API Pro, polling every ${POLL_SECONDS}s`) } let last = (await client.radarEvents({ ...filter, limit: 1 })).data[0]?.id ?? 0 while (!ctrl.signal.aborted) { await sleep(POLL_SECONDS * 1000) try { const fresh = (await client.radarEvents({ ...filter, limit: 50 })).data.filter((e) => e.id > last).reverse() for (const e of fresh) { last = Math.max(last, e.id) yield envelope(e) } } catch (e) { console.error(`poll failed: ${e.message}`) } } } for await (const ev of events()) { const ok = await send(format(ev)) console.log(`${ev.data.ts} ${ev.type} ${ev.id} → telegram ${ok ? 'sent' : 'failed'}`) await sleep(1100) }