coasean-cuts
A live 36-month tracker of which tech job titles are vanishing from
Ask HN: Who is hiring? threads — and which are exploding.
For founders deciding which roles to skip, laid-off PMs/engineers picking
their next bet, and journalists who need a citable number when the next
"AI ate the org chart" essay drops.
Top riser this run: Prompt Engineer (+133% YoY). Top decliner: Developer Relations (-36% YoY).
What it does
- Discovers every monthly "Ask HN: Who is hiring?" thread on HN going back
- ~36 months via the public Algolia API.
- Pulls every top-level comment in each thread — each comment is one job
- posting — and parses out the candidate role title and company from the
- conventional
Company | Role | Location | Remote/On-siteheader. - Normalizes each unique title into one of 22 canonical role categories
- via Claude Haiku 4.5 (or, if no API key is configured, via a
- deterministic regex classifier — that path is what produced the current
- public deployment).
- Layers in Remote OK's public feed as a daily second-source signal.
- Materializes monthly counts and computes year-over-year movers,
- surfacing the top 6 risers and top 6 decliners on the home page.
- Renders a 1200×630 share card (
/api/snapshot.svg) so links pasted into - Slack / X / Bluesky generate a preview with the headline number.
Real data sources
| Source | URL | Refresh |
|---|---|---|
| HN Algolia search (discover threads) | https://hn.algolia.com/api/v1/search?... and https://hn.algolia.com/api/v1/search_by_date?tags=story,author_whoishiring&... | Weekly cron — Mondays 03:00 UTC |
| HN Algolia item endpoint | https://hn.algolia.com/api/v1/items/{story_id} | Per discovered thread; re-fetched only for current month |
| Remote OK public feed | https://remoteok.com/api | Daily cron — 06:00 UTC |
| Anthropic Messages API (Haiku 4.5) | https://api.anthropic.com/v1/messages | Batch on ingest, only for titles not in the cache. Optional — without a key, the regex classifier handles normalization. |
No mock data. No Math.random. No seeded counts. Every chart traces back
to one of those four endpoints.
API endpoints
All under /coasean-cuts. No auth.
| Method | Path | Returns |
|---|---|---|
| GET | /coasean-cuts/health | {ok:true} |
| GET | /coasean-cuts/ | SPA shell |
| GET | /coasean-cuts/api/movers | {risers, decliners, generated_at} — top 6 each by YoY delta with 24-month sparkline |
| GET | /coasean-cuts/api/categories | All 22 canonical categories with current-month count, 12m count, YoY % |
| GET | /coasean-cuts/api/categories/:slug | Per-category: 36-month timeseries, top 10 hiring companies, 5 recent postings linked to HN, Remote OK 30d vs prev 30d |
| GET | /coasean-cuts/api/snapshot | JSON metadata for the share card |
| GET | /coasean-cuts/api/snapshot.svg | 1200×630 SVG share card. Accepts ?slug=<category> for per-role cards. Cache-Control: public, max-age=3600 |
| GET | /coasean-cuts/api/ingest-status | Per-source last-run timestamps, totals, fallback %, error string |
| GET | /coasean-cuts/api/search?q=... | Filter the 22-category taxonomy by free-text query |
| GET | /coasean-cuts/api/spark/:slug | 24-month sparkline data for one category |
Run locally
npm install
PORT=4808 node server.js
# → http://localhost:4808/coasean-cuts/
On first boot the server starts immediately and serves /health. Ifthreads is empty (or COASEAN_FORCE_INGEST=1), a backfill kicks off in
the background — usually 30–60s to crawl 36 threads and ~12,000 postings.
To pre-warm the DB before serving traffic:
npm run ingest
node server.js
Environment
| Var | Default | Notes |
|---|---|---|
| PORT | 4808 | |
| OPENROUTER_API_KEY | unset | If set, normalization goes through OpenRouter's anthropic/claude-haiku-4.5 |
| ANTHROPIC_API_KEY | unset | Preferred over OpenRouter when both are set |
| COASEAN_FORCE_INGEST | unset | When 1, runs a full ingest at boot even if data exists |
With no key, the service falls back to a deterministic regex
classifier. Coverage is lower (more rows land in other) but every
endpoint still works.
Methodology caveats
- HN is self-selected — startup, dev-tools, and AI-infra heavy. This is a
- signal of "what shows up in Ask HN: Who is hiring?", not a labor-market
- census.
- Recent months are 30-day windows; small base rates create high-variance
- YoY deltas for newer categories (the "Prompt Engineer +412%" effect).
- No salary parsing (too noisy in HN free text), no LinkedIn (ToS),
- no Glassdoor.
- The 22-category taxonomy is fixed in
lib/taxonomy.js. "Other" exists - as a 23rd bucket but is excluded from the
/api/categoriesenumeration.
Architecture
server.js Express app, mounts /coasean-cuts, starts cron + boot backfill
db.js better-sqlite3 (WAL) bootstrap + ingest_runs helpers
config.js env, paths, source URLs, constants
routes/
api.js JSON endpoints
snapshot.js SVG share card renderer
pages.js /coasean-cuts → index.html
scrapers/
hn.js discoverThreads(), fetchThread(id)
remoteok.js fetchRemoteOk()
parser.js extractTitleLine, extractCompany, regexClassify
workers/
ingest.js full orchestration
normalize.js batched Haiku calls + regex fallback
stats.js materializes monthly_stats, computes movers
lib/
anthropic.js Anthropic + OpenRouter wrapper, JSON-mode prompt
taxonomy.js 22 canonical categories + slugify
cron.js weekly HN sweep, daily Remote OK pull, 30-min stats recompute
public/
index.html SPA shell
app.js hash router (home / category / about), all views
style.css dark theme
sparkline.js inline SVG sparkline + bar chart
Definition of done
The eight checks from SPEC.md §7 all pass against the live server.