← back to gallery

Coasean Cuts

Which tech job titles are vanishing from Ask HN Who-is-hiring, with YoY deltas.

researchhiringlabor-markethacker-newsai-displacementjob-trends
Open product ↗

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

  1. Discovers every monthly "Ask HN: Who is hiring?" thread on HN going back
  2. ~36 months via the public Algolia API.
  3. Pulls every top-level comment in each thread — each comment is one job
  4. posting — and parses out the candidate role title and company from the
  5. conventional Company | Role | Location | Remote/On-site header.
  6. Normalizes each unique title into one of 22 canonical role categories
  7. via Claude Haiku 4.5 (or, if no API key is configured, via a
  8. deterministic regex classifier — that path is what produced the current
  9. public deployment).
  10. Layers in Remote OK's public feed as a daily second-source signal.
  11. Materializes monthly counts and computes year-over-year movers,
  12. surfacing the top 6 risers and top 6 decliners on the home page.
  13. Renders a 1200×630 share card (/api/snapshot.svg) so links pasted into
  14. 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. If
threads 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

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.