Benelux AI Job Scout
An internal intelligence tool that replaced two hours of manual Benelux job-searching with a 20-minute filtered review — by automating collection from 14 free sources and classifying every vacancy on the one signal job boards never surface: language requirements
- Problem — Searching ML/Data/AI roles across Benelux ate ~2 hrs/week; ~38% of relevant-looking postings hide hard Dutch/French/German requirements invisible in previews
- Insight — Language-requirement vocabulary is consistent and learnable, so rule-based classifiers handle 5 of 7 dimensions; an LLM is needed for only ~3% of cases
- Solution — I solo-built the pipeline end-to-end: collect from 14 free sources, classify each vacancy on 7 dimensions + CV-match, in a filter-first React dashboard
- Outcome — Live at ai-job-hunter.ontwrpn.com; 282 vacancies indexed, every one language-labelled (28/30 correct on manual review); weekly search ~2 hrs to ~20 min (−83%), €0/month
The blocker was invisible — buried in paragraph three
Searching ML/Data/AI roles across Belgium, Netherlands, and Luxembourg took roughly two hours every Monday. The deeper problem: ~38% of postings that looked relevant required fluent Dutch, French, or German — a hard blocker that was never in the title or listing preview. It appeared three paragraphs in, flagged by a word like vereist, requis, or Voraussetzung. A candidate without that language spends ten minutes reading a posting they were never eligible for.
Three compounding failures: discovery was fragmented across five boards with no overlap; language requirements were systematically hidden; and with no fit signal, every vacancy looked equally worth investigating. The result was two hours of unfocused browsing with a stale spreadsheet.
The pain was not a lack of job postings. It was the cost of reading the right signal out of the noise
Eight weeks of self-audit before a line of code
Discovery ran through November 2025. I audited my own job-search sessions over eight weeks (average: ~2 hours, ~40 minutes of that on language-blocked roles), surveyed 12 active Benelux sources for API availability and ToS constraints, and manually sampled 50 ML/Data postings from Adzuna BE and NL to characterise the language-requirement vocabulary.
Key findings: Adzuna is the only free source covering Belgium and Netherlands with salary signals; VDAB gates API access behind a signed partner agreement; LinkedIn and Indeed are effectively off-limits for personal tools. Luxembourg has a structural coverage gap — no lu endpoint exists and ADEM publishes no live-vacancy API. The language-blocker vocabulary proved consistent and learnable: vloeiend, vereist, courant, maîtrise, muttersprachlich, C1, B2 for required; een plus, un atout, von Vorteil, nice to have for optional.
~38% of manually sampled postings contained a hard language requirement invisible in the listing preview. The vocabulary was stable enough to classify rule-based — with an LLM needed only for the ~3% ambiguous tail where phrasing like “working language is French” defied regex.
Rule-based first; LLM only for the ambiguous 3%
The classification pipeline covers seven dimensions. Five proved reliably rule-based: job-post language (franc-min trigram model), employment type (multilingual regex), role family (title-pattern matching with fuzzy fallback), seniority (title regex), and skills extraction (50-term gazetteer with aliases). Remote type uses a keyword classifier. Only language requirements escalate to an LLM — when the modifier around a language mention is unclear.
The language-match label has four values: Good (English OK), Maybe (local language preferred), Risk (likely required, phrasing ambiguous), Blocker (explicitly required). On the first production run across 282 vacancies, the LLM was called 8 times — 8 of 1,500 free Gemini daily requests. NLP cost: €0/month. The rule-based logic has run four weeks with no false Blocker labels — the two false positives found in review were Maybe/Risk. A fine-tuned model would be marginally more accurate on edge cases; for a vocabulary-stable domain like HR signal words, deterministic logic is the right default.
Language blockers surfaced before a card is opened — the classifier flags hard requirements that job boards bury in paragraph three
Filter-first dashboard built around the language-match signal
14 free job sources (Adzuna, EURES, Arbeitnow, Remotive, RemoteOK, Jobicy, The Muse, Greenhouse, Lever, Recruitee, SmartRecruiters, Ashby, Workable, HN Who’s-Hiring) feed a daily collect→classify→embed pipeline running in-process via node-cron. Every vacancy is classified on seven dimensions and scored against an uploaded CV using term-overlap matching. The React dashboard surfaces filters as URL parameters — any filtered view is shareable and survives a page refresh.
A persistent language legend sits below the filter bar on every view. The job detail surfaces classification badges and an Apply button above the fold — before the description. Both dark (default) and light themes ship; preference is persisted in localStorage via a single data-theme attribute on the root element. A five-step onboarding tour walks first-time users through the language-badge logic.

Live — 14 sources, 282+ vacancies indexed, filter by language match, role family, seniority, remote type
Dark theme (default)
Light theme
Job detail — Apply above the fold
Language legend — persistent on every view
Weekly search: 2 hours → 20 minutes. Marginal infrastructure cost: €0 — it rides a VPS that was already running
Live since May 2026. Four weeks of early-production data (estimated from tracked sessions, not a rigorous study): weekly job-search session −83% (2 hours → 20 minutes); time on language-blocked roles −95% (40 min → 2 min); time to identify Apply Today candidates −92% (60 min → 5 min). 282 vacancies indexed across Belgium (122), Netherlands (145), and remote Benelux (15). 100% classified with language match. Marginal infrastructure cost: €0 — it rides a VPS that was already running/month — shared Hetzner VPS with self-hosted Coolify and Postgres, LLM on the Gemini/Groq free tiers.
Manual accuracy review on 30 randomly sampled classifications: language match 28/30 correct (two false positives from Dutch in boilerplate legal sections, a known fix queued); employment type 26/30; role family 29/30. Directionally encouraging — not a rigorous study. The dominant saving is in language-requirement filtering: the classifier surfaces the blocker before the card is opened, eliminating the ten-minute reads that were never viable applications.
The most valuable signal is often the one nobody surfaces. Every job board knows about language requirements. Almost none let you filter on it — and the ones that do cover only their own market
What I’d do differently — and what the €0 constraint taught me
Three retrospective notes. First: I built the dashboard before the fit score. For the first four weeks of production, 282 vacancies were ranked only by posting date — not meaningfully different from a job board. A fit score from day one would have made the product useful immediately. Second: the language-blocker classifier was validated against a manual sample of 30 postings; the right validation would have been logging application outcomes from the start to compute real recall/precision. Third: VDAB — the richest source for Belgian vacancies — was deferred because of its partner-agreement requirement. It should have been the first integration attempted.
The €0/month constraint forced better architecture. Rule-based NLP requires no API and debugs predictably when it fails. The in-process node-cron scheduler needs no paid cron service and runs on the same box as the data. The discipline of building under a hard cost ceiling produced more portable, less coupled code than an open budget might have.