Skip to main content

WyrdFold: A Self-Hostable LLM Job-Search Platform

WyrdFold dashboard showing a job-search pipeline with 917 new matches and saved, applied, interviewing, and offer counters
May 2, 20266 min readNext.js, TypeScript, Architecture, REST APIs, Security, Sentry

Overview

WyrdFold is an AI job-search platform I built and run solo, and it's the project where I stopped owning just the frontend and started owning the whole stack. It polls job boards against the roles you're targeting, grades every posting for fit with a two-phase LLM pipeline, and tailors resumes and cover letters for the matches worth pursuing. In three weeks of running it on my own search it scored about 44,000 postings for $167 in LLM spend, a number that only holds because of the cost and quality guardrails wrapped around the model.

It started as a quick script to scrape jobs out of a few boards, and I added one feature at a time until it outgrew the monorepo it was living in and moved into its own repo with full CI: a Next.js web app, a FastAPI matching service, a Postgres schema, and an LLM pipeline running in production.

The problem

Searching for a senior role means scrolling hundreds of near-identical postings, guessing which ones actually fit, and rewriting your resume for every one that does. The signal is real, but it's buried, and digging it out is exactly the kind of repetitive judgment call a language model is good at, as long as you can afford to run it and trust what comes back.

It started life inside this portfolio's monorepo as a job-pipeline experiment, then outgrew its host. It needed a Python service for the matching work, its own deploy cadence, and a database schema that had nothing to do with a marketing site. So I pulled it out into a standalone repo and rebuilt it as a product: source-available, self-hostable, and built so that one person running it on their own Supabase project with their own LLM key gets the whole thing.

WyrdFold dashboard with a New matches counter at 917 and pipeline columns for Saved, Drafts, Ready, Applied, Interviewing, and Offer

The architecture

Three deployables talk to each other through Supabase, and the browser only ever talks to the Next.js app. That app acts as a backend-for-frontend: it proxies to the Python service, which owns the LLM work and the polling. That keeps FastAPI off the public internet and gives me one place to forward auth.

The browser talks only to the Next.js web app on Vercel, which forwards the user's Supabase JWT as a Bearer token to the FastAPI matching service on Railway. FastAPI verifies the token against Supabase's JWKS endpoint, polls job sources, runs the LLM matching pipeline, and reads and writes the Supabase Postgres database. Supabase also provides magic-link auth.

The web app is Next.js 16 on Vercel; the matching service is FastAPI on Railway, shipped as a Docker image. Auth is magic-link only through Supabase, and the database is Postgres with pgvector for embeddings. The web app forwards the user's Supabase JWT to the API as a Bearer token, and the API verifies that token against Supabase's JWKS endpoint instead of just trusting the proxy sitting in front of it. The design system is the same @danieljoffe/shared-ui package this portfolio runs on, pulled from npm, so the two products stay visually consistent without sharing a line of code.

Everything past Supabase degrades gracefully. Brave Search for source discovery, Voyage for embeddings, Twilio for SMS alerts, Sentry for errors: leave any of them unset and that feature quietly switches off or mocks itself. The API even boots with LLM_PROVIDER=mock, so a contributor can run the whole stack without spending a cent, though matching quality is the actual product, so a real key is the point.

The matching pipeline

Grading every posting with a full LLM call is the obvious approach, and it's the wrong one. Most postings are off-target on the title alone, and paying for a deep reasoning pass on a job that was never a fit just burns budget for no signal. The cheapest filter runs first and never touches the model: an embedding-similarity check on the title, pgvector with Voyage embeddings, drops the postings that aren't close before a token is spent. Then matching runs in two phases. A cheap title-triage pass scores thousands of jobs per target and decides which ones are even worth a closer look; only the survivors reach the expensive, in-depth fit grade.

The deep grade scores four axes you can weight yourself: technologies, domain, seniority, and title. What comes back isn't a black-box number. Every job carries a score breakdown by component plus an LLM analysis with its reasoning, the skills it thinks you're missing, and a per-axis read. A job can sit at the top of the list with a plain-English "near-perfect technical match, the only gap is one backend technology that's unlikely to block a frontend-focused role" instead of an unexplained 99.

Expanded job match showing a Score Breakdown for Technologies, Domain skills, and Seniority signals alongside an LLM Analysis with reasoning and a flagged missing skill

