kvcops/JobHunterX

Python

4

104 commits

updated Oct 3, 2026

See the code

See what people are saying

SourceMessageScoreDate

I built JobHunterX — an AI agent that finds suitable jobs, explains the match, and helps you apply (r/SideProject)

​ Job hunting shouldn't mean endlessly scrolling through irrelevant listings and rewriting your resume for every role. So I built JobHunterX — an AI-powered career agent that: \- 🔎 Discovers jobs and checks whether they're real and still open. \- 🎯 Evaluates actual fit, seniority and…

1

Oct 3, 2026

README

JobHunterX — roles that genuinely fit you

Typing SVG

🚀 AI career intelligence: understands your career, finds roles that genuinely fit, explains every match, and prepares honest application material




Upload your resume once. Press search. Go grab a coffee. ☕

JobHunterX reads your resume, finds real jobs that truly fit you, tells you why each one fits (or doesn't), writes an honest one-page resume, a cover letter and a CV for you — and then a browser agent fills the application live, right inside the app, while you watch. Stuck on a CAPTCHA or a login? Just take over, fix it, and hand it back. 🙌

💜 The job-hunting buddy you always wished you had. 💜



📚 What's inside


🇮🇳 Built for Indian job seekers

JobHunterX is made for people like this: 1–3 years of experience, living in South India (Hyderabad / Bengaluru first), who can't move North or abroad for a job.

The real problem it solves: a public LinkedIn / Naukri / Indeed post now gets hundreds to thousands of applicants within days. Recruiters read the first few dozen and filter on years, current company and notice period. With 1–2 years of experience you are filtered out unread — your skills never reach a human. So "apply to more jobs" does not work. Being seen early, on the right channel, does.

What it doesWhy it helps
🏢 Company watchlist — 130 researched companies in Hyderabad, Bengaluru and remote-India (AI startups, product companies, GCCs)Each one has: what real AI work happens there, whether it hires 1–3 year engineers, AmbitionBox / Glassdoor ratings, what employees say, red flags (layoffs, bad reviews), how crowded applying is
⏱️ Checks their own job boards every 4 hours while the app is openNew roles land in the New tab within hours — you apply among the first, not as applicant #1,500
📡 Chance-to-be-seen score next to every fit scoreA fresh post on the company's site = good chance. A 3-week-old LinkedIn post with 300 applicants = low chance. You see this before you spend time
🚫 Mass IT-services recruiters are excluded (TCS, Infosys, Wipro, HCL, Cognizant, Accenture…)Bulk hiring, huge crowds, little real product AI work — marked "Not a fit" with the reason
💰 Indian salaries understood — "12–18 LPA", "₹12L", "1.2 Cr", per monthYour minimum CTC is checked properly (type 14 in the salary box and it means 14 LPA)
⏳ Notice period aware"Immediate joiners only" posts are flagged when your notice is 45+ days
🌍 India onlyJobs outside your country are dropped before scoring, so they never push out Indian roles

[!NOTE] The watchlist covers Hyderabad, Bengaluru and remote-India today. More South Indian cities (Chennai, Kochi, Coimbatore, Vizag…) come next. The research lives in jobhunterx/jobhunterx/watchlist/companies.json.



🗺️ The whole journey

Everything happens in this order — the sidebar is ordered the same way. 👇

flowchart LR
    K(["🔑 Free AI key<br/><sub>first screen</sub>"]) --> A(["📄 Upload resume<br/><sub>once</sub>"])
    A --> B["🙋 Check what<br/>we understood"]
    B --> C["🎯 Your goals<br/><sub>titles · cities · remote · CTC</sub>"]
    C --> D["🔎 Search + watchlist<br/><sub>real live progress</sub>"]
    D --> E["⭐ Pick a job<br/><sub>see why it fits</sub>"]
    E --> F["📝 Documents<br/><sub>resume · letter · CV</sub>"]
    F --> G["🤖 Auto-apply<br/><sub>live in the app</sub>"]
    G -->|"CAPTCHA / login"| H["🙌 You take over"]
    H -->|"give back"| G
    G --> I(["🎉 Applied!<br/><sub>tracked for you</sub>"])

    classDef start fill:#EE6B33,stroke:#c4501d,color:#fff,stroke-width:2px;
    classDef step fill:#fff7f2,stroke:#EE6B33,color:#1f1f24,stroke-width:1.5px;
    classDef agent fill:#1f1f24,stroke:#1f1f24,color:#fff,stroke-width:2px;
    classDef you fill:#fde9b8,stroke:#c4860f,color:#1f1f24,stroke-width:1.5px;
    classDef done fill:#2f9a68,stroke:#21754f,color:#fff,stroke-width:2px;
    class K,A start; class B,C,D,E,F step; class G agent; class H you; class I done;
StepWhat you doWhat JobHunterX does
0️⃣Paste one free AI key — or press Start freeThe first screen links to Google AI Studio / NVIDIA / Groq / Mistral, tests the key and saves it. Start free uses Kilo's free models with no key
1️⃣Upload your resume PDFReads it in the background (with live progress) and builds your profile
2️⃣Check "how we see you"Shows total experience, with and without internships, every role, contact and location
3️⃣Add goalsMany job titles, many cities, remote / hybrid / relocation, current & expected CTC, notice period
4️⃣Press Search nowFinds postings (web + your watchlist companies' own boards), checks they are real and open, reads each one, scores fit and chance to be seen — live
⏱️Nothing — it runs by itselfEvery 4 hours it checks the watchlist companies again; new matches appear in the New tab
5️⃣Open a jobExplains the score (what fits, what's missing, what's unknown), the chance a person reads your application, and what the company is like
6️⃣Press Auto-applyChecks your documents → writes the missing ones → opens the browser agent inside the app
7️⃣Watch (or take over)The agent fills the form; you can stop, take over, continue or finish it yourself
8️⃣Relax 😌The job moves to Applied in your tracker


📸 A tour, step by step

0 · First run: one free AI key 🔑

Free AI key setup
No key yet? Press Start free to use Kilo's free models right away, or get a free key: the screen shows where, tests it with one call and saves it to .env. Placeholder values never count as a key.

1 · Who's searching? 👥

Profile picker
Several people (or personas) can use one install — each profile keeps its own resume, matches, tracker and documents.

2 · Upload your resume — only once 📄

One-time onboarding
A short 5-step setup: resume → about you → goals → pay & availability → first search. Reload any time; it remembers where you were.

3 · "Here's how we see you" 🙋

What we understood
Experience is calculated from your dates — total, professional only, and internships — so the numbers are never guessed.

4 · Search, live 🔎

Live search
Real progress only: the bar and the text move with real counts the server reports — "Web searches: 7 of 15", "Company job boards: 21 of 33", "Jobs analysed: 3 of 20 · reading 'AI Engineer' at …". Nothing is estimated from time.

5 · Why this score? ⭐

Why this score
Click a job and it opens beside the list: hard requirements, the weighted score, requirements, verification and the application kit.

5b · Chance to be seen + what the company is like 📡

Chance to be seen
Every signal is listed with its points: how fresh the post is, which channel it is on, applicant counts (only when the page shows one), email routes, notice period, competition — plus the researched company card.

5c · Companies to watch 🏢

Companies to watch
The researched watchlist for your cities: verdict, competition, early-career hiring, ratings, red flags. "Check for new roles now" shows the check live, step by step.

6 · Honest documents 📝

Documents
Every AI edit is fact-checked against your profile. "What changed" shows each edit — accepted or rejected, and why.

7 · Auto-apply: documents first ✅

Getting documents ready
Before the browser opens, the kit is checked: resume (one page), cover letter and CV. Anything missing is written right then.

8 · Auto-apply: watch it work, live 🤖

Live browser agent
No extra Chrome window — the browser is streamed into the app. Every step is listed in plain words, with what was clicked and typed.

9 · When it needs you 🙌

Needs you
CAPTCHA or login? Take over, click and type right in the view, then press Continue. Progress is saved, so nothing starts over.

10 · Track everything 📊

Tracker
The whole journey in one stage bar, one-click "next stage", and the job beside the list — no sideways scrolling.

11 · Your profile, your settings ⚙️

Profile

Settings — AI providers

Settings — Auto-apply
Turn providers on/off, add keys (loaded from .env), pick a model per task, choose which documents Auto-apply attaches.

12 · Dark mode & phones 🌙📱

Dark theme

Mobile
One click for dark mode. On phones the sidebar becomes a floating bottom bar.


🤖 Auto-apply — the browser agent

Press Auto-apply on any job and three things happen, in this order:

flowchart TD
    S(["▶️ Auto-apply"]) --> K{"📂 Check the kit"}
    K -->|"resume missing?"| R["✍️ Write a one-page resume"]
    K -->|"cover letter missing?"| L["✍️ Write a cover letter"]
    K -->|"CV missing?"| V["✍️ Write the CV"]
    K -->|"all ready"| O
    R --> O
    L --> O
    V --> O
    O["🌐 Open a private browser<br/><sub>streamed into the app</sub>"] --> F["⌨️ Fill the form<br/><sub>step by step</sub>"]
    F -->|"submitted"| A(["🎉 Applied"])
    F -->|"CAPTCHA · login · code"| N["🙌 Needs you"]
    F -->|"⏹ you press Stop"| X["⏸ Stopped — progress saved"]
    N -->|"take over, then Continue"| F
    X -->|"Continue"| F

    classDef go fill:#EE6B33,stroke:#c4501d,color:#fff;
    classDef kit fill:#fff7f2,stroke:#EE6B33,color:#1f1f24;
    classDef run fill:#1f1f24,stroke:#1f1f24,color:#fff;
    classDef wait fill:#fde9b8,stroke:#c4860f,color:#1f1f24;
    classDef ok fill:#2f9a68,stroke:#21754f,color:#fff;
    class S go; class K,R,L,V kit; class O,F run; class N,X wait; class A ok;
ButtonWhat it does
⏹ Stop nowStops at once (it does not wait for the current step). The browser stays open and every step is saved.
✋ Take overPauses the agent. Your clicks, scrolling and typing go straight to the page in the live view.
▶️ Give back to agentThe agent carries on from exactly where you left it.
▶️ ContinueAfter a stop, a CAPTCHA or even an app restart: starts again from the last page, knowing what was already done.
✅ I submitted itYou finished it yourself — the job is marked Applied.
✖️ Close browserCloses the private browser. Your steps stay saved.

Why it feels calm and clean ✨

  • 🪟 No separate Chrome window. The page is streamed with Chrome's own screencast into the Auto-apply tab. (Want a real window for debugging? Settings → Auto-apply → Also show a separate Chrome window.)
  • 🧾 Readable steps. Each step shows the goal in plain words ("Fill in email and phone") with small chips for what was done ("Type … into Email", "Upload Asha_Rao_Resume.pdf"). Repeats are counted (×2) instead of listed again.
  • 🧵 One owner for the browser. The agent, the live view and your clicks all run on one dedicated browser thread, so they never fight over the connection (this fixed the old "navigation timed out" and "duplicate response" errors).
  • 💾 Saved state. Status, the kit, every step and the last page are stored in the database — reload the app, restart it, or switch tabs and pick up right where you were.
  • 🐢 Polite pacing. A small pause between steps looks human and keeps you inside free AI limits (BROWSER_STEP_DELAY_S).
🛡️ Stealth & safety details
  • 🍪 Persistent private profile (data/browser_profile) so cookies build trust over time
  • 🖥️ Normal desktop user-agent, automation flags hidden
  • 🧹 Old/zombie Chrome processes and stale lock files are cleaned before each launch
  • 🔐 Passwords and one-time codes are shown as •••• in the step log
  • 🔑 Optional email OTP reader for verification codes (EMAIL_* settings)


🧠 How matching works

JobHunterX does not just count shared keywords. It understands you, finds and checks real postings, reads each job, and explains whether it genuinely fits.

🔍 The 9 stages of every search (click to open)
#StageWhat happensWhere
1UnderstandThe AI reads your profile into a validated structure: career tracks (with how close adjacent tracks are), realistic titles, skills with evidence (used at work / in projects / only listed), normalized locations. Years of experience are computed from your dates (overlaps merged, internships separate). Every AI claim is checked against your profile — invented skills are dropped.intelligence/candidate.py
2PlanDiverse queries from your own titles and places, anchored on employers' own hiring systems (site: Greenhouse / Lever / Ashby / …).discovery/search.py
3DiscoverWeb search results are treated as leads only. Employer job boards found in results — and every watchlist company's board in your cities — are read via their public APIs. Jobs outside your country are dropped here.discovery/search.py, discovery/ats.py, discovery/watchlist.py
4NormalizeEvery lead becomes one JobPosting: ATS API → schema.org JSON-LD → page text, in that order of trust.discovery/page.py
5DedupeSame job from many places is merged (ATS id, canonical URL, company+title+place, near-identical text); the first-party source wins and all sources are kept.discovery/dedupe.py
6ValidateIs it real, reachable, current, open? Per-field status: verified / inferred / unverified / unknown / failed. Unknown stays unknown.discovery/validate.py
7ExtractThe AI reads the JD into a schema (required vs nice-to-have vs mandatory skills, experience, education, notice period, salary…). A skill is accepted only if it appears in the JD; experience/salary only with a verbatim quote that contains the number. Cached per JD.intelligence/job.py
8MatchHard constraints first (career track, experience gap, seniority, location/work mode, mandatory skills, education, job open, notice, salary, excluded companies). A failed hard constraint caps the score — keyword overlap can never lift an incompatible job. Then weighted components (role, required/preferred skills by evidence strength, experience fit, responsibility overlap, seniority, location).intelligence/matching.py, intelligence/policy.py
9Rank & explainScore 0–100, verdict (strong / good / stretch / weak / incompatible), strengths, gaps, unknowns, the exact reason a job was rejected — plus the separate chance-to-be-seen score.intelligence/reach.py, UI: "Why this score"

[!NOTE] No hardcoded skill or title vocabularies. Skills, titles and career tracks come from the AI (schema-validated and verified against the source text); the code only compares and checks. The only fixed lists are deliberate, researched data: the company watchlist (watchlist/companies.json), the city names it covers, and the mass-recruiter exclusion list. Scoring weights and tolerances live in one tunable place: intelligence/policy.py (override with MATCH_POLICY_JSON).

Example: one candidate, five jobs

Candidate: ~1.5 years professional AI/ML experience, Hyderabad, not relocating.

JobResultWhy
AI Engineer · 0–2 yrs · Python/LLM/RAG · HyderabadStrong matchSame track, experience fits, all required skills demonstrated
Senior AI Engineer · 6+ yrs · same skillsIncompatible"Requires 6+ years; you have ~1.6" — identical skills do not help
Frontend Engineer · React/TypeScriptIncompatibleDifferent career track
AI Engineer · 1–3 yrs · Berlin on-siteDroppedOutside India — removed before scoring, so it never takes a result slot
AI Engineer · mandatory KubernetesIncompatible"Mandatory: Kubernetes — not found in your profile"

This exact scenario runs in the test suite (tests/test_scenarios.py).

📡 Chance to be seen (reach)

Fit answers "am I right for this job?". Reach answers "will a person actually read my application?". It is shown next to fit and never mixed into the fit score. Every signal is shown with its reason:

SignalRaises the chanceLowers the chance
FreshnessPosted in the last 1–3 daysOlder than 2–4 weeks
ChannelCompany's own careers board / ATS; curated boards (Instahyre, Cutshort, Wellfound)Public LinkedIn / Naukri / Indeed listing
ApplicantsPage shows a small countPage shows 50+ / 200+ applicants
Direct routeThe JD gives an email to send your resume to—
Notice period—"Immediate joiners" while your notice is longer
CompanyLesser-known company; known to hire 1–3 yr engineersFamous brand (very crowded); mass IT-services recruiter

The default sort, Best chance, ranks by fit and reach together. You can also sort by Best fit or Least crowded.

📄 Resume vs CV vs cover letter

They are three different documents — JobHunterX makes all three, and Auto-apply checks for them before it starts.

📄 Resume📚 CV✉️ Cover letter
ForOne jobYour whole careerOne job
LengthOne page (text shrinks first, then the least relevant points are trimmed)Kept to one page when possible (roomy layout first, then tighter)Under 300 words
What's in itThe points that matter most for this jobEvery role, project, education, certificates, achievementsLinks your real experience to what the job asks for
Codegeneration/resume.pygeneration/cv.pygeneration/cover_letter.py

🔗 Links stay where they belong: when your resume PDF is read, every link is kept together with the text on it ("Code", "Live demo", "Verify") and the lines around it. So a project keeps its GitHub and demo links, a certificate keeps its credential link, and blog / Kaggle-style profiles go to the header. In the resume and CV they show up as small clickable labels next to the right project or entry — nothing extra, no long raw URLs. You can edit them in Profile.

✅ Nothing invented: every AI-written line is fact-checked (generation/evidence.py). A new number, tool, employer or claim that isn't in your profile is rejected and your own words are kept.



🤖 LLM Models & Provider Architecture

One router (config/llm_router.py) sends every AI call through a task chain: the first usable model answers, and the next one takes over if it is busy, rate limited, turned off or not on your account. The model catalog, chains and free-tier limits live in config/models.py.

graph LR
    subgraph Google["☁️ Google AI Studio"]
        G1["gemma-4-31b-it"]
        G2["gemini-3.5-flash-lite"]
    end
    subgraph Groq["⚡ Groq"]
        GR1["openai/gpt-oss-120b"]
        GR2["moonshotai/kimi-k2-instruct-0905"]
        GR3["qwen/qwen3.6-27b"]
        GR4["openai/gpt-oss-20b"]
    end
    subgraph Mistral["🌀 Mistral"]
        M1["mistral-medium-latest"]
        M2["mistral-small-latest"]
    end
    subgraph Kilo["🆓 Kilo Gateway (no key)"]
        K1["kilo-auto/free"]
        K2["nemotron-3-super-120b:free"]
        K3["laguna-s-2.1:free"]
    end
    subgraph NIM["🟩 NVIDIA NIM"]
        N1["nemotron-nano-3-30b-a3b"]
        N2["glm-5.3-flash"]
        N3["deepseek-v4.1-flash"]
    end
    G2 -->|fallback| K1 -->|fallback| N1 -->|fallback| G1 -->|fallback| GR1 -->|fallback| M1

    classDef google fill:#4285F4,stroke:#1a73e8,color:#fff,stroke-width:2px;
    classDef groq fill:#F55036,stroke:#c9302c,color:#fff,stroke-width:2px;
    classDef mistral fill:#FF7000,stroke:#cc5a00,color:#fff,stroke-width:2px;
    classDef kilo fill:#8B5CF6,stroke:#6d28d9,color:#fff,stroke-width:2px;
    classDef nim fill:#76B900,stroke:#5a8f00,color:#fff,stroke-width:2px;
    class K1,K2,K3 kilo;
    class N1,N2,N3 nim;
    class G1,G2 google;
    class GR1,GR2,GR3,GR4 groq;
    class M1,M2 mistral;

Task chains

Task (Settings label)ChainModel order
Quick tasksfastGemini 3.5 Flash Lite → Kilo (Auto, Nemotron 3 Super, Laguna S) → NIM (Nemotron Nano 3, GPT-OSS 20B) → Gemma 4 31B → Groq GPT-OSS 20B / Qwen → Mistral Small
Matching & analysisreasoningGemini 3.5 Flash Lite → Kilo (Nemotron 3 Super, Auto, Ling Flash, Laguna S, Nemotron Nano Omni) → NIM (GLM 5.3 Flash, DeepSeek V4.1 Flash) → Gemma 4 31B → GPT-OSS 120B → Kimi K2 → Mistral Medium
Resume & letter writingtailoringGemini 3.5 Flash Lite → Kilo (Nemotron 3 Super, Auto) → NIM (DeepSeek V4.1 Flash, GLM 5.3 Flash) → Gemma 4 31B → Kimi K2 → GPT-OSS 120B → Mistral Medium
Resume readingextractionsame as Matching, then Qwen3.6 27B / Qwen3 32B → Mistral Medium
Browser agentbrowserGemini 3.5 Flash Lite → GPT-OSS 120B → Mistral Small (→ Gemma 4 31B)

Kilo comes before NVIDIA NIM on purpose: it needs no key and each Kilo model has its own 200 requests/hour, while NIM's 40 requests/minute is shared by every NIM model on your account. Kilo's free models are "thinking" models; the app turns their thinking off (reasoning: {enabled: false}) — in tests that made them answer in about 1–3 s instead of running out of room before the answer. If a model still returns an empty answer, it gets one retry with more room and is never "rested" for it.

[!WARNING] Privacy: Kilo's free models are marked may train on your prompts. Your resume text and job posts are sent in those prompts. If that matters to you, add any free key (Google, NVIDIA, Groq or Mistral) and turn Kilo off in Settings → AI providers.

Llama models are deliberately not used. In Settings → AI providers you can turn any provider off, add or replace its key, pick the first model for each task, and press Check available models — the app asks each provider's /models endpoint with your key and skips models your account cannot call.

💳 Free-tier limits the router respects

Each model gets its own limiter: requests are spaced to its RPM, a rolling one-minute window keeps it under TPM, and daily RPD / TPD counters stop using it for the day once spent (the chain moves on). A 429 also puts the provider in a short cooldown. Override any number with MODEL_LIMITS_JSON in .env, e.g. MODEL_LIMITS_JSON={"gemini/gemma-4-31b-it": {"rpm": 30, "rpd": 14400}} — limits differ per account and change often.

ProviderModelRPMPer dayTPMNotes
Google AI Studiogemma-4-31b-it151,500 req—Gemma runs on the Gemini API (guide); no system role, so instructions are sent inline
Google AI Studiogemini-3.5-flash-lite15500 req250KFast; primary for the browser agent
Groqopenai/gpt-oss-120b301K req · 200K tok8KReasoning effort set to low
Groqmoonshotai/kimi-k2-instruct-0905601K req · 300K tok10KStrong writing
Groqqwen/qwen3.6-27b · qwen/qwen3-32b30 · 601K req8K · 6KReasoning trace hidden
Groqopenai/gpt-oss-20b301K req · 200K tok8KLight and fast
Mistralmistral-medium-latest50—25KFree plan limits are per account (Admin console → Limits)
Mistralmistral-small-latest50—50K
Mistralmistral-large-latest4—250KVery low request rate on the free plan
Kilo Gatewaykilo-auto/free · nvidia/nemotron-3-super-120b-a12b:free · poolside/laguna-s-2.1:free · inclusionai/ling-3.1-flash · nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free—200 req / hour per model—No key. Measured 1–4 s per answer. May train on prompts
NVIDIA NIMnvidia/nemotron-nano-3-30b-a3b · openai/gpt-oss-20b · z-ai/glm-5.3-flash · deepseek-ai/deepseek-v4.1-flash40 per account——Free key at build.nvidia.com; the 40/min is shared by all NIM models, so the router counts it as one budget

[!NOTE] Why you may see 500 INTERNAL or 503 high demand from Gemma: those come from Google's servers. In live tests (Oct 2026) the free Gemma 4 31B endpoint took 12–120 s per answer and often refused larger prompts, while Gemini 3.5 Flash Lite answered in about 1–5 s. So Flash Lite is now the first choice for every task and Gemma 4 31B is the first backup. When Gemma fails it gets one quick retry, then the app moves on and Gemma rests for 2 minutes (4, 8… up to 30 if it keeps failing). Settings → AI providers shows "resting N min". A Gemma call never waits more than GEMMA_TIMEOUT_S (90 s). Resume upload runs in the background with live progress, so a slow free-tier call never times out the page.



🌐 Job Sources & Verification

SourceTrustHow it is used
Employer ATS APIs — Greenhouse, Lever, Ashby, SmartRecruiters, Recruitee, Workable, Workday (most GCCs), Keka (many Indian product companies)First-party, verifiedStructured postings; re-verified live by job id (a closed job disappears from the board / returns 404)
Company watchlist — 130 researched Hyderabad / Bengaluru / remote-India employersResearched, sources linkedTheir boards are read on every search and every 4 hours in the background; the research card is shown on each of their jobs
schema.org JobPosting on a pageVerified when the hiring organisation's site is the page's site; otherwise inferredTitle, company, location, dates, validThrough, remote eligibility, salary
Other pages (job boards, aggregators)UnverifiedText only; if the page links to a supported ATS posting, that posting is used instead
Web search — TinyFish → Tavily → Exa → Brave → Deep Search (free) → DuckDuckGoLeads onlyNever shown as jobs until resolved by one of the above

🔎 Deep Search — good results without any search key

Paid search APIs like Tavily and Exa do three things one free scraper does not: they ask several indexes, they read the pages, and they rank by meaning. Deep Search (tools/deep_search.py) does the same with free parts:

  1. Ask many engines at once — Bing, Yandex, Google and Brave (through Mullvad's proxies), Yahoo and DuckDuckGo. An engine that keeps failing is rested for 10 minutes instead of slowing every query.
  2. Merge — results are combined with reciprocal-rank fusion (a page several engines agree on rises). Employer job pages get a boost (a single ATS posting 1.6×, a company job board 1.4×, a careers page 1.25×); aggregators are lowered and people-lookup sites (RocketReach, ZoomInfo…) are pushed out.
  3. Open the pages — the best careers / listing pages are opened and mined for links to the employer's own job board and individual postings (Greenhouse, Lever, Ashby, Workday, Keka…). One careers page can become many real postings.
  4. Rank by meaning — the AI scores the top 20 for "is this a real, open posting that matches the search?" (it uses Kilo's free models when you have no key).

Search queries now run 3 at a time, so a full search finishes much faster than one by one.

Search providers are tried in priority order (primary first, DuckDuckGo last) with zero-spend protection (tools/zero_spend.py) and a usage ledger. All fetching of untrusted URLs goes through an SSRF-safe client (discovery/net.py: public addresses only, checked on every redirect, size and time limits).



🎨 Design: fonts & themes

🔤 FontsGeist for text, Instrument Serif for the warm accent word in each heading, Geist Mono for links and numbers — all bundled, no CDN
🎨 ColoursWarm off-white canvas with a soft pastel wash, white cards, black pill buttons, one orange accent
🌙 ThemesLight and dark, one click in the sidebar (or Settings → Appearance)
🎞️ MotionOrbit loader, resume "scan" beam, springy tabs, count-up numbers, self-drawing score rings, live step log. Settings → Appearance → Motion: Full · Reduced · Follow system
🧭 No long scrollingEvery screen fits the window: list ⇄ detail, library ⇄ preview, nav ⇄ settings — panes scroll on their own
♿ AccessibleKeyboard friendly, focus-trapped dialogs, live regions for progress and toasts


⚡ Quickstart

Get the code, then run one command. It does everything for you: installs uv, makes the virtual environment, installs all packages (~30 seconds), creates the settings file, starts the app and opens it in your browser.

git clone https://github.com/kvcops/jobhunterx.git
cd jobhunterx
Your computerRun this
Windowspowershell -ExecutionPolicy Bypass -File start.ps1  — or just double-click start.bat
macOS / Linuxbash start.sh

The first screen asks for a free AI key and shows where to get it. That's it. 🎉

Next time: run the same command (or double-click start.bat again). It skips the setup and just starts the app. Stop the app: press Ctrl + C in that window.

What you need

  • 🌐 Internet for the first setup
  • 🌐 Google Chrome, used by Auto-apply
  • 🔑 One free AI key — Google AI Studio is the easiest (get it here)

You don't need to install Python yourself — uv downloads Python 3.11 if it's missing.

Prefer doing it by hand? Step-by-step commands

① Get the code

git clone https://github.com/kvcops/jobhunterx.git
cd jobhunterx

② Install uv (a fast installer)

uv installs everything in about 30 seconds. Plain pip can take a very long time here.

Windows (PowerShell):

powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

macOS / Linux:

curl -LsSf https://astral.sh/uv/install.sh | sh

Close the terminal and open a new one, then check it works: uv --version

Can't run the installer? pip install uv works too.

③ Make a virtual environment (venv)

A venv is a private folder for this project's packages, so they don't mix with other projects.

uv venv --python 3.11

This creates a .venv folder. If Python 3.11 is missing, uv downloads it.

Now turn the venv on (do this every time you open a new terminal):

SystemCommand
Windows (PowerShell).venv\Scripts\Activate.ps1
Windows (Command Prompt).venv\Scripts\activate.bat
macOS / Linuxsource .venv/bin/activate

You will see (.venv) at the start of the line when it is on.

PowerShell says "running scripts is disabled"? Run this once, then try again: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned

④ Install the packages

uv pip install -r requirements.txt

All versions are fixed on purpose, so this finishes in seconds instead of hours.

⑤ Add your AI key

Easy way: skip this step. When you start the app, the first screen asks for your keys, shows where to get each one for free, tests it, and saves it for you. Or press Start free to use Kilo's free models with no key at all (they may use your prompts for training — see the privacy note above).

Or do it by hand: copy the example file and paste your key after the = sign.

# Windows
copy jobhunterx\.env.example jobhunterx\.env
# macOS / Linux
cp jobhunterx/.env.example jobhunterx/.env
GOOGLE_API_KEY=paste-your-key-here     # free — start with this one
NVIDIA_API_KEY=                        # optional — 40 requests/min, fast small models
GROQ_API_KEY=                          # optional — makes it faster
MISTRAL_API_KEY=                       # optional — backup

[!TIP] Free keys: Google AI Studio · NVIDIA NIM · Groq · Mistral. No key at all? Kilo's free models work out of the box. One key is enough. Two or three make searches much faster, because each free plan allows only a few calls per minute.

⑥ Start

cd jobhunterx
python -m jobhunterx.api.main

Open http://127.0.0.1:8000 and start hunting. 🎯

Next time, you only need: open a terminal in the project folder → turn the venv on (step ③) → cd jobhunterx → python -m jobhunterx.api.main.

Stop the app: press Ctrl + C in the terminal.

🆘 If something goes wrong

ProblemFix
Resume upload keeps loadingNo real AI key. Restart the app — the first screen asks for one. Keys like your_gemini_api_key don't count.
pip install takes foreverUse uv pip install -r requirements.txt (step ④).
uv not foundClose and reopen the terminal after installing it, or use pip install uv.
No module named jobhunterxRun the start command from inside the inner jobhunterx folder (cd jobhunterx), with the venv on.
Port 8000 is busySet PORT=8001 in jobhunterx/.env and open http://127.0.0.1:8001.
Auto-apply can't open a browserInstall Google Chrome.
Searches are slowAdd a second free AI key (Groq). Free plans allow only a few calls per minute each.

⚙️ Handy settings (.env or Settings page)

SettingDefaultWhat it means
APPLY_WITH_COVER_LETTERtrueWrite and attach a cover letter when applying
APPLY_WITH_CVtrueWrite and attach your CV when a form asks for one
BROWSER_MAX_STEPS40Most steps per Auto-apply run
BROWSER_STEP_DELAY_S3Pause between agent steps (seconds)
BROWSER_SHOW_WINDOWfalseAlso open a real Chrome window (debugging only)
MAX_JOBS_PER_SEARCHsee .env.exampleHow many jobs one search analyses
MODEL_LIMITS_JSON—Override any model's free-tier limits


📐 Architecture

jobhunterx/jobhunterx/
├── domain/          # 📦 Data contracts: profile, snapshot, job posting, match, document
├── intelligence/    # 🧠 Understanding + judgement (candidate, job, matching, reach, policy)
├── discovery/       # 🔎 Search, ATS adapters (incl. Workday, Keka), watchlist, page parsing, dedupe, validation, SSRF-safe fetch
├── watchlist/       # 🏢 companies.json — the researched company watchlist (Hyderabad, Bengaluru, remote India)
├── generation/      # 📝 Resume / CV / cover letter + fact-checking + one-page PDF rendering
├── services/        # 🧩 Orchestration: search runs, watchlist watcher, jobs, documents, profiles, auto-apply kit
├── agents/
│   ├── browser_agent.py   # 🤖 Auto-apply session: steps, stop / take over / continue / close
│   ├── browser_worker.py  # 🧵 One long-lived thread that owns the browser
│   └── live_view.py       # 📺 Chrome screencast → the app, your clicks → the page
├── storage.py       # 💾 SQLite: jobs, documents, people, search runs, apply sessions (+ migrations)
├── db_health.py     # 🩺 Check, repair and back up the database on every start
├── api/             # 🔌 FastAPI routes, WebSockets (/ws events, /ws/browser live view)
├── config/          # ⚙️ Settings, model catalog & limits, LLM router, app state
└── web/             # 🎨 Preact + htm UI with bundled fonts — no build step
  • 🚀 One command to run it: start.ps1 / start.bat (Windows) and start.sh (macOS / Linux) set up uv, the venv and packages, check that everything loads, start the app, and open the browser only once it answers.
  • 🔁 One search at a time; every event carries its run id, so old results never leak into a new search. Background watchlist checks never interrupt a search you started.
  • 📶 Real progress only: progress is streamed as real counts (search.progress events), never estimated from time.
  • 💸 Low AI cost: profile understanding and job reading are cached, role-fit is batched, out-of-country jobs are dropped before any AI call, watchlist checks read only new postings (20 per check), and a provider whose key is rejected is skipped instead of retried.
  • 🔒 Secure by default: same-origin CORS, CSRF guard, WebSocket origin checks, SSRF-safe fetching, upload limits, autoescaped templates.


🔌 API Reference

The full contract (REST + WebSocket) is in jobhunterx/docs/API.md.

GroupEndpoints
👤 ProfileGET/PUT /api/profile, POST /api/profile/upload, people: GET/POST /api/people, POST /api/people/{id}/activate
🔑 SetupGET /api/setup (which keys are set), POST /api/setup/test-key (one free call to check a key)
🔎 SearchesPOST /api/searches, GET /api/searches/current, GET /api/searches/{id}, POST /api/searches/{id}/cancel
🏢 WatchlistGET /api/watchlist?scope=mine or scope=all, POST /api/watchlist/check
💼 JobsGET /api/jobs (views incl. fresh; sorts chance · score · reach · recent), GET /api/jobs/{id}, PUT/DELETE /api/jobs/{id}/saved, PATCH /api/jobs/{id}, POST /api/jobs/{id}/verify, POST /api/jobs/{id}/rescore
📝 DocumentsPOST /api/jobs/{id}/documents, POST /api/documents/cv, GET /api/documents, GET /api/documents/{id}/pdf
🤖 Auto-applyPOST /api/jobs/{id}/apply, GET /api/apply/current, POST /api/apply/{job_id}/stop · take-over · release · continue · close · done, WS /ws/browser
⚙️ Systemsettings, providers, models, usage, database health / repair / backup, interventions, reset
📡 Live events (WS /ws)search.run, search.progress (real done / total per stage), search.activity, search.job, watch.status, watch.done, job.updated, document.status, profile.upload, apply.session


🧪 Testing

# with the venv on (see Quickstart)
cd jobhunterx
python -m pytest -q --ignore=tests/frontend      # fast: API, pipeline scenarios, parsers

uv pip install playwright                        # only needed for the browser UI tests
python -m pytest -q tests/frontend
  • tests/test_scenarios.py — the full pipeline on a realistic candidate (fake network + fake AI, including made-up claims that must be rejected)
  • tests/test_api_v2.py — API contract, errors, cancellation, CSRF / WebSocket / SSRF guards
  • tests/frontend/test_ui.py — Playwright against the real app



📜 License

MIT License — Build on it, fork it, make it yours.

Built with ❤️ and an unhealthy amount of caffeine for job seekers who refuse to waste time on repetitive applications.


⭐ Star this repo if JobHunterX saved you from the soul-crushing grind of manual job applications




kvcops/JobHunterX

Python

4

104 commits

updated Oct 3, 2026

See the code

See what people are saying

SourceMessageScoreDate

I built JobHunterX — an AI agent that finds suitable jobs, explains the match, and helps you apply (r/SideProject)

&amp;#x200B; Job hunting shouldn't mean endlessly scrolling through irrelevant listings and rewriting your resume for every role. So I built JobHunterX — an AI-powered career agent that: \- 🔎 Discovers jobs and checks whether they're real and still open. \- 🎯 Evaluates actual fit, seniority and…

1

Oct 3, 2026

README

JobHunterX — roles that genuinely fit you

Typing SVG

🚀 AI career intelligence: understands your career, finds roles that genuinely fit, explains every match, and prepares honest application material




Upload your resume once. Press search. Go grab a coffee. ☕

JobHunterX reads your resume, finds real jobs that truly fit you, tells you why each one fits (or doesn't), writes an honest one-page resume, a cover letter and a CV for you — and then a browser agent fills the application live, right inside the app, while you watch. Stuck on a CAPTCHA or a login? Just take over, fix it, and hand it back. 🙌

💜 The job-hunting buddy you always wished you had. 💜



📚 What's inside


🇮🇳 Built for Indian job seekers

JobHunterX is made for people like this: 1–3 years of experience, living in South India (Hyderabad / Bengaluru first), who can't move North or abroad for a job.

The real problem it solves: a public LinkedIn / Naukri / Indeed post now gets hundreds to thousands of applicants within days. Recruiters read the first few dozen and filter on years, current company and notice period. With 1–2 years of experience you are filtered out unread — your skills never reach a human. So "apply to more jobs" does not work. Being seen early, on the right channel, does.

What it doesWhy it helps
🏢 Company watchlist — 130 researched companies in Hyderabad, Bengaluru and remote-India (AI startups, product companies, GCCs)Each one has: what real AI work happens there, whether it hires 1–3 year engineers, AmbitionBox / Glassdoor ratings, what employees say, red flags (layoffs, bad reviews), how crowded applying is
⏱️ Checks their own job boards every 4 hours while the app is openNew roles land in the New tab within hours — you apply among the first, not as applicant #1,500
📡 Chance-to-be-seen score next to every fit scoreA fresh post on the company's site = good chance. A 3-week-old LinkedIn post with 300 applicants = low chance. You see this before you spend time
🚫 Mass IT-services recruiters are excluded (TCS, Infosys, Wipro, HCL, Cognizant, Accenture…)Bulk hiring, huge crowds, little real product AI work — marked "Not a fit" with the reason
💰 Indian salaries understood — "12–18 LPA", "₹12L", "1.2 Cr", per monthYour minimum CTC is checked properly (type 14 in the salary box and it means 14 LPA)
⏳ Notice period aware"Immediate joiners only" posts are flagged when your notice is 45+ days
🌍 India onlyJobs outside your country are dropped before scoring, so they never push out Indian roles

[!NOTE] The watchlist covers Hyderabad, Bengaluru and remote-India today. More South Indian cities (Chennai, Kochi, Coimbatore, Vizag…) come next. The research lives in jobhunterx/jobhunterx/watchlist/companies.json.



🗺️ The whole journey

Everything happens in this order — the sidebar is ordered the same way. 👇

flowchart LR
    K(["🔑 Free AI key<br/><sub>first screen</sub>"]) --> A(["📄 Upload resume<br/><sub>once</sub>"])
    A --> B["🙋 Check what<br/>we understood"]
    B --> C["🎯 Your goals<br/><sub>titles · cities · remote · CTC</sub>"]
    C --> D["🔎 Search + watchlist<br/><sub>real live progress</sub>"]
    D --> E["⭐ Pick a job<br/><sub>see why it fits</sub>"]
    E --> F["📝 Documents<br/><sub>resume · letter · CV</sub>"]
    F --> G["🤖 Auto-apply<br/><sub>live in the app</sub>"]
    G -->|"CAPTCHA / login"| H["🙌 You take over"]
    H -->|"give back"| G
    G --> I(["🎉 Applied!<br/><sub>tracked for you</sub>"])

    classDef start fill:#EE6B33,stroke:#c4501d,color:#fff,stroke-width:2px;
    classDef step fill:#fff7f2,stroke:#EE6B33,color:#1f1f24,stroke-width:1.5px;
    classDef agent fill:#1f1f24,stroke:#1f1f24,color:#fff,stroke-width:2px;
    classDef you fill:#fde9b8,stroke:#c4860f,color:#1f1f24,stroke-width:1.5px;
    classDef done fill:#2f9a68,stroke:#21754f,color:#fff,stroke-width:2px;
    class K,A start; class B,C,D,E,F step; class G agent; class H you; class I done;
StepWhat you doWhat JobHunterX does
0️⃣Paste one free AI key — or press Start freeThe first screen links to Google AI Studio / NVIDIA / Groq / Mistral, tests the key and saves it. Start free uses Kilo's free models with no key
1️⃣Upload your resume PDFReads it in the background (with live progress) and builds your profile
2️⃣Check "how we see you"Shows total experience, with and without internships, every role, contact and location
3️⃣Add goalsMany job titles, many cities, remote / hybrid / relocation, current & expected CTC, notice period
4️⃣Press Search nowFinds postings (web + your watchlist companies' own boards), checks they are real and open, reads each one, scores fit and chance to be seen — live
⏱️Nothing — it runs by itselfEvery 4 hours it checks the watchlist companies again; new matches appear in the New tab
5️⃣Open a jobExplains the score (what fits, what's missing, what's unknown), the chance a person reads your application, and what the company is like
6️⃣Press Auto-applyChecks your documents → writes the missing ones → opens the browser agent inside the app
7️⃣Watch (or take over)The agent fills the form; you can stop, take over, continue or finish it yourself
8️⃣Relax 😌The job moves to Applied in your tracker


📸 A tour, step by step

0 · First run: one free AI key 🔑

Free AI key setup
No key yet? Press Start free to use Kilo's free models right away, or get a free key: the screen shows where, tests it with one call and saves it to .env. Placeholder values never count as a key.

1 · Who's searching? 👥

Profile picker
Several people (or personas) can use one install — each profile keeps its own resume, matches, tracker and documents.

2 · Upload your resume — only once 📄

One-time onboarding
A short 5-step setup: resume → about you → goals → pay & availability → first search. Reload any time; it remembers where you were.

3 · "Here's how we see you" 🙋

What we understood
Experience is calculated from your dates — total, professional only, and internships — so the numbers are never guessed.

4 · Search, live 🔎

Live search
Real progress only: the bar and the text move with real counts the server reports — "Web searches: 7 of 15", "Company job boards: 21 of 33", "Jobs analysed: 3 of 20 · reading 'AI Engineer' at …". Nothing is estimated from time.

5 · Why this score? ⭐

Why this score
Click a job and it opens beside the list: hard requirements, the weighted score, requirements, verification and the application kit.

5b · Chance to be seen + what the company is like 📡

Chance to be seen
Every signal is listed with its points: how fresh the post is, which channel it is on, applicant counts (only when the page shows one), email routes, notice period, competition — plus the researched company card.

5c · Companies to watch 🏢

Companies to watch
The researched watchlist for your cities: verdict, competition, early-career hiring, ratings, red flags. "Check for new roles now" shows the check live, step by step.

6 · Honest documents 📝

Documents
Every AI edit is fact-checked against your profile. "What changed" shows each edit — accepted or rejected, and why.

7 · Auto-apply: documents first ✅

Getting documents ready
Before the browser opens, the kit is checked: resume (one page), cover letter and CV. Anything missing is written right then.

8 · Auto-apply: watch it work, live 🤖

Live browser agent
No extra Chrome window — the browser is streamed into the app. Every step is listed in plain words, with what was clicked and typed.

9 · When it needs you 🙌

Needs you
CAPTCHA or login? Take over, click and type right in the view, then press Continue. Progress is saved, so nothing starts over.

10 · Track everything 📊

Tracker
The whole journey in one stage bar, one-click "next stage", and the job beside the list — no sideways scrolling.

11 · Your profile, your settings ⚙️

Profile

Settings — AI providers

Settings — Auto-apply
Turn providers on/off, add keys (loaded from .env), pick a model per task, choose which documents Auto-apply attaches.

12 · Dark mode & phones 🌙📱

Dark theme

Mobile
One click for dark mode. On phones the sidebar becomes a floating bottom bar.


🤖 Auto-apply — the browser agent

Press Auto-apply on any job and three things happen, in this order:

flowchart TD
    S(["▶️ Auto-apply"]) --> K{"📂 Check the kit"}
    K -->|"resume missing?"| R["✍️ Write a one-page resume"]
    K -->|"cover letter missing?"| L["✍️ Write a cover letter"]
    K -->|"CV missing?"| V["✍️ Write the CV"]
    K -->|"all ready"| O
    R --> O
    L --> O
    V --> O
    O["🌐 Open a private browser<br/><sub>streamed into the app</sub>"] --> F["⌨️ Fill the form<br/><sub>step by step</sub>"]
    F -->|"submitted"| A(["🎉 Applied"])
    F -->|"CAPTCHA · login · code"| N["🙌 Needs you"]
    F -->|"⏹ you press Stop"| X["⏸ Stopped — progress saved"]
    N -->|"take over, then Continue"| F
    X -->|"Continue"| F

    classDef go fill:#EE6B33,stroke:#c4501d,color:#fff;
    classDef kit fill:#fff7f2,stroke:#EE6B33,color:#1f1f24;
    classDef run fill:#1f1f24,stroke:#1f1f24,color:#fff;
    classDef wait fill:#fde9b8,stroke:#c4860f,color:#1f1f24;
    classDef ok fill:#2f9a68,stroke:#21754f,color:#fff;
    class S go; class K,R,L,V kit; class O,F run; class N,X wait; class A ok;
ButtonWhat it does
⏹ Stop nowStops at once (it does not wait for the current step). The browser stays open and every step is saved.
✋ Take overPauses the agent. Your clicks, scrolling and typing go straight to the page in the live view.
▶️ Give back to agentThe agent carries on from exactly where you left it.
▶️ ContinueAfter a stop, a CAPTCHA or even an app restart: starts again from the last page, knowing what was already done.
✅ I submitted itYou finished it yourself — the job is marked Applied.
✖️ Close browserCloses the private browser. Your steps stay saved.

Why it feels calm and clean ✨

  • 🪟 No separate Chrome window. The page is streamed with Chrome's own screencast into the Auto-apply tab. (Want a real window for debugging? Settings → Auto-apply → Also show a separate Chrome window.)
  • 🧾 Readable steps. Each step shows the goal in plain words ("Fill in email and phone") with small chips for what was done ("Type … into Email", "Upload Asha_Rao_Resume.pdf"). Repeats are counted (×2) instead of listed again.
  • 🧵 One owner for the browser. The agent, the live view and your clicks all run on one dedicated browser thread, so they never fight over the connection (this fixed the old "navigation timed out" and "duplicate response" errors).
  • 💾 Saved state. Status, the kit, every step and the last page are stored in the database — reload the app, restart it, or switch tabs and pick up right where you were.
  • 🐢 Polite pacing. A small pause between steps looks human and keeps you inside free AI limits (BROWSER_STEP_DELAY_S).
🛡️ Stealth & safety details
  • 🍪 Persistent private profile (data/browser_profile) so cookies build trust over time
  • 🖥️ Normal desktop user-agent, automation flags hidden
  • 🧹 Old/zombie Chrome processes and stale lock files are cleaned before each launch
  • 🔐 Passwords and one-time codes are shown as •••• in the step log
  • 🔑 Optional email OTP reader for verification codes (EMAIL_* settings)


🧠 How matching works

JobHunterX does not just count shared keywords. It understands you, finds and checks real postings, reads each job, and explains whether it genuinely fits.

🔍 The 9 stages of every search (click to open)
#StageWhat happensWhere
1UnderstandThe AI reads your profile into a validated structure: career tracks (with how close adjacent tracks are), realistic titles, skills with evidence (used at work / in projects / only listed), normalized locations. Years of experience are computed from your dates (overlaps merged, internships separate). Every AI claim is checked against your profile — invented skills are dropped.intelligence/candidate.py
2PlanDiverse queries from your own titles and places, anchored on employers' own hiring systems (site: Greenhouse / Lever / Ashby / …).discovery/search.py
3DiscoverWeb search results are treated as leads only. Employer job boards found in results — and every watchlist company's board in your cities — are read via their public APIs. Jobs outside your country are dropped here.discovery/search.py, discovery/ats.py, discovery/watchlist.py
4NormalizeEvery lead becomes one JobPosting: ATS API → schema.org JSON-LD → page text, in that order of trust.discovery/page.py
5DedupeSame job from many places is merged (ATS id, canonical URL, company+title+place, near-identical text); the first-party source wins and all sources are kept.discovery/dedupe.py
6ValidateIs it real, reachable, current, open? Per-field status: verified / inferred / unverified / unknown / failed. Unknown stays unknown.discovery/validate.py
7ExtractThe AI reads the JD into a schema (required vs nice-to-have vs mandatory skills, experience, education, notice period, salary…). A skill is accepted only if it appears in the JD; experience/salary only with a verbatim quote that contains the number. Cached per JD.intelligence/job.py
8MatchHard constraints first (career track, experience gap, seniority, location/work mode, mandatory skills, education, job open, notice, salary, excluded companies). A failed hard constraint caps the score — keyword overlap can never lift an incompatible job. Then weighted components (role, required/preferred skills by evidence strength, experience fit, responsibility overlap, seniority, location).intelligence/matching.py, intelligence/policy.py
9Rank & explainScore 0–100, verdict (strong / good / stretch / weak / incompatible), strengths, gaps, unknowns, the exact reason a job was rejected — plus the separate chance-to-be-seen score.intelligence/reach.py, UI: "Why this score"

[!NOTE] No hardcoded skill or title vocabularies. Skills, titles and career tracks come from the AI (schema-validated and verified against the source text); the code only compares and checks. The only fixed lists are deliberate, researched data: the company watchlist (watchlist/companies.json), the city names it covers, and the mass-recruiter exclusion list. Scoring weights and tolerances live in one tunable place: intelligence/policy.py (override with MATCH_POLICY_JSON).

Example: one candidate, five jobs

Candidate: ~1.5 years professional AI/ML experience, Hyderabad, not relocating.

JobResultWhy
AI Engineer · 0–2 yrs · Python/LLM/RAG · HyderabadStrong matchSame track, experience fits, all required skills demonstrated
Senior AI Engineer · 6+ yrs · same skillsIncompatible"Requires 6+ years; you have ~1.6" — identical skills do not help
Frontend Engineer · React/TypeScriptIncompatibleDifferent career track
AI Engineer · 1–3 yrs · Berlin on-siteDroppedOutside India — removed before scoring, so it never takes a result slot
AI Engineer · mandatory KubernetesIncompatible"Mandatory: Kubernetes — not found in your profile"

This exact scenario runs in the test suite (tests/test_scenarios.py).

📡 Chance to be seen (reach)

Fit answers "am I right for this job?". Reach answers "will a person actually read my application?". It is shown next to fit and never mixed into the fit score. Every signal is shown with its reason:

SignalRaises the chanceLowers the chance
FreshnessPosted in the last 1–3 daysOlder than 2–4 weeks
ChannelCompany's own careers board / ATS; curated boards (Instahyre, Cutshort, Wellfound)Public LinkedIn / Naukri / Indeed listing
ApplicantsPage shows a small countPage shows 50+ / 200+ applicants
Direct routeThe JD gives an email to send your resume to—
Notice period—"Immediate joiners" while your notice is longer
CompanyLesser-known company; known to hire 1–3 yr engineersFamous brand (very crowded); mass IT-services recruiter

The default sort, Best chance, ranks by fit and reach together. You can also sort by Best fit or Least crowded.

📄 Resume vs CV vs cover letter

They are three different documents — JobHunterX makes all three, and Auto-apply checks for them before it starts.

📄 Resume📚 CV✉️ Cover letter
ForOne jobYour whole careerOne job
LengthOne page (text shrinks first, then the least relevant points are trimmed)Kept to one page when possible (roomy layout first, then tighter)Under 300 words
What's in itThe points that matter most for this jobEvery role, project, education, certificates, achievementsLinks your real experience to what the job asks for
Codegeneration/resume.pygeneration/cv.pygeneration/cover_letter.py

🔗 Links stay where they belong: when your resume PDF is read, every link is kept together with the text on it ("Code", "Live demo", "Verify") and the lines around it. So a project keeps its GitHub and demo links, a certificate keeps its credential link, and blog / Kaggle-style profiles go to the header. In the resume and CV they show up as small clickable labels next to the right project or entry — nothing extra, no long raw URLs. You can edit them in Profile.

✅ Nothing invented: every AI-written line is fact-checked (generation/evidence.py). A new number, tool, employer or claim that isn't in your profile is rejected and your own words are kept.



🤖 LLM Models & Provider Architecture

One router (config/llm_router.py) sends every AI call through a task chain: the first usable model answers, and the next one takes over if it is busy, rate limited, turned off or not on your account. The model catalog, chains and free-tier limits live in config/models.py.

graph LR
    subgraph Google["☁️ Google AI Studio"]
        G1["gemma-4-31b-it"]
        G2["gemini-3.5-flash-lite"]
    end
    subgraph Groq["⚡ Groq"]
        GR1["openai/gpt-oss-120b"]
        GR2["moonshotai/kimi-k2-instruct-0905"]
        GR3["qwen/qwen3.6-27b"]
        GR4["openai/gpt-oss-20b"]
    end
    subgraph Mistral["🌀 Mistral"]
        M1["mistral-medium-latest"]
        M2["mistral-small-latest"]
    end
    subgraph Kilo["🆓 Kilo Gateway (no key)"]
        K1["kilo-auto/free"]
        K2["nemotron-3-super-120b:free"]
        K3["laguna-s-2.1:free"]
    end
    subgraph NIM["🟩 NVIDIA NIM"]
        N1["nemotron-nano-3-30b-a3b"]
        N2["glm-5.3-flash"]
        N3["deepseek-v4.1-flash"]
    end
    G2 -->|fallback| K1 -->|fallback| N1 -->|fallback| G1 -->|fallback| GR1 -->|fallback| M1

    classDef google fill:#4285F4,stroke:#1a73e8,color:#fff,stroke-width:2px;
    classDef groq fill:#F55036,stroke:#c9302c,color:#fff,stroke-width:2px;
    classDef mistral fill:#FF7000,stroke:#cc5a00,color:#fff,stroke-width:2px;
    classDef kilo fill:#8B5CF6,stroke:#6d28d9,color:#fff,stroke-width:2px;
    classDef nim fill:#76B900,stroke:#5a8f00,color:#fff,stroke-width:2px;
    class K1,K2,K3 kilo;
    class N1,N2,N3 nim;
    class G1,G2 google;
    class GR1,GR2,GR3,GR4 groq;
    class M1,M2 mistral;

Task chains

Task (Settings label)ChainModel order
Quick tasksfastGemini 3.5 Flash Lite → Kilo (Auto, Nemotron 3 Super, Laguna S) → NIM (Nemotron Nano 3, GPT-OSS 20B) → Gemma 4 31B → Groq GPT-OSS 20B / Qwen → Mistral Small
Matching & analysisreasoningGemini 3.5 Flash Lite → Kilo (Nemotron 3 Super, Auto, Ling Flash, Laguna S, Nemotron Nano Omni) → NIM (GLM 5.3 Flash, DeepSeek V4.1 Flash) → Gemma 4 31B → GPT-OSS 120B → Kimi K2 → Mistral Medium
Resume & letter writingtailoringGemini 3.5 Flash Lite → Kilo (Nemotron 3 Super, Auto) → NIM (DeepSeek V4.1 Flash, GLM 5.3 Flash) → Gemma 4 31B → Kimi K2 → GPT-OSS 120B → Mistral Medium
Resume readingextractionsame as Matching, then Qwen3.6 27B / Qwen3 32B → Mistral Medium
Browser agentbrowserGemini 3.5 Flash Lite → GPT-OSS 120B → Mistral Small (→ Gemma 4 31B)

Kilo comes before NVIDIA NIM on purpose: it needs no key and each Kilo model has its own 200 requests/hour, while NIM's 40 requests/minute is shared by every NIM model on your account. Kilo's free models are "thinking" models; the app turns their thinking off (reasoning: {enabled: false}) — in tests that made them answer in about 1–3 s instead of running out of room before the answer. If a model still returns an empty answer, it gets one retry with more room and is never "rested" for it.

[!WARNING] Privacy: Kilo's free models are marked may train on your prompts. Your resume text and job posts are sent in those prompts. If that matters to you, add any free key (Google, NVIDIA, Groq or Mistral) and turn Kilo off in Settings → AI providers.

Llama models are deliberately not used. In Settings → AI providers you can turn any provider off, add or replace its key, pick the first model for each task, and press Check available models — the app asks each provider's /models endpoint with your key and skips models your account cannot call.

💳 Free-tier limits the router respects

Each model gets its own limiter: requests are spaced to its RPM, a rolling one-minute window keeps it under TPM, and daily RPD / TPD counters stop using it for the day once spent (the chain moves on). A 429 also puts the provider in a short cooldown. Override any number with MODEL_LIMITS_JSON in .env, e.g. MODEL_LIMITS_JSON={"gemini/gemma-4-31b-it": {"rpm": 30, "rpd": 14400}} — limits differ per account and change often.

ProviderModelRPMPer dayTPMNotes
Google AI Studiogemma-4-31b-it151,500 req—Gemma runs on the Gemini API (guide); no system role, so instructions are sent inline
Google AI Studiogemini-3.5-flash-lite15500 req250KFast; primary for the browser agent
Groqopenai/gpt-oss-120b301K req · 200K tok8KReasoning effort set to low
Groqmoonshotai/kimi-k2-instruct-0905601K req · 300K tok10KStrong writing
Groqqwen/qwen3.6-27b · qwen/qwen3-32b30 · 601K req8K · 6KReasoning trace hidden
Groqopenai/gpt-oss-20b301K req · 200K tok8KLight and fast
Mistralmistral-medium-latest50—25KFree plan limits are per account (Admin console → Limits)
Mistralmistral-small-latest50—50K
Mistralmistral-large-latest4—250KVery low request rate on the free plan
Kilo Gatewaykilo-auto/free · nvidia/nemotron-3-super-120b-a12b:free · poolside/laguna-s-2.1:free · inclusionai/ling-3.1-flash · nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free—200 req / hour per model—No key. Measured 1–4 s per answer. May train on prompts
NVIDIA NIMnvidia/nemotron-nano-3-30b-a3b · openai/gpt-oss-20b · z-ai/glm-5.3-flash · deepseek-ai/deepseek-v4.1-flash40 per account——Free key at build.nvidia.com; the 40/min is shared by all NIM models, so the router counts it as one budget

[!NOTE] Why you may see 500 INTERNAL or 503 high demand from Gemma: those come from Google's servers. In live tests (Oct 2026) the free Gemma 4 31B endpoint took 12–120 s per answer and often refused larger prompts, while Gemini 3.5 Flash Lite answered in about 1–5 s. So Flash Lite is now the first choice for every task and Gemma 4 31B is the first backup. When Gemma fails it gets one quick retry, then the app moves on and Gemma rests for 2 minutes (4, 8… up to 30 if it keeps failing). Settings → AI providers shows "resting N min". A Gemma call never waits more than GEMMA_TIMEOUT_S (90 s). Resume upload runs in the background with live progress, so a slow free-tier call never times out the page.



🌐 Job Sources & Verification

SourceTrustHow it is used
Employer ATS APIs — Greenhouse, Lever, Ashby, SmartRecruiters, Recruitee, Workable, Workday (most GCCs), Keka (many Indian product companies)First-party, verifiedStructured postings; re-verified live by job id (a closed job disappears from the board / returns 404)
Company watchlist — 130 researched Hyderabad / Bengaluru / remote-India employersResearched, sources linkedTheir boards are read on every search and every 4 hours in the background; the research card is shown on each of their jobs
schema.org JobPosting on a pageVerified when the hiring organisation's site is the page's site; otherwise inferredTitle, company, location, dates, validThrough, remote eligibility, salary
Other pages (job boards, aggregators)UnverifiedText only; if the page links to a supported ATS posting, that posting is used instead
Web search — TinyFish → Tavily → Exa → Brave → Deep Search (free) → DuckDuckGoLeads onlyNever shown as jobs until resolved by one of the above

🔎 Deep Search — good results without any search key

Paid search APIs like Tavily and Exa do three things one free scraper does not: they ask several indexes, they read the pages, and they rank by meaning. Deep Search (tools/deep_search.py) does the same with free parts:

  1. Ask many engines at once — Bing, Yandex, Google and Brave (through Mullvad's proxies), Yahoo and DuckDuckGo. An engine that keeps failing is rested for 10 minutes instead of slowing every query.
  2. Merge — results are combined with reciprocal-rank fusion (a page several engines agree on rises). Employer job pages get a boost (a single ATS posting 1.6×, a company job board 1.4×, a careers page 1.25×); aggregators are lowered and people-lookup sites (RocketReach, ZoomInfo…) are pushed out.
  3. Open the pages — the best careers / listing pages are opened and mined for links to the employer's own job board and individual postings (Greenhouse, Lever, Ashby, Workday, Keka…). One careers page can become many real postings.
  4. Rank by meaning — the AI scores the top 20 for "is this a real, open posting that matches the search?" (it uses Kilo's free models when you have no key).

Search queries now run 3 at a time, so a full search finishes much faster than one by one.

Search providers are tried in priority order (primary first, DuckDuckGo last) with zero-spend protection (tools/zero_spend.py) and a usage ledger. All fetching of untrusted URLs goes through an SSRF-safe client (discovery/net.py: public addresses only, checked on every redirect, size and time limits).



🎨 Design: fonts & themes

🔤 FontsGeist for text, Instrument Serif for the warm accent word in each heading, Geist Mono for links and numbers — all bundled, no CDN
🎨 ColoursWarm off-white canvas with a soft pastel wash, white cards, black pill buttons, one orange accent
🌙 ThemesLight and dark, one click in the sidebar (or Settings → Appearance)
🎞️ MotionOrbit loader, resume "scan" beam, springy tabs, count-up numbers, self-drawing score rings, live step log. Settings → Appearance → Motion: Full · Reduced · Follow system
🧭 No long scrollingEvery screen fits the window: list ⇄ detail, library ⇄ preview, nav ⇄ settings — panes scroll on their own
♿ AccessibleKeyboard friendly, focus-trapped dialogs, live regions for progress and toasts


⚡ Quickstart

Get the code, then run one command. It does everything for you: installs uv, makes the virtual environment, installs all packages (~30 seconds), creates the settings file, starts the app and opens it in your browser.

git clone https://github.com/kvcops/jobhunterx.git
cd jobhunterx
Your computerRun this
Windowspowershell -ExecutionPolicy Bypass -File start.ps1  — or just double-click start.bat
macOS / Linuxbash start.sh

The first screen asks for a free AI key and shows where to get it. That's it. 🎉

Next time: run the same command (or double-click start.bat again). It skips the setup and just starts the app. Stop the app: press Ctrl + C in that window.

What you need

  • 🌐 Internet for the first setup
  • 🌐 Google Chrome, used by Auto-apply
  • 🔑 One free AI key — Google AI Studio is the easiest (get it here)

You don't need to install Python yourself — uv downloads Python 3.11 if it's missing.

Prefer doing it by hand? Step-by-step commands

① Get the code

git clone https://github.com/kvcops/jobhunterx.git
cd jobhunterx

② Install uv (a fast installer)

uv installs everything in about 30 seconds. Plain pip can take a very long time here.

Windows (PowerShell):

powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

macOS / Linux:

curl -LsSf https://astral.sh/uv/install.sh | sh

Close the terminal and open a new one, then check it works: uv --version

Can't run the installer? pip install uv works too.

③ Make a virtual environment (venv)

A venv is a private folder for this project's packages, so they don't mix with other projects.

uv venv --python 3.11

This creates a .venv folder. If Python 3.11 is missing, uv downloads it.

Now turn the venv on (do this every time you open a new terminal):

SystemCommand
Windows (PowerShell).venv\Scripts\Activate.ps1
Windows (Command Prompt).venv\Scripts\activate.bat
macOS / Linuxsource .venv/bin/activate

You will see (.venv) at the start of the line when it is on.

PowerShell says "running scripts is disabled"? Run this once, then try again: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned

④ Install the packages

uv pip install -r requirements.txt

All versions are fixed on purpose, so this finishes in seconds instead of hours.

⑤ Add your AI key

Easy way: skip this step. When you start the app, the first screen asks for your keys, shows where to get each one for free, tests it, and saves it for you. Or press Start free to use Kilo's free models with no key at all (they may use your prompts for training — see the privacy note above).

Or do it by hand: copy the example file and paste your key after the = sign.

# Windows
copy jobhunterx\.env.example jobhunterx\.env
# macOS / Linux
cp jobhunterx/.env.example jobhunterx/.env
GOOGLE_API_KEY=paste-your-key-here     # free — start with this one
NVIDIA_API_KEY=                        # optional — 40 requests/min, fast small models
GROQ_API_KEY=                          # optional — makes it faster
MISTRAL_API_KEY=                       # optional — backup

[!TIP] Free keys: Google AI Studio · NVIDIA NIM · Groq · Mistral. No key at all? Kilo's free models work out of the box. One key is enough. Two or three make searches much faster, because each free plan allows only a few calls per minute.

⑥ Start

cd jobhunterx
python -m jobhunterx.api.main

Open http://127.0.0.1:8000 and start hunting. 🎯

Next time, you only need: open a terminal in the project folder → turn the venv on (step ③) → cd jobhunterx → python -m jobhunterx.api.main.

Stop the app: press Ctrl + C in the terminal.

🆘 If something goes wrong

ProblemFix
Resume upload keeps loadingNo real AI key. Restart the app — the first screen asks for one. Keys like your_gemini_api_key don't count.
pip install takes foreverUse uv pip install -r requirements.txt (step ④).
uv not foundClose and reopen the terminal after installing it, or use pip install uv.
No module named jobhunterxRun the start command from inside the inner jobhunterx folder (cd jobhunterx), with the venv on.
Port 8000 is busySet PORT=8001 in jobhunterx/.env and open http://127.0.0.1:8001.
Auto-apply can't open a browserInstall Google Chrome.
Searches are slowAdd a second free AI key (Groq). Free plans allow only a few calls per minute each.

⚙️ Handy settings (.env or Settings page)

SettingDefaultWhat it means
APPLY_WITH_COVER_LETTERtrueWrite and attach a cover letter when applying
APPLY_WITH_CVtrueWrite and attach your CV when a form asks for one
BROWSER_MAX_STEPS40Most steps per Auto-apply run
BROWSER_STEP_DELAY_S3Pause between agent steps (seconds)
BROWSER_SHOW_WINDOWfalseAlso open a real Chrome window (debugging only)
MAX_JOBS_PER_SEARCHsee .env.exampleHow many jobs one search analyses
MODEL_LIMITS_JSON—Override any model's free-tier limits


📐 Architecture

jobhunterx/jobhunterx/
├── domain/          # 📦 Data contracts: profile, snapshot, job posting, match, document
├── intelligence/    # 🧠 Understanding + judgement (candidate, job, matching, reach, policy)
├── discovery/       # 🔎 Search, ATS adapters (incl. Workday, Keka), watchlist, page parsing, dedupe, validation, SSRF-safe fetch
├── watchlist/       # 🏢 companies.json — the researched company watchlist (Hyderabad, Bengaluru, remote India)
├── generation/      # 📝 Resume / CV / cover letter + fact-checking + one-page PDF rendering
├── services/        # 🧩 Orchestration: search runs, watchlist watcher, jobs, documents, profiles, auto-apply kit
├── agents/
│   ├── browser_agent.py   # 🤖 Auto-apply session: steps, stop / take over / continue / close
│   ├── browser_worker.py  # 🧵 One long-lived thread that owns the browser
│   └── live_view.py       # 📺 Chrome screencast → the app, your clicks → the page
├── storage.py       # 💾 SQLite: jobs, documents, people, search runs, apply sessions (+ migrations)
├── db_health.py     # 🩺 Check, repair and back up the database on every start
├── api/             # 🔌 FastAPI routes, WebSockets (/ws events, /ws/browser live view)
├── config/          # ⚙️ Settings, model catalog & limits, LLM router, app state
└── web/             # 🎨 Preact + htm UI with bundled fonts — no build step
  • 🚀 One command to run it: start.ps1 / start.bat (Windows) and start.sh (macOS / Linux) set up uv, the venv and packages, check that everything loads, start the app, and open the browser only once it answers.
  • 🔁 One search at a time; every event carries its run id, so old results never leak into a new search. Background watchlist checks never interrupt a search you started.
  • 📶 Real progress only: progress is streamed as real counts (search.progress events), never estimated from time.
  • 💸 Low AI cost: profile understanding and job reading are cached, role-fit is batched, out-of-country jobs are dropped before any AI call, watchlist checks read only new postings (20 per check), and a provider whose key is rejected is skipped instead of retried.
  • 🔒 Secure by default: same-origin CORS, CSRF guard, WebSocket origin checks, SSRF-safe fetching, upload limits, autoescaped templates.


🔌 API Reference

The full contract (REST + WebSocket) is in jobhunterx/docs/API.md.

GroupEndpoints
👤 ProfileGET/PUT /api/profile, POST /api/profile/upload, people: GET/POST /api/people, POST /api/people/{id}/activate
🔑 SetupGET /api/setup (which keys are set), POST /api/setup/test-key (one free call to check a key)
🔎 SearchesPOST /api/searches, GET /api/searches/current, GET /api/searches/{id}, POST /api/searches/{id}/cancel
🏢 WatchlistGET /api/watchlist?scope=mine or scope=all, POST /api/watchlist/check
💼 JobsGET /api/jobs (views incl. fresh; sorts chance · score · reach · recent), GET /api/jobs/{id}, PUT/DELETE /api/jobs/{id}/saved, PATCH /api/jobs/{id}, POST /api/jobs/{id}/verify, POST /api/jobs/{id}/rescore
📝 DocumentsPOST /api/jobs/{id}/documents, POST /api/documents/cv, GET /api/documents, GET /api/documents/{id}/pdf
🤖 Auto-applyPOST /api/jobs/{id}/apply, GET /api/apply/current, POST /api/apply/{job_id}/stop · take-over · release · continue · close · done, WS /ws/browser
⚙️ Systemsettings, providers, models, usage, database health / repair / backup, interventions, reset
📡 Live events (WS /ws)search.run, search.progress (real done / total per stage), search.activity, search.job, watch.status, watch.done, job.updated, document.status, profile.upload, apply.session


🧪 Testing

# with the venv on (see Quickstart)
cd jobhunterx
python -m pytest -q --ignore=tests/frontend      # fast: API, pipeline scenarios, parsers

uv pip install playwright                        # only needed for the browser UI tests
python -m pytest -q tests/frontend
  • tests/test_scenarios.py — the full pipeline on a realistic candidate (fake network + fake AI, including made-up claims that must be rejected)
  • tests/test_api_v2.py — API contract, errors, cancellation, CSRF / WebSocket / SSRF guards
  • tests/frontend/test_ui.py — Playwright against the real app



📜 License

MIT License — Build on it, fork it, make it yours.

Built with ❤️ and an unhealthy amount of caffeine for job seekers who refuse to waste time on repetitive applications.


⭐ Star this repo if JobHunterX saved you from the soul-crushing grind of manual job applications




Languages

Python

55.1%

JavaScript

30.2%

CSS

12.9%