← back to gallery

bill-shock

Live ledger of cloud + AI billing shocks pulled from Hacker News and Reddit.

financecloudbillingfintechawsgcpopenaianthropicincidenttrackermonitoring
Open product ↗

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):

  1. "cloud bill"
  2. "AWS bill"
  3. "Google Cloud bill"
  4. "OpenAI bill"
  5. "Anthropic bill"
  6. "unexpected charge"
  7. "surprise bill"
  8. "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:

  1. Filter for incident-ness — must mention a provider AND (a dollar amount OR a billing keyword like bill, charged, invoice, shock, nightmare, drained, vacation).
  2. Extract provider from a keyword map (AWS, GCP, Azure, OpenAI, Anthropic, Vercel, Cloudflare, Netlify, Heroku, Supabase, Render, Fly.io, DigitalOcean, Replit).
  3. Extract dollar amount — the largest match of \$[0-9,]+(?:\.\d+)?[kKmMbB]? in the post.
  4. Compute severity: minor (<$1k), notable ($1k+), major ($10k+), critical ($100k+), catastrophic ($1M+), or unknown (no amount extracted).
  5. Extract root-cause tagskey_leak, runaway_loop, cron_misfire, bot_attack, tier_upgrade, forgotten, dev_mode, ai_runaway, egress, storage_spike.
  6. 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.