The full list view scores every polled posting against your active targets, with score, salary, and location inline, filterable by both. And the targets are first-class: each one is a role profile that new jobs get graded against, so the same posting can rank completely differently for a "Senior Frontend" target and a "Full-Stack" one. There's a third, deeper pass too: open a specific job and the LLM runs a closer analysis, flagging when a role leans toward areas your experience doesn't cover, so you get a quick read on whether it's worth pursuing.

WyrdFold jobs list showing polled postings with fit scores, salaries, and locations, filterable by score and status

Operating an LLM pipeline in production

The hard part of shipping an LLM feature isn't the prompt; it's everything around it: the cost, the reproducibility, and what happens when a generation dies while someone's sitting there waiting on it.

Versioned prompt caching. LLM output gets cached, and the cache key includes a prompt_version. When I change a prompt the version bumps, so every old entry misses by construction and a prompt edit can never quietly serve stale reasoning from the previous wording. Leave the version out of the key and the cache turns into a correctness bug the first day you improve a prompt.

Shadow runs before anything flips. A prompt change doesn't reach users on merge. It ships behind a flag and shadow-runs against the old prompt for at least a week, so I can compare the two on real postings before I flip it. You can't unit-test prompt quality; the only honest evaluation is the live distribution.

Cost guardrails, three layers deep. Per-user budgets (hourly, daily, monthly), a global daily circuit breaker that defers all LLM work once it trips while still ingesting jobs, and provider-side spend caps set just above the global cap as a last backstop. The poller fires a Sentry warning at 80% of the daily cap and an error when the breaker trips, so a cost run-up shows up as an alert instead of a surprise invoice.

A failed generation becomes a row, not a buried log line. Every user-visible async task carries a timeout and writes its error to a column, and the frontend gives you a retry button. A generation that dies server-side and only logs a stack trace leaves the user staring at a spinner forever, so persisting the error and offering a retry is the whole difference between a product and a demo.

Resume tailoring and insights

Tailoring is grounded in a master document that holds your whole work history, versioned, with a health indicator, so a generated resume draws from real experience instead of inventing it. That document gets chunked and embedded by type, skills, outcomes, and roles, so tailoring retrieves the experience that actually matches a given posting instead of stuffing the entire history into the prompt. That grounding matters more than it sounds: an earlier version happily fabricated experience until a screenshot test caught it, which is exactly the kind of bug you don't want surfacing in front of a user mid-application. The profile also flags the gaps worth filling and exports finished documents to DOCX.

WyrdFold profile page showing a Document Health indicator at 98 percent, a Gaps to Fill section, and a versioned Master Document

Settings expose the levers a self-hoster actually cares about: resume style and accent, the score threshold that decides what counts as a match, and SMS notifications through Twilio. Sane defaults, with the knobs there when you want them.

WyrdFold settings showing resume style and accent options, a match score threshold, and SMS notification preferences

The result

The two-phase split is what keeps the bill honest. In three weeks of running it for my own search, the pipeline ingested 14,000 jobs and triaged about 44,000 postings, roughly 2,300 a day, for $167 in total LLM spend. The deep fit-grade costs about $0.04 and 26 seconds a job, so reserving it for the few hundred survivors instead of grading all 44,000 is the difference between $167 and roughly $1,750, plus a few hundred hours of model time. That ratio is the whole reason the pipeline has two phases instead of one.

Most of that $167 was the early version finding its feet. It triaged with a prompt that returned paragraphs of reasoning and deep-graded with no caching, so once I traced where the tokens were actually going, I added cheap pre-gates before the model ever ran, cached the static prefix of the grading prompt, and cut the triage down to a score instead of an essay. That dropped the cost per job by about 76% on the triage and 54% on the deep grade. The two-phase split decides which jobs are worth the model's time; the optimization pass is what made each of those calls cheap.

WyrdFold is live, owned solo, and self-hostable end to end. One person with a Supabase project and a single LLM key gets target-driven discovery, two-phase graded matching with transparent per-axis breakdowns, a tracked pipeline from saved to offer, and resume tailoring grounded in real history. It's source-available under a license that keeps self-hosting genuinely free.

I'll be straight about what's still in flight: source coverage grows board by board, the insights dashboards get richer as a real pipeline builds up history, and the shadow-run evaluation loop is an ongoing practice rather than a finished feature. The cost and observability scaffolding got built first precisely so flying the rest in is safe.

Takeaway

The frontend was never the risky part. What made WyrdFold a full-stack project was all the unglamorous stuff wrapped around the model: a cache key that knows which prompt produced it, a circuit breaker that caps the bill, and a retry path for the generation that fell over while someone was waiting on it. Build those first and the LLM feature is a product; skip them and it's a liability.