DEV Community

Cover image for I Built a Job-Scraping Bot to Escape Cloud Consulting (And It's Open Source)
Le Beltagy
Le Beltagy

Posted on

I Built a Job-Scraping Bot to Escape Cloud Consulting (And It's Open Source)

I Built a Job-Scraping Bot to Escape Cloud Consulting (And It's Open Source)

From manually copy-pasting cover letters to a Python pipeline that hunts Dutch tech roles while I sleep โ€” the architecture, the failures, and why I need your help.

The Setup

I had applied to 73 jobs in 4 weeks.

Not casual browsing. I mean:

  • Custom cover letters per role
  • CV tweaks for Platform Eng vs SRE vs Cloud Security
  • LinkedIn cold messages to hiring managers
  • Tracking everything in a Notion database

The result? Ghosted by 61. Rejected by 8. Phone screen with 4.

Meanwhile, I was working full-time at Siemens, studying Go for kube-radar, and maintaining a homelab that was literally on fire one Tuesday because a Cilium policy update bricked the cluster network. I was burning out on the process of job searching instead of actually getting better at it.

So I did what any engineer would do: I automated the pain away.

That's how job-digest started โ€” a Python pipeline that scrapes job boards, filters by my exact criteria, ranks roles by match quality, and delivers a daily summary to my Telegram. It didn't get me hired overnight. But it got me interviewed by companies I actually wanted to work for โ€” and it taught me more about web scraping, async Python, and data pipelines than any course.

This is the story of building it. The code is public. I want collaborators. If you're job-hunting in tech and you know Python, read on.


Why This Problem?

Cloud consulting taught me how to build systems. It did not teach me how to sell myself.

When I decided to relocate to Amsterdam ๐Ÿ‡ณ๐Ÿ‡ฑ and target backend/platform roles, I faced three problems no recruiter warns you about:

1. The signal-to-noise ratio is brutal

Job boards are flooded with "Cloud Engineer" roles that are actually MSP helpdesk jobs, "DevOps" roles that mean "you're the only ops person for 40 devs," and "Kubernetes" roles at companies running Docker Compose on a single EC2 instance.

I needed filtering that understood my stack: Go, Kubernetes internals, Cilium/eBPF, Azure + AWS multi-cloud, security compliance (NIS2/DORA).

2. Timing asymmetry

The best roles at Adyen, Booking.com, ASML โ€” they go live on Tuesday morning and have 200 applicants by Thursday afternoon. I was checking boards manually on Sunday evenings. I was always late.

3. Context switching kills deep work

Every 20-minute "let me check LinkedIn" session destroyed my flow state. I wanted one daily digest at 7 AM. Read it on the train. Apply to the top 3 roles. Then close the tab and write Go code.


The Architecture

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Scrapers   โ”‚โ”€โ”€โ”€โ–บโ”‚   Enricher   โ”‚โ”€โ”€โ”€โ–บโ”‚    Ranker    โ”‚โ”€โ”€โ”€โ–บโ”‚   Notifier  โ”‚
โ”‚ (async)     โ”‚    โ”‚ (LLM + rules)โ”‚    โ”‚ (scoring)    โ”‚    โ”‚ (Telegram)  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
     โ”‚                                              โ”‚
     โ–ผ                                              โ–ผ
 LinkedIn Jobs                              SQLite cache
 Indeed                                     (dedup + history)
 Glassdoor
 We Work Remotely
 RemoteOK
Enter fullscreen mode Exit fullscreen mode

Design Decisions

Decision Chose Over Because
Language Python 3.11 + asyncio Go Rapid prototyping, BeautifulSoup/httpx ecosystem, LLM SDKs
Storage SQLite PostgreSQL Zero infra, portable, single file. WAL mode for concurrent reads.
Scraping httpx + BeautifulSoup Selenium/Playwright Faster, lighter, cheaper. JavaScript-rendered boards get headless fallback.
Rate limiting Adaptive backoff (exponential + jitter) Fixed delays Respects robots.txt without being slower than necessary.
LLM enrichment OpenAI GPT-4o-mini Local LLM Cost: ~$0.02 per 100 jobs. Quality worth it for extraction.
Delivery Telegram Bot API Email I live in Telegram. Push notification = instant awareness.
Scheduling cron on homelab RPi GitHub Actions Free, persistent, no cold start. Runs at 06:00 CET daily.

The Pipeline (Simplified)

# 1. COLLECT โ€” async scrapers, one per source
async def scrape_linkedin(query: str, location: str) -> list[RawJob]:
    # httpx + BeautifulSoup
    # Extract: title, company, location, description, salary, url, posted_at
    # Returns list of RawJob dataclasses

# 2. NORMALIZE โ€” unify schema across sources
def normalize(raw: RawJob) -> Job:
    # Map "Amsterdam, North Holland" โ†’ "Amsterdam"
    # Extract seniority: Junior / Mid / Senior / Staff from title
    # Detect remote policy: onsite / hybrid / remote
    # Parse salary ranges from description text (regex + LLM fallback)

# 3. FILTER โ€” hard rules (cheap, deterministic)
def passes_filter(job: Job) -> bool:
    return (
        job.location in TARGET_CITIES  # Amsterdam, Berlin, Dublin, etc.
        and job.seniority in {"Senior", "Staff", "Principal"}
        and any(kw in job.description.lower() for kw in TECH_KEYWORDS)
        and job.company not in BLOCKLIST  # agencies, body shops
    )

# 4. ENRICH โ€” LLM judges fit quality
def enrich_with_llm(job: Job) -> EnrichedJob:
    prompt = f"""
    Role: {job.title} at {job.company}
    Description excerpt: {job.description[:2000]}

    Candidate profile: 6 years cloud/platform, K8s internals (Cilium, Talos),
    Go learner, Azure+AWS, security compliance (NIS2/DORA).
    Target: Platform Engineer or Backend Engineer in Amsterdam/Berlin/Dublin.

    Tasks:
    1. Is this role genuinely backend/platform work (not IT support, not MSP)?
    2. Does the tech stack mention K8s, Go, distributed systems, or cloud-native?
    3. Visa sponsorship likely? (big tech yes, small local shop probably no)
    4. Score 0-100 for fit.
    5. Is it worth applying? yes/no/maybe.
    """
    response = openai.chat.completions.create(...)
    return EnrichedJob(..., llm_score=response.score, verdict=response.verdict)

# 5. RANK โ€” composite score
def score(job: EnrichedJob) -> float:
    return (
        job.llm_score * 0.5 +
        salary_score(job.salary_max) * 0.2 +
        company_tier_score(job.company) * 0.2 +  # Tier 1 = Adyen/Booking/ASML/etc.
        recency_score(job.posted_at) * 0.1        # Fresh postings score higher
    )

# 6. NOTIFY โ€” formatted Telegram message
async def send_digest(jobs: list[EnrichedJob]):
    msg = format_telegram(jobs[:10])  # top 10 only
    await telegram_bot.send_message(chat_id=MY_CHAT, text=msg)
Enter fullscreen mode Exit fullscreen mode

The Telegram Output

๐Ÿ” Daily Job Digest โ€” 2026-07-22
18 new roles | 4 match your filters | Top 3 below

โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
๐Ÿฅ‡ 94 pts โ€” Adyen โ€” Platform Engineer
   ๐Ÿ“ Amsterdam ๐Ÿ‡ณ๐Ÿ‡ฑ  |  ๐Ÿ’ฐ โ‚ฌ90-120K  |  ๐Ÿ  Hybrid
   ๐Ÿ›  Go ยท K8s ยท Kafka ยท Terraform
   ๐Ÿ“ "Build internal primitives for developer platforms..."
   โœ… LLM: "Strong match. Go-heavy platform team. Sponsors HSM."
   ๐Ÿ”— https://careers.adyen.com/jobs/...

๐Ÿฅˆ 88 pts โ€” Booking.com โ€” SRE
   ๐Ÿ“ Amsterdam ๐Ÿ‡ณ๐Ÿ‡ฑ  |  ๐Ÿ’ฐ โ‚ฌ85-110K  |  ๐Ÿ  Hybrid
   ๐Ÿ›  K8s ยท Prometheus ยท Python/Go ยท Cilium
   โœ… LLM: "Excellent infra depth match. Large K8s fleet."

๐Ÿฅ‰ 81 pts โ€” N26 โ€” Platform Engineer
   ๐Ÿ“ Berlin ๐Ÿ‡ฉ๐Ÿ‡ช  |  ๐Ÿ’ฐ โ‚ฌ80-110K  |  ๐Ÿ  Hybrid
   ๐Ÿ›  Go ยท K8s ยท Terraform ยท Microservices
   โœ… LLM: "Good fit. Fintech, EU Blue Card likely."

๐Ÿ“Š Actions: /apply N26  |  /dismiss Adyen  |  /details Booking
Enter fullscreen mode Exit fullscreen mode

5 Failures Before It Worked

Failure 1: LinkedIn Rate-Limited Me in 6 Minutes

My first scraper used a single httpx session with a 2-second delay between requests. I was proud of the backoff. LinkedIn was not impressed.

They returned a 429 after ~30 requests and started serving CAPTCHA pages. I added:

  • Rotating User-Agent headers (from a pool of 50 real browser strings)
  • Proxy rotation via free proxy lists (terrible idea โ€” 80% were dead)
  • Randomized delays (1-5 seconds with jitter)

The fix that actually worked: scrape LinkedIn's public job search JSON endpoint instead of HTML parsing. It returns structured data at /jobs-guest/jobs/api/jobs and is significantly more stable. I also dropped proxy rotation and just respected a 5-second delay. LinkedIn is aggressive but fair.

Lesson: Don't fight the frontend. Find the API the frontend uses.

Failure 2: The LLM Hallucinated a Role at Google That Didn't Exist

I blindly trusted GPT-4o-mini's extraction for the first week. It invented a "Kubernetes Platform Engineer" role at Google Amsterdam with a salary of โ‚ฌ500K. The URL 404'd. I had already told my wife about it.

The fix: every scraped job gets a live URL validation (HEAD request, follow redirects). If the URL doesn't return 200, it's dropped before enrichment. LLMs are pattern matchers, not fact checkers.

Lesson: LLMs are enrichment, not validation. Never trust their facts without verification.

Failure 3: I Applied to the Same Job 4 Times

My SQLite schema initially keyed jobs by (title, company). Then a company reposted the same role with a slightly different title. I applied twice. The second time, the recruiter replied: "You already applied last week."

The fix: content hashing. I now hash the normalized job description (after removing whitespace and dates). Same description = same job, even if the title changes. Deduplication is fuzzy, not exact.

Lesson: Job boards are messy. Exact matching is naive.

Failure 4: Running It on My Laptop Meant It Died When I Closed the Lid

I initially ran the script manually: python main.py, wait 8 minutes, get results. But I kept forgetting. And when I closed my MacBook, the script died.

The fix: a headless Raspberry Pi 4 in my homelab runs a cron job at 06:00 CET. It has a persistent SQLite file on an external SSD. The Pi also runs my DNS (Pi-hole), my monitoring (Prometheus), and now my job search. One device, many duties.

Lesson: Automations that depend on you remembering to run them are not automations.

Failure 5: I Got 18 "Matches" and Felt Overwhelmed

Even with filtering and scoring, getting 18 "good" roles in one digest meant I applied to none. Decision paralysis. I would read all 18, open 12 tabs, and close the laptop exhausted.

The fix: the digest shows top 3 by default. To see 4-10, you reply /more. To see the rest, /all. This tiny UX change meant I actually applied to the top 3 instead of bookmarking 12.

Lesson: More data is not better. More decisions is worse.


Results (So Far)

I won't claim this got me hired. I'm still interviewing. But here's what changed:

Metric Before After
Time spent browsing jobs/day 45 min 5 min reading digest
Applications/week ~8 (sporadic) ~12 (consistent)
Roles matching my stack ~30% of browsed ~85% of digest
Time-to-apply after posting 3-4 days <24 hours
Relevant recruiter conversations/month 1-2 4-5

The real win: mental bandwidth. I stopped treating job search as a thing I do when I'm tired at 10 PM. It became a system that runs without me.

Companies where the pipeline surfaced roles I wouldn't have found manually:

  • A stealth-mode fintech in Amsterdam hiring Go platform engineers (not on LinkedIn, found via We Work Remotely)
  • A CDN company in Berlin rebuilding their edge on Cilium + eBPF (mentioned in a blog post the scraper indexed)
  • A Series B startup in Dublin with ex-Google SREs building a K8s-native security product (literally my exact niche)

The Code

git clone https://github.com/beltagyy/job-digest.git
cd job-digest
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

cp config.example.yaml config.yaml
# Edit: add your OpenAI key, Telegram bot token, target cities, tech keywords
python main.py --run-now
Enter fullscreen mode Exit fullscreen mode

What you get:

  • Modular scrapers (add a new source in ~50 lines)
  • SQLite cache with WAL mode
  • LLM enrichment with prompt templates
  • Telegram bot interface with simple commands
  • Docker support for headless deployment

What you need:

  • Python 3.11+, an OpenAI API key (~$5/month at my volume)
  • A Telegram bot (free via @botfather)
  • A server that runs cron (RPi, VPS, old laptop)

I Need Collaborators

This project helps me, but it's built for my profile and my targets. To make it useful for others, I need help with:

1. More scrapers

  • AngelList / Wellfound (startup roles)
  • Hacker News "Who is Hiring?" (monthly thread parser)
  • Greenhouse / Workday ATS backends (many companies use these and have predictable JSON APIs)
  • Country-specific boards (StepStone.de, Indeed.nl, IrishJobs.ie)

2. Better filtering

  • Visually parse "years of experience" requirements and flag "possibly underqualified" vs "definitely qualified"
  • Detect visa sponsorship mentions from description text (pattern-based, not just LLM)
  • Filter out roles from recruiting agencies vs direct employers

3. Smarter enrichment

  • Local LLM option (Ollama/Llama 3.1) for people who don't want OpenAI costs
  • Company research: pull funding stage, Glassdoor rating, team size from public APIs
  • Salary benchmarking: compare offered range to levels.fyi / Glassdoor data

4. Alternative frontends

  • Discord bot (many communities live there)
  • Email digest (for non-Telegram users)
  • Web dashboard (Streamlit or similar) for browsing history and stats

5. Testing & reliability

  • Scrapers break when job boards change HTML. We need snapshot tests (save HTML, assert extraction still works).
  • Mock LLM responses for unit tests (expensive to hit OpenAI in CI).

If any of this sounds like you โ€” especially if you're job-hunting in EU tech and want a tool that actually understands your stack โ€” open an issue or PR.

Even if you're not technical: try it, tell me what job board you want added, and I'll prioritize it.


3 Lessons That Transfer Beyond Job Searching

1. Automate the boring, not the important

I automated discovery and filtering. I did not automate applying or interviewing. The bot finds roles. I still write every cover letter by hand and research every company before applying. The pipeline handles noise; I handle signal.

2. Your side project's tech stack should teach you something

I chose Python + asyncio because I already knew Python but had never built a production async pipeline. I learned:

  • asyncio.gather for parallel scrapers
  • SQLite WAL mode and connection pooling
  • Prompt engineering for structured LLM output (JSON mode, function calling)
  • Rate limiting and backpressure

If your side project doesn't stretch you, it's just work you don't get paid for.

3. Ship before it's perfect

The first version didn't have LLM enrichment. It was just a list of job titles and URLs sorted by posting date. It was ugly. It still saved me 30 minutes a day.

I added scoring later. Then filtering. Then the Telegram bot. Each increment made it better, but the core value (stop manually browsing job boards) existed on day 1.


What's Next

Feature Status
Core pipeline (scrape โ†’ filter โ†’ notify) โœ… shipped
LLM enrichment โœ… shipped
SQLite dedup + history โœ… shipped
More scrapers (HN, Wellfound, StepStone) ๐Ÿ”„ in progress
Company enrichment (funding, Glassdoor) ๐Ÿ“‹ planned
Web dashboard ๐Ÿ“‹ planned
Resume-to-job matching score ๐Ÿ“‹ planned

**
๐Ÿš€ Try it:** https://github.com/beltagyy/job-digest
**
๐Ÿค Collaborate:** Open an issue with the tag help-wanted or DM me

**

If you're job-hunting right now โ€” what's the most frustrating part? Drop it in the comments. If it's common enough, I'll add it to the pipeline.


TL;DR

  • I applied to 73 jobs in 4 weeks, got exhausted by the process, and built a Python bot (job-digest) to automate job-board scraping, filtering, and ranking.
  • It's a pipeline: async scrapers โ†’ LLM enrichment โ†’ scoring โ†’ daily Telegram digest. Runs on a Raspberry Pi at 6 AM.
  • It cut my daily job-browsing time from 45 minutes to 5, and surfaced roles at companies I actually wanted (Adyen, Booking, N26) that I would have missed manually.
  • Open source and looking for collaborators: github.com/beltagyy/job-digest. I need help with scrapers, filtering rules, local-LLM support, and a web dashboard.

Top comments (0)