A small, standalone Node.js service that counts the number of distinct Hive accounts active in the last 30 minutes and exposes that number via a single local HTTP endpoint.
Designed to run as a sidecar alongside a Next.js app on the same VPS. The Next.js app proxies the count to the browser — no browser ever polls Hive directly.
Hive produces a block every ~3 seconds. On startup the sidecar bulk-fetches the last 600 blocks (30 minutes worth), then polls for new blocks every 30 seconds. Any account that appears as a signer or actor in an operation within the window is counted. The window self-prunes — accounts that haven't been seen for 30 minutes are evicted automatically.
One poller. One number. One source of truth.
index.js — entry point, wires everything together
hive-client.js — beacon node discovery, dhive client, 60-min node refresh
rolling-window.js — in-memory Map with upsert + evict logic
poller.js — block fetching loop and account extraction
server.js — Express HTTP server (localhost only)
ecosystem.config.js— PM2 process config
tests/ — Jest test suite (21 tests)
The sidecar binds to 127.0.0.1:3099 only — never exposed to the public internet.
{ "count": 2104, "warming": false, "updatedAt": "2026-06-12T14:32:00.000Z" }count— distinct accounts active in the last 30 minutes.nullduring warm-up.warming—truewhile the cold-start bulk fetch is in progress.updatedAt— ISO timestamp of the last successful poll tick.
{ "status": "ok" }npm install
cp .env.example .env
node index.jsThe service logs progress to stdout:
[hive-sidecar] Starting bulk cold-start fetch from block 87654321…
[hive-sidecar] Cold-start complete. 1847 accounts in window. Starting poll loop.
[hive-sidecar] Tick — block 87654931 — 1923 active accounts
npm testpm2 start ecosystem.config.js
pm2 saveThe process will restart automatically on crash and survive VPS reboots after pm2 save.
All options are set via environment variables. See .env.example for the full list with defaults.
| Variable | Default | Description |
|---|---|---|
PORT |
3099 |
Port to bind on localhost |
POLL_INTERVAL_MS |
30000 |
How often to fetch new blocks (ms) |
WINDOW_MS |
1800000 |
Rolling activity window size (ms) |
BULK_BATCH_SIZE |
50 |
Blocks per RPC call during cold-start |
MIN_NODE_SCORE |
80 |
Minimum beacon score to accept a node |
BEACON_API_URL |
https://beacon.peakd.com/api/nodes |
Node discovery endpoint |
Create app/api/active-users/route.ts in your Next.js app to proxy the sidecar:
import { NextResponse } from 'next/server';
const SIDECAR_URL = process.env.HIVE_ACTIVITY_SIDECAR_URL ?? 'http://127.0.0.1:3099';
export const dynamic = 'force-dynamic';
export const revalidate = 0;
export async function GET() {
try {
const res = await fetch(`${SIDECAR_URL}/active-users`, {
signal: AbortSignal.timeout(3000),
});
if (!res.ok) return NextResponse.json({ count: null, warming: false }, { status: 200 });
const data = await res.json();
return NextResponse.json(data, {
status: 200,
headers: { 'Cache-Control': 'public, max-age=30, stale-while-revalidate=60' },
});
} catch {
return NextResponse.json({ count: null, warming: false }, { status: 200 });
}
}Add to .env.local:
HIVE_ACTIVITY_SIDECAR_URL=http://127.0.0.1:3099