Ogłoszenia o pracę to dokumenty marketingowe. Przeczytanie jednego i ocena, czy warto poświęcić mu czas, zajmuje mi 15–20 minut, a większość z nich spędzam na dekodowaniu języka zaprojektowanego po to, żeby dobrze wyglądać, a nie żeby być precyzyjnym. Każdy portal z ofertami optymalizuje pod coś odwrotnego niż to, czego potrzebuję jako kandydat: więcej aplikacji, szybciej, z jak najszerszą siecią. Nic na tym rynku nie optymalizuje pod ochronę mojej uwagi.
Postanowiłem więc zbudować narzędzie, które robi odwrotność tego, co nagradza rynek. Zamiast pomagać mi aplikować do większej liczby ofert — pomagać mi odrzucać więcej ofert, szybciej, z mniejszym nakładem czytania i bez kompromisów wobec moich wartości. Sześć warstw analizy — Triage, Product, Business, Reputation, Values, Fit — przechodzi przez każde ogłoszenie zanim zostanie wydany werdykt. Zero List odrzuca automatycznie, bez połowicznych ocen, bez wymówek „ale brzmi ciekawie". To funkcja, której żaden portal z ofertami pracy nigdy by nie zbudował, bo żaden taki portal nie korzysta na tym, że kandydat widzi mniej ogłoszeń.
Założenie, z którym zaczynałem brzmiało: AI sprawia, że budowanie jest proste — jeden dobry prompt równa się działający produkt. Tyle przynajmniej głosi obietnica marketingowa. Okazuje się natomiast, że złożoność projektu wcale nie znika, ona po prostu się przesuwa. W moim przypadku przeniosła się do system promptu. Co liczy się jako dowód? Jak różnią się stany odrzucenia? Jak nie pozwolić modelowi halucynować pewności siebie? Przeniosła się do warstwy bezpieczeństwa — to narzędzie przechowuje prawdziwe CV i prawdziwą historię ofert — nie mogłem traktować tego lekko. Przeniosła się do nieefektownej infrastruktury, która nigdy nie pojawia się w żadnym demo. Okazuje się, że w pracy z LLM-ami trudność nie leży w samym budowaniu — leży w tym, żeby precyzyjnie określić, czego się właściwie potrzebuje.
Zbudowałem to narzędzie, żeby przetestować założenie — a stało się nawykiem. Odwróciła się kolejność działań: z czytania ogłoszenia i ewentualnego sprawdzenia w narzędziu, na weryfikację ogłoszenia najpierw w narzędziu i czytanie tylko wtedy, jeśli przejdzie analizę. To nie była planowana zmiana, ale wyłoniła się z codziennego użytkowania.
Stack jest celowo minimalny. Flask i SQLite, bo problem nie wymaga więcej. Vanilla JS i Jinja2, bo framework dodałby złożoność bez dodania możliwości. Flask-WTF, Flask-Limiter, blokada konta po nieudanych logowaniach i rejestracja przez token zaproszenia — bo bezpieczeństwo nie powinno być opcjonalne nawet w narzędziu osobistym. Pobieranie ofert z Remote OK, Lever, Greenhouse i RSS — z ochroną przed SSRF przy URL-ach podawanych przez użytkownika. Wdrożone na Hetznerze za Caddy, z gunicorn i systemd.
Zbudowane z uv i Claude Code.
Projekt jest studium przypadku innego sposobu myślenia o produkcie: zaczynania od kryteriów etycznych, a nie od wielkości rynku oraz traktowania jako zalety stwierdzenia „to działa wbrew dominującym graczom".
Job Screener nie ma naturalnego sponsora — żaden rekruter, żaden portal z ofertami, żadna platforma HR nie zyskuje nic na tym, że kandydat aplikuje do mniejszej liczby ofert. I to jest w porządku. To, co naprawdę tutaj testuję: jak wygląda product discovery, gdy myślisz o wyjściu zanim zaczniesz myśleć o wejściu?
Job listings are marketing documents. Reading one and deciding whether it's worth my time costs 15-20 minutes — most of it spent decoding language designed to look good, not to be accurate. Every job board optimizes for the opposite of what I need as a candidate: more applications, faster, cast a wider net. Nothing in this market optimizes for protecting my attention.
I wanted to build a tool that does the inverse of what the market rewards. Instead of helping me apply to more jobs, help me reject more jobs, faster, with less reading, without compromising my values. Six analysis layers — Triage, Product, Business, Reputation, Values, Fit — run against every listing before a verdict is given. A user-defined Zero List rejects automatically, no partial credit, no "but it sounds interesting." That's a feature no job board would ever build, because no job board benefits from a candidate seeing fewer listings.
The assumption I had going in: AI makes building simple — one good prompt equals a working product. That’s the marketing promise at least. But the complexity doesn't disappear, it simply shifts. In my case, it moved into the system prompt. What counts as evidence? How do rejection states differ? How do I keep the model from hallucinating confidence? It moved into security — this tool stores real CVs and real job history — I couldn't treat that lightly. It moved into the unglamorous infrastructure that never shows up in any demo. None of that was where I expected the difficulty to be. All of it turned out to be where the actual work lived.
I built this to test an assumption, but it became a habit instead. The order flipped — from reading a listing and then maybe checking the tool, to running the tool first and only reading the listing if it passed. That shift wasn't designed. It emerged from using it daily.
The stack is intentionally minimal. Flask and SQLite because the problem doesn't need more. Vanilla JS and Jinja2 because a framework would add complexity without adding capability. Flask-WTF, Flask-Limiter, account lockout, and invite-token registration because security shouldn't be optional even in a personal tool. Feed ingestion from Remote OK, Lever, Greenhouse, and RSS — with SSRF protection on user-supplied URLs. Deployed on Hetzner behind Caddy with gunicorn and systemd.
Built with uv and Claude Code.
The project is a case study in a different kind of product thinking: starting from ethical criteria rather than market size, and treating a statement "this works against the dominant players" as a feature, not a liability.
I don’t think Job Screener has natural sponsor — no recruiter, no job board, no HR platform gains anything from a candidate applying to fewer jobs. And that's OK. What I'm really testing here: what does product discovery look like when you think of exit before you think of entry?