← back to gallery

AI Cancel Watch

Public ledger of enterprise AI projects that got scrapped — with sources

researchaienterprisenewsledgerskeptic
Open product ↗

ai-cancel-watch

A public, live ledger of enterprise AI projects that got scrapped. Every row is a real news story about a real company killing a real AI pilot — extracted at runtime from three public feeds via LLM, never seeded.

Built for the AI-skeptical CFO, board director, or consultant tired of vendor decks claiming "transformational ROI" — this is the counter-corpus.

What it does

  1. Scrapes three public feeds on a cron schedule for cancellation stories.
  2. Enriches each new candidate via OpenRouter (anthropic/claude-haiku-4-5), which extracts a strict JSON object: {company, tool, vendor, duration_months, stated_reason, cost_usd, is_cancellation_story}. Rows where is_cancellation_story=false are dropped — they never enter the database.
  3. Exposes a searchable / sortable table + per-row OG share cards (1200×630 SVG).
  4. Accepts crowdsourced submissions via a public POST endpoint — same LLM enrichment runs server-side, the row is stored as verified=0 and shown with an "Unverified" badge.

There is no auth. There is no seed data. If all three sources are down on day one, the table is empty — that is the correct behaviour. The "Live source health" panel at the bottom of the UI shows each source's last_fetched_at, status, and items added on the last run so users can verify the system is actually live.

Data sources

All fetched at runtime from real public endpoints. No mocks, no presets.

| Source | URL | Refresh |
|---|---|---|
| HN Algolia (story search) | https://hn.algolia.com/api/v1/search_by_date?query=<term>&tags=story&hitsPerPage=50 — rotates 6 query terms (scraps AI, abandons AI, shelves AI, killed AI pilot, sunset AI, shuts down AI tool), one term per tick | every 30 minutes |
| Google News RSS | https://news.google.com/rss/search?q=%22scraps+AI%22+OR+%22shelves+AI%22+OR+%22abandons+AI%22+OR+%22killed+AI+pilot%22+OR+%22sunset+AI%22+OR+%22shuts+down+AI+tool%22&hl=en-US&gl=US&ceid=US:en | every 60 minutes (at minute 7) |
| The Register | https://www.theregister.com/headlines.atom (pre-filtered with a regex; only candidates touching cancellation verbs near AI terms are sent to the LLM) | every 60 minutes (at minute 23) |
| User submissions | POST /ai-cancel-watch/api/submit — fetches the submitted URL with a 10s timeout / 1 MB cap, extracts <title> + <meta og:description> + plaintext body, runs the same LLM enrichment, stores as verified=0 | on-demand |
| Favicons (rendering only) | https://www.google.com/s2/favicons?domain=<host>&sz=128 | per render |
| Enrichment (not a row source) | OpenRouter anthropic/claude-haiku-4-5 | per candidate |

Dedupe key is a canonicalized URL (lowercased host, no www., no #fragment, no tracking query params).

API

All endpoints are public. No auth, no API keys for callers.

| Method | Path | What it returns |
|---|---|---|
| GET | /ai-cancel-watch/health | {ok:true} |
| GET | /ai-cancel-watch/ | the SPA |
| GET | /ai-cancel-watch/api/stats | {total, total_cost_usd, distinct_vendors, distinct_companies, last_7_days} |
| GET | /ai-cancel-watch/api/cancellations | paginated list. Query: q, vendor, year, min_cost, verified (0/1/all), sort (date_desc/cost_desc/duration_asc), limit (≤200), offset |
| GET | /ai-cancel-watch/api/cancellations/:id | single row |
| GET | /ai-cancel-watch/api/vendors | distinct vendor list (for filter UI) |
| GET | /ai-cancel-watch/api/years | distinct year list (for filter UI) |
| GET | /ai-cancel-watch/api/leaderboard | {weekly_hero, top_cost, shortest_life} |
| GET | /ai-cancel-watch/api/sources | per-source {name, label, url, interval, last_fetched_at, last_status, items_total, ...} |
| POST | /ai-cancel-watch/api/submit | body {source_url, note?}. Fetches the URL, enriches via LLM. Returns {id, verified:0} or {error} with HTTP 400/409 |
| GET | /ai-cancel-watch/cards/:id.svg | 1200×630 SVG cancellation card, Cache-Control: public, max-age=3600 |
| GET | /ai-cancel-watch/share/:id | HTML page with og:image/cards/:id.svg, designed for social previews |

Stack

No frontend build step. Vanilla JS SPA in public/ (~500 lines).

Run locally

npm install
cp .env.example .env
# edit .env and set OPENROUTER_API_KEY for enrichment to work
node server.js
# open http://localhost:4849/ai-cancel-watch/

Without OPENROUTER_API_KEY, the scrapers still run and log to fetch_log, but enrichment fails so no rows get inserted — the system shows "0 tracked" and the sources panel reflects that honestly.

Schema

CREATE TABLE cancellations (
  id, company, tool, vendor, duration_months, stated_reason, cost_usd,
  source_url, source_name, headline, snippet, published_at, scraped_at,
  url_canonical UNIQUE, verified, enrichment_raw
);
CREATE TABLE fetch_log (
  id, source_name, status, items_found, items_new, error, ts
);

No users table. No sessions. No admin login. Bad rows are fixed by SSH'ing into the DB — outside MVP.

Out of scope

Predicting cancellations, vendor reviews, individual layoffs (that's ai-pink-slip), email alerts, RSS output, payments, edit-from-UI, PNG card rasterization (SVG is enough — Twitter/LinkedIn render SVG og:image fine and we don't ship a headless Chrome in a 4-hour build).