ai-pink-slip
A live, searchable leaderboard of layoffs that companies have explicitly attributed to AI or automation — sourced from real public feeds, classified by a transparent keyword scorer, and ranked by AI-attributed headcount over rolling windows.
Built for journalists, recruiters, HR strategists, and tech workers who want one place to answer "how much of this layoff wave is genuinely AI vs. vague restructuring?" and to benchmark whether their own role might be next.
What it does
- Polls 4 public sources on cron, dedupes by URL, persists to SQLite.
- Runs a pure-JS keyword classifier over each title+summary that emits
{score, label, signals}. No LLM call — every decision is auditable. - Extracts headcount and percentage from text (best-effort regex; null if unclear — never invented).
- Normalizes company names via an ~80-entry alias map (Meta=Facebook, X=Twitter, Alphabet=Google, etc.) plus a heuristic title parser.
- Aggregates into a leaderboard sorted by total AI-attributed headcount over 30d / 90d / 365d / all.
- Renders a vanilla-JS dark-themed SPA plus a server-rendered weekly share card with OG meta tags (designed to be screenshotable / link-previewable in Slack, Twitter, Telegram).
Data sources
Every "live" data point traces to one of these. No mock data. No Math.random().
| Source | URL | Refresh |
|--------|-----|---------|
| HN Algolia API | https://hn.algolia.com/api/v1/search_by_date?query=layoff&tags=story&hitsPerPage=100 | every 15 min |
| TechCrunch layoffs RSS | https://techcrunch.com/tag/layoffs/feed/ | every 30 min |
| Google News RSS | https://news.google.com/rss/search?q=layoffs+AI+OR+%22ai-attributed%22+OR+%22replaced+by+AI%22&hl=en-US&gl=US&ceid=US:en | every 30 min |
| layoffs.fyi | https://layoffs.fyi/ (HTML scrape) | every 6 h |
If a source fails, the previous data is kept; /health reports per-source status.
API endpoints
All endpoints are under /ai-pink-slip. No auth. Public.
| Method | Path | Purpose |
|--------|------|---------|
| GET | /health | {ok, sources:{<src>:{status,lastRunAt,itemsLastRun,…}}, sourcesLive} |
| GET | /api/stats | totals: AI-attributed items 30/90/365d, headcount 30/90/365d, total items, companies tracked, last refresh |
| GET | /api/leaderboard?window=30d\|90d\|365d\|all&limit=50 | array of {slug, name, headcount, itemCount, aiItemCount, latestAt, topItem} |
| GET | /api/companies | every company tracked, with item / AI-item / AI-headcount counts |
| GET | /api/companies/:slug | {company, items[], totals} for one company |
| GET | /api/items?q=&company=&label=&source=&limit=&offset= | filtered / searched items |
| GET | /share/weekly | server-rendered HTML page with OG meta tags — top 5 AI-attributed cuts last 7 days |
| GET | / | SPA shell |
| GET | /app.js, /style.css | static assets |
Classification
Three labels via a weighted regex scorer:
- AI-attributed (score ≥ 40): strong phrases like "because of AI", "replaced by AI", "AI-driven layoffs", "AI restructuring".
- Maybe (15–39): AI / automation / GenAI / LLM in close proximity to layoff words.
- No (< 15): off-topic or no AI angle.
Every signal that matched is stored in ai_signals and surfaced as a chip in the UI so you can audit the decision.
Run locally
Requirements: Node ≥ 22.
npm install
PORT=4834 node server.js
# open http://localhost:4834/ai-pink-slip/
The server triggers an initial scrape of all 4 sources on boot and then schedules cron jobs. The SQLite database lives at data/ai-pink-slip.db (WAL mode).
Configuration
.env.example:
PORT=4834
NODE_ENV=production
OPENROUTER_API_KEY=__INJECT_FROM_VAULT__
BRAVE_API_KEY=__INJECT_FROM_VAULT__
OPENROUTER_API_KEY and BRAVE_API_KEY are reserved for future use — no current code path requires them.
Tech stack
Node 22 · Express · better-sqlite3 (WAL) · node-cron · helmet · compression · cheerio · fast-xml-parser. Frontend is vanilla JS (no framework).
File layout
ai-pink-slip/
├── server.js # express bootstrap, mounts /ai-pink-slip, starts cron
├── db.js # better-sqlite3 init, schema, prepared statements
├── lib/
│ ├── classifier.js # keyword + regex AI-attribution scoring
│ ├── headcount.js # regex extract headcount / percent
│ ├── company.js # normalize, slugify, alias map (~80 entries)
│ ├── fetcher.js # fetch w/ timeout + UA
│ ├── cron.js # registers scrapers with node-cron
│ └── windows.js # date-window helpers
├── scrapers/
│ ├── hn.js # HN Algolia → items
│ ├── techcrunch.js # TC RSS → items
│ ├── googlenews.js # Google News RSS → items
│ └── layoffsfyi.js # layoffs.fyi HTML → items
├── routes/
│ ├── health.js
│ ├── stats.js
│ ├── leaderboard.js
│ ├── companies.js
│ ├── items.js
│ └── share.js
└── public/
├── index.html
├── app.js # vanilla JS SPA — hash routing
└── style.css # dark theme
Caveats
- The classifier is keyword-based; it cannot detect sarcasm or denial wrapped in quotes.
- Company names from heuristic title parsing may produce a row like "AI Layoffs" when a generic headline talks about the topic without naming a specific firm — refine the alias map to fix.
- layoffs.fyi renders via Airtable; its HTML scrape is best-effort and may yield zero rows in some cycles.
- No predictive model, no comments, no email signup, no auth. Read-only public dashboard.
License
MIT — built by holyai.me.