Job Matcher
A self-hosted tool that watches the job boards I follow and scores every new listing against my resume. I use it in my own job hunt.
- Role
- Sole developer
- When
- October 2026
- Stack
- TypeScript, Bun, NestJS, Next.js, Effect, PostgreSQL, Prisma, BullMQ, Docker, Playwright
The problem
Job hunting meant checking many careers pages and job boards by hand, then reading every listing to judge whether it fitted. That didn’t scale.
What I did
Job Matcher re-checks the boards and careers pages I follow on a schedule, drops listings outside my locations, experience level and title keywords before spending anything on AI, then has an LLM score each remaining job against my resume and explain the fit.
- Typed failures. The matching pipeline is written with Effect, so every failure is a typed error and each one is handled on purpose: a rejected API key stops the run, a blocked job page fails only that job, and a Discord outage is only logged.
- Safe, idempotent runs. The API only queues a run and a BullMQ worker does the slow work. The run id doubles as the queue job id, so a double click can’t start a run twice, and a per-user Postgres advisory lock stops runs overlapping.
- Budget-aware by design. Each run reads the scraping plan’s remaining credits and keeps a reserve, scrapes on a self-hosted instance first and falls back to the cloud only for pages that block it, and only uses an LLM endpoint under a per-token price cap.
- Resumable. Scraped pages are cached in Postgres and evaluations are keyed by a hash of the resume, sources, model and requirements, so a rerun picks up where the last one stopped.
- Tested like a product: 50+ test files with a 90% per-file line-coverage gate, Playwright end-to-end tests against fake providers, and CI that boots the full stack and runs the installer before anything merges. It installs with one command on Windows, macOS and Linux.


Outcome
I use it regularly in my own job hunt. It saves me screening every listing by hand, and it has surfaced roles I went on to apply for.