bill-shock
A live, public ledger of cloud and AI billing-shock incidents — pulled from Hacker News and Reddit, classified by provider, severity-scored, and surfaced as a searchable wall of shame with a prevention checklist.
Live: https://holyai.me/bill-shock/
Port: 4848
Base path: /bill-shock
Stack: Node.js 18+ · Express · better-sqlite3 (WAL) · node-cron · helmet · compression
Why
In May 2026 cloud-billing horror stories have become a weekly news cycle: $18,391 Google Cloud bills from a forgotten public API key, $1M/month production overshoots from a $1,500 proof-of-concept, OpenAI key leaks that drain accounts in hours. The stories trend on Hacker News, light up r/aws, r/googlecloud and r/OpenAI, and get write-ups in TheRegister and Tom's Hardware. There is no single canonical place where they are tracked over time. bill-shock is that place.
Data sources (real, no mocks, no seeds)
Every incident in the database comes from one of the public sources below. No hand-curated seed data, no Math.random() placeholders, no fallback values. If a source returns zero matches today, the row count stays where it is.
| Source | Endpoint | Auth | Frequency |
|---|---|---|---|
| Hacker News (Algolia) | https://hn.algolia.com/api/v1/search_by_date?query=<q>&tags=story&hitsPerPage=50 | none | every 20 min (round-robin over 8 queries) |
| Reddit r/googlecloud | https://www.reddit.com/r/googlecloud/search.json?q=bill&restrict_sr=on&sort=new&limit=50 | none, custom UA | every 30 min |
| Reddit r/aws | same pattern for r/aws | none, custom UA | every 30 min |
| Reddit r/OpenAI | same for r/OpenAI | none, custom UA | every 30 min |
| Reddit r/ChatGPT | same for r/ChatGPT | none, custom UA | every 30 min |
| Reddit r/programminghorror | same | none, custom UA | every 30 min |
| Reddit r/devops | same | none, custom UA | every 30 min |
| Reddit r/AzureCloud | same | none, custom UA | every 30 min |
The 8 HN queries (rotated one-per-tick):
"cloud bill""AWS bill""Google Cloud bill""OpenAI bill""Anthropic bill""unexpected charge""surprise bill""API key leaked" "$"
Optional LLM enrichment runs hourly when OPENROUTER_API_KEY is present: it asks anthropic/claude-haiku-4.5 (via OpenRouter) to extract a dollar amount + provider + root-cause from posts where the regex extractor couldn't. The product works fully without the key — only enrichment quality degrades.
Extraction pipeline
For each raw post:
- Filter for incident-ness — must mention a provider AND (a dollar amount OR a billing keyword like
bill,charged,invoice,shock,nightmare,drained,vacation). - Extract provider from a keyword map (AWS, GCP, Azure, OpenAI, Anthropic, Vercel, Cloudflare, Netlify, Heroku, Supabase, Render, Fly.io, DigitalOcean, Replit).
- Extract dollar amount — the largest match of
\$[0-9,]+(?:\.\d+)?[kKmMbB]?in the post. - Compute severity:
minor(<$1k),notable($1k+),major($10k+),critical($100k+),catastrophic($1M+), orunknown(no amount extracted). - Extract root-cause tags —
key_leak,runaway_loop,cron_misfire,bot_attack,tier_upgrade,forgotten,dev_mode,ai_runaway,egress,storage_spike. - Deduplicate by source_id (HN:
objectID, Reddit: post ID). Updates touch the score / excerpt but never lose history.
HTTP API
All endpoints are public. No auth, no tokens, no admin panel.
| Method | Path | Description |
|---|---|---|
| GET | /bill-shock/health | Health + count + last fetch |
| GET | /bill-shock/api/stats | Hero counters + provider + severity breakdowns |
| GET | /bill-shock/api/incidents?provider=&severity=&min_amount=&q=&limit=50&offset=0&sort=posted_at\|amount_cents | Paged list |
| GET | /bill-shock/api/incidents/:id | Single incident |
| GET | /bill-shock/api/wall-of-shame?limit=10 | Top N by dollar amount |
| GET | /bill-shock/api/leaderboard | Per-provider totals, ranked by damage |
| GET | /bill-shock/api/trend?days=90 | Daily count + damage series |
| GET | /bill-shock/api/causes | Root-cause tag frequency + sample link |
| GET | /bill-shock/api/sources | Last 50 fetch-log rows |
| GET | /bill-shock/api/checklist | Curated prevention checklist |
| GET | /bill-shock/api/providers | Provider-count helper for UI |
Run locally
git clone <repo> bill-shock
cd bill-shock
npm install
cp .env.example .env
node server.js
# → http://127.0.0.1:4848/bill-shock/
The database is created on first run at ./data/bill-shock.db (WAL mode). The initial fetch happens 3 seconds after boot.
Configuration
| Variable | Default | Notes |
|---|---|---|
| PORT | 4848 | HTTP port |
| BASE_PATH | /bill-shock | Path prefix used by every route and the SPA |
| DB_PATH | ./data/bill-shock.db | better-sqlite3 file |
| USER_AGENT | bill-shock/1.0 (+https://holyai.me/bill-shock) | Reddit requires a real UA |
| OPENROUTER_API_KEY | _unset_ | Enables LLM enrichment. Without it, enrichment no-ops |
| ENRICH_ENABLED | true | Set to false to disable enrichment even when key is present |
Cron schedule
| Cron | Job |
|---|---|
| /20 | Hacker News (one query, round-robin) |
| /30 | All seven subreddits |
| 5 | LLM enrichment of unenriched, amount-less incidents from the last 7 days |
| 0 3 * (UTC) | Vacuum fetch_log entries older than 30 days |
Disclaimer
Incidents are reported as found in public posts and are not editorially verified. Severity is derived from the largest dollar amount mentioned in the post text — which is sometimes a hypothetical or a forecast rather than an actual bill. Treat the wall of shame as a finger-on-the-pulse, not as forensic evidence.
License
MIT. Co-authored by Claude Opus 4.7 via Cowork.