惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Y
Y Combinator Blog
GbyAI
GbyAI
爱范儿
爱范儿
H
Hackread – Cybersecurity News, Data Breaches, AI and More
C
Check Point Blog
M
MIT News - Artificial intelligence
量子位
宝玉的分享
宝玉的分享
MongoDB | Blog
MongoDB | Blog
V
Visual Studio Blog
罗磊的独立博客
F
Fortinet All Blogs
美团技术团队
博客园_首页
博客园 - 【当耐特】
L
LangChain Blog
月光博客
月光博客
腾讯CDC
The Cloudflare Blog
D
Docker
博客园 - 聂微东
Stack Overflow Blog
Stack Overflow Blog
WordPress大学
WordPress大学
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
I automated my entire job search with AI — here's the ful...
Sanambir Sin · 2026-04-23 · via DEV Community

Sanambir Singh

I automated my entire job search with AI — here's the full stack I built

Manual job searching is genuinely miserable.

You open LinkedIn. You scroll. You find something promising. You copy-paste
your resume into their portal, tweak the cover letter for 20 minutes, submit,
and hear nothing for two weeks. Then you do it again. And again.

I got tired of it, so I built a tool that does the entire pipeline for me:
search → AI score → tailor resume → generate cover letter → email me the
best matches.

Here's how it works and what I learned building it.


What it actually does

  1. Scrapes jobs from LinkedIn, Indeed, Glassdoor, ZipRecruiter, and Google Jobs simultaneously
  2. Scores each listing against your resume using Gemini AI (0–100% match, with reasoning and a "missing skills" list)
  3. Skips low matches — anything below your threshold (default 75%) gets stored but not processed further
  4. Tailors your resume bullet points to the specific job description
  5. Writes a cover letter personalised to the role and company
  6. Emails you a summary of the best matches with the tailored documents attached

The whole pipeline runs in the background. You set it once, and it finds and
evaluates jobs while you sleep.


The tech stack

Layer Tech
Backend FastAPI (Python 3.11)
Frontend React + TypeScript + Tailwind CSS
Database SQLite via SQLAlchemy ORM
AI Google Gemini API
Job scraping jobspy
Email Resend SMTP
Auth httpOnly cookie sessions + bcrypt
Hosting VPS via Coolify + Cloudflare CDN

Nothing exotic — the interesting parts are in how the pieces connect.


The scraping layer

I'm using jobspy, an open-source Python library that wraps LinkedIn,
Indeed, and others into a single interface. It returns a pandas DataFrame
which I convert straight to dicts:

from jobspy import scrape_jobs

jobs = scrape_jobs(
    site_name=["linkedin", "indeed", "glassdoor"],
    search_term="React Developer",
    location="Remote",
    results_wanted=15,
    hours_old=72,
    linkedin_fetch_description=True,  # full JD, not just snippet
)
linkedin_fetch_description=True is the one flag that matters most  without
it you get a 2-sentence snippet, not the actual job description. It also makes
searches 23x slower because each listing needs a separate HTTP request, but
the AI scoring is useless without the full text.

The AI scoring pipeline
Each job gets sent to Gemini with the user's resume and the full job
description. The prompt asks for a structured JSON response:


prompt = f"""
You are a recruiter. Given this resume and job description, return a JSON
object with exactly these fields:
- score: integer 0-100 (match percentage)
- reasoning: 2-3 sentence explanation
- missing_skills: list of strings (skills in JD not in resume)

Resume:
{resume_text}

Job Description:
{job_description}

Return only valid JSON. No markdown fences.
"""
Gemini is surprisingly good at this. The scores correlate well with what a
human recruiter would say — a senior dev resume against a junior listing gets
95+, the same resume against a role requiring 5 years of Java when you've
never touched it gets 2030.

I added an exponential-backoff retry wrapper because Gemini's free tier hits
rate limits when processing a batch of 15–20 jobs in parallel:


async def call_gemini_with_retry(prompt: str, max_retries: int = 4):
    delay = 2
    for attempt in range(max_retries):
        try:
            response = await model.generate_content_async(prompt)
            return response.text
        except ResourceExhausted:
            if attempt == max_retries - 1:
                raise
            await asyncio.sleep(delay + random.uniform(0, 1))
            delay *= 2
Streaming results with SSE
Waiting 5–8 minutes for a search to finish with no feedback is a terrible UX.
I switched the search endpoint to Server-Sent Events so the frontend sees
jobs appear in real time as each platform finishes:


@router.post("/search")
async def search_jobs(payload: SearchPayload, ...):
    async def event_stream():
        for platform in payload.platforms:
            jobs = await scrape_platform(platform, payload)
            for job in jobs:
                scored = await score_job(job, user.resume_text)
                yield f"data: {json.dumps(scored)}\n\n"
        yield "data: {\"done\": true}\n\n"

    return StreamingResponse(event_stream(), media_type="text/event-stream")
On the React side, the EventSource API processes each job as it arrives
and appends it to the feed — the list builds itself as you watch.

The bug that humbled me
About two days after launching, a user reported seeing someone else's jobs
in their feed.

I panicked.

After two hours of debugging, I found the root cause: Cloudflare was
caching /api/jobs responses by URL, completely ignoring the auth cookie.

The sequence that caused it:

User A logs in, hits /api/jobs  Cloudflare caches the response
User B logs in, hits /api/jobs  Cloudflare serves User A's cached data
It only happened after a cache purge (which I triggered on every deployment)
because the first request after a cold cache got stored. The fix was two lines
on every API response:


response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, private"
response.headers["Vary"]          = "Cookie, Authorization"
Plus a Cloudflare Cache Rule to bypass caching entirely for /api*. The
private directive tells any intermediary (Cloudflare, ISPs, proxies) that
the response is user-specific and must never be shared. The Vary: Cookie
tells it that even if it wanted to cache, responses differ per cookie value.

Lesson: never assume a CDN won't cache your authenticated API endpoints.
Always set cache headers explicitly.

The freemium model
I added a tier system to avoid burning through my Gemini API credits. The
backend is straightforward:


TIER_LIMITS = {"free": 3, "premium": 10}

def check_rate_limit(user: User):
    today = date.today().isoformat()
    if user.last_search_date != today:
        user.daily_searches_used = 0
        user.last_search_date = today
    limit = TIER_LIMITS.get(user.tier or "free", 3)
    if user.daily_searches_used >= limit:
        raise HTTPException(429, f"Daily limit of {limit} searches reached")
    user.daily_searches_used += 1
The reset logic compares ISO date strings  no timezone handling needed for
a daily counter. Admins bypass the check entirely.

What I'd do differently
Use PostgreSQL from day one. SQLite is fine for a solo project but
migrating later is painful. The moment you want concurrent writes or proper
JSON queries, you'll wish you started with Postgres.

Abstract the AI layer earlier. All Gemini calls are currently scattered
across scorer.py, tailor_service.py, etc. with duplicated retry logic. A
single ai_client.py with a clean interface would have saved a lot of
copy-paste.

Add observability before users arrive. I had no logging, no error
tracking, nothing. When the Cloudflare bug hit, I was debugging blind. Even
a free Sentry integration would have surfaced the issue immediately.

Enter fullscreen mode Exit fullscreen mode

Try it / look at the code

The project is open source and free to use:

Live: workfinderx.sanambir.com
GitHub: github.com/Sanambir/AutoJob-Finder
If you're in a job search right now, give it a try. If you're a developer,
the codebase is a reasonable example of a full-stack FastAPI + React app
with AI integration, SSE streaming, and a freemium tier system.

Built with Claude Code

I vibe coded the majority of this with Claude Code
— Anthropic's AI coding assistant that works directly in the terminal and
your editor. Most of the backend routers, the React components, the Gemini
integration, and the Cloudflare debugging were done in conversation with it.

If you're building a side project and haven't tried AI-assisted development
yet, it's worth it. The iteration speed is genuinely different — you spend
more time on product decisions and less time looking up syntax or boilerplate.

Questions or feedback welcome in the comments.