// Minimal Blockmetric webhook receiver on node:http (no framework): verifies Blockmetric-Signature over the RAW body, // answers fast, deduplicates on the event id and then processes the event. // // BLOCKMETRIC_WEBHOOK_SECRET=whsec_… PORT=3000 node server.mjs // # local test without an API Pro endpoint: npx blockmetric listen --forward-to http://localhost:3000/hook import http from 'node:http' import { constructEvent } from 'blockmetric/webhooks' const SECRET = process.env.BLOCKMETRIC_WEBHOOK_SECRET if (!SECRET) { console.error('Missing BLOCKMETRIC_WEBHOOK_SECRET (whsec_…): the signing secret of your endpoint on /account/api, or the one "blockmetric listen" prints.') process.exit(1) } const PORT = Number(process.env.PORT ?? 3000) const PATH = process.env.HOOK_PATH ?? '/hook' const MAX_BODY = 256 * 1024 // deliveries are at most 64 KiB const seen = new Map() // event id → time; use your database in production const remember = (id) => { seen.set(id, Date.now()) if (seen.size > 10_000) for (const [k] of seen) if (seen.size > 5_000) seen.delete(k) } function handle(event) { // Your logic here: store it, notify someone, update a dashboard … console.log(`${new Date().toISOString()} ${event.type.padEnd(16)} sev ${event.data.severity} ${event.data.coin ?? '-'} ${event.data.title}`) } const server = http.createServer((req, res) => { if (req.method !== 'POST' || req.url?.split('?')[0] !== PATH) { res.writeHead(404).end() return } const chunks = [] let size = 0 req.on('data', (c) => { size += c.length if (size > MAX_BODY) req.destroy() else chunks.push(c) }) req.on('end', async () => { const raw = Buffer.concat(chunks) // verify these exact bytes, never re-serialised JSON let event try { event = await constructEvent(raw, req.headers['blockmetric-signature'], SECRET) } catch { res.writeHead(400, { 'Content-Type': 'text/plain' }).end('invalid signature') console.warn(`${new Date().toISOString()} rejected delivery (bad signature)`) return } res.writeHead(204).end() // answer within 10 s; the work happens after the response if (seen.has(event.id)) return // retries reuse the id remember(event.id) try { handle(event) } catch (e) { console.error('handler failed', e) } }) }) server.listen(PORT, () => console.log(`listening on http://localhost:${PORT}${PATH}`)) process.once('SIGINT', () => server.close()) process.once('SIGTERM', () => server.close())