Skip to content
All projects
Case study · GenAI · LLM engineering · Full stack

Multi-LLM GenAI Job-Search Agent

A multi-LLM system I designed, built and use every day. It finds relevant roles across the UK and Europe, ranks each one against my CV with a three-model LLM-as-a-judge cascade, tracks every application on a Kanban board, and assists with application forms through a Chrome extension that learns from my edits.

2,000+
live roles, refreshed daily
356
employer career sites tracked
9
ATS integrations
3
LLM providers in one cascade
9
role-specific CVs, matched automatically
140+
automated tests
The problem

Job hunting is a data problem

Relevant roles are spread across hundreds of employer sites and a dozen job boards. Many are duplicates or stale "ghost" adverts, and each suitable role ends in a form with 20 to 60 questions. I wanted a system that does the finding, filtering and form filling, and leaves the judgement calls to me.

How it works

Collect, clean, rank, track, apply

01 · Collect

Sources

9 ATS APIs across 356 employers, plus Reed, Adzuna and LinkedIn, Indeed and Google Jobs through Bright Data.

02 · Clean

Normalise

One schema, fuzzy de-duplication, agency filtering and UK, EU and rest-of-world tiers.

03 · Rank

LLM scoring

A rubric-based fit score, reasoning, skill gaps and a ghost-job flag for every role.

04 · Track

Web app

A Next.js dashboard, a Kanban tracker and daily Telegram reports with the best matches.

05 · Apply

Browser extension

Saves postings, flags duplicates, fills forms from the right CV and drafts written answers.

Runs daily on GitHub Actions, stores data in Supabase Postgres, serves the web app from Vercel, and uses OpenAI, Google Gemini and DeepSeek models.

Technical detail

The engineering behind it

LLM orchestration · Evaluation

1. A three-model LLM-as-a-judge cascade that keeps costs down

Each role is scored from 0 to 100 against my CV by a model acting as a strict recruiter. It follows a written rubric (seniority, required stack, domain, location and right to work) and returns structured JSON: the score, its reasoning, named skill gaps and a ghost-job flag. The design problem was accuracy at thousands of roles a day without paying frontier-model prices for all of them.

Bulk pass
A fast, low-cost model (DeepSeek) scores every new role in batches, which keeps each run inside provider rate limits.
Catching false negatives
Low scores are escalated: Gemini 2.5 Pro re-checks everything under 60 and an OpenAI model re-checks what is still under 35. The higher score is kept, so one model's mistake can't bury a strong match. Target employers are escalated first and a per-run cap limits cost.
Catching false positives
High scores need a second model to agree. The lower score is kept and ghost flags are combined, so a role only stays at the top if two independent models rate it.
Ghost-job detection
The rubric flags stale reposts, vague adverts with no named employer or responsibilities, and roles outside my right to work, and caps their score.
Resilience
A provider fallback chain, rate-limit handling that stops cleanly and resumes on the next run, and API calls that drop a parameter a model rejects and retry instead of failing.

Strong matches also get CV recommendations: the keywords the role asks for that I can genuinely evidence, and where to put them. The right one of my nine role-specific CVs is picked by fixed rules based on role family, seniority and the cloud platform the advert emphasises.

OpenAIGemini 2.5 ProDeepSeekPrompt engineeringStructured outputsModel cascading
Data engineering · Postgres

2. Collecting jobs from 9 ATS platforms and the main job boards

  • ATS detection: each employer's careers URL is classified and routed to the right public API: Greenhouse, Lever, Ashby, SmartRecruiters, Workable, Recruitee, Personio, Workday and Teamtailor.
  • Job boards: Reed, Adzuna across a prioritised list of countries, and LinkedIn, Indeed and Google Jobs results through Bright Data and Apify.
  • Fuzzy de-duplication: two adverts count as one job when employer and title match and either the description is near-identical or the location matches, so a role posted in five towns becomes one row.
  • Quality filters: recruitment agencies are removed so only direct employers remain, and roles are grouped into data science, AI engineering and data analysis.
  • One source of truth: the country table and CV-matching rules are written once in Python and generated into TypeScript for the web app and extension, with tests that fail if the copies drift apart.
PythonREST APIsBright DataSupabaseGitHub Actions
Agentic workflow · Human in the loop

3. A Chrome extension that fills application forms and learns

A Manifest V3 extension adds a floating assistant to job pages. It reads the advert, warns me if the job is already on my tracker, and on an application form fills in four passes:

1 · Rules
Facts only (identity, right to work, notice, salary) plus my work history taken word for word from the CV being attached, whether the form uses numbered sections or one card at a time.
2 · Memory
Answers I've given before, matched on the meaningful words of the question, so differently worded questions reuse the same answer.
3 · LLM draft
The model receives my full CV, the job advert (found automatically even on an application page that no longer shows it) and the page itself, with a separate brief for each type of question.
4 · Ask me
Anything still unanswered becomes a card I fill in once. My edited version is saved and used next time without a model call.

Most of the effort went on the web itself: forms inside shadow roots and cross-origin iframes, React inputs that ignore plain value changes, Workday dropdowns that are buttons rather than select boxes, and attaching the right CV to the upload field.

Chrome MV3Service workerShadow DOMDOM automationPlaywright
Full stack · Product

4. A web app and Kanban tracker on the same data

  • Next.js 14, TypeScript and Tailwind, exported as a static site on Vercel behind Supabase Auth with row-level security, and installable on iPhone and iPad.
  • A jobs explorer split by region and role family, with spreadsheet-style column filters that persist, CSV export and one-click saving to a board.
  • A Kanban tracker with custom boards, drag and drop, activity logs and age badges that count from the date I applied. My earlier application history was migrated in with an import script.
Next.jsReactTypeScriptTailwind CSSSupabase Auth
Guardrails

Built to be trusted

A tool that writes to employers on my behalf has to be reliable before it is clever.

A person in the loop

Nothing is submitted automatically. Every drafted answer is shown for review, and CAPTCHAs are handed back to me rather than bypassed.

No invented facts

Facts come from rules, not the model. Multiple-choice answers must match one of the form's options, and tools I've only used in projects are labelled as such.

Secrets kept out of the page

API keys live only in the extension's background worker. The browser uses the public key, and the database is locked with row-level security.

What I learned: sending only borderline and disputed roles to stronger models gave near frontier-level ranking at a fraction of the cost, and the biggest gain in answer quality came from giving the model the real job advert and my full CV, not from a longer prompt.

The code is in a private repository because it holds personal data. I'm happy to demo it or walk through the code.