Jun – Sep 2026 · Zürich · Personal project
Flatbot
An apartment-hunting bot for Zürich. It watches five rental portals and sends an email the moment a matching flat appears. You still apply by hand, so it never logs in or sends your details anywhere.
- Role
- Solo, design to deploy
- Portals
- 5
- Poll
- 15 min ± 5
- Runs on
- Docker · Raspberry Pi
- Tests
- 174 pytest cases
Why
Good flats in Zürich are gone within hours of being listed. They're spread across Flatfox, Homegate, ImmoScout24, NewHome and Comparis, and each portal has its own API and its own bot protection.
Checking all five every fifteen minutes by hand is a part-time job. I wanted one alert per flat, as soon as it went live, written well enough to act on straight away.
The pipelineone cycle, per portal
- SearchOne adapter per portal
- FilterRooms, rent in CHF, postcode 80xx
- Seen?Constant-time lookup of listing IDs
- DedupSame flat on another portal?
- NotifyClaude writes the email, Resend sends it
- CommitLog to Sheets, then mark seen
A dry-run mode walks the whole pipeline without sending or writing anything, and a seed mode marks the current inventory as seen before going live, so the first real run doesn't flood the inbox.
One client per portalthe lightest client that works
| Portal | Protection | Client |
|---|---|---|
| Flatfox | None, public JSON API | httpx |
| NewHome | Cloudflare challenge | nodriver, then an in-page fetch |
| Comparis | DataDome | nodriver, headful Chrome |
| Homegate | Cloudflare + DataDome | FlareSolverr + nodriver |
| ImmoScout24 | Cloudflare + DataDome | FlareSolverr + nodriver |
Hard parts
Headless Chrome gets caught
DataDome fingerprints headless browsers however the user agent is patched. The fix was headful Chrome driven by nodriver, under an Xvfb virtual display, so it still runs on a Raspberry Pi with no screen attached.
The same flat, listed twice
Agencies post one flat on several portals. A content-addressed match store keys listings by normalised address. When the second URL arrives, it's appended to the existing sheet row instead of sending a second email.
Safe to crash
The seen and match stores are append-only files, written only after the email has gone out. If the process dies between sending and writing, the worst case is one duplicate alert. It can't lose a flat, which is the only failure that matters.
Emails worth reading
Claude writes each subject and body from the listing data and a private profile file. If the API is down, a plain template goes out instead.
Try it
# run the full pipeline without sending or writing anything uv run python -m flatbot --one-shot --dry-run # mark current inventory as seen before going live uv run python -m flatbot --seed # build for arm64 and ship to the Pi ./scripts/ship-to-pi.sh pi@raspberrypi.local
Stack
| Language | Python 3.11, uv |
| Fetching | httpx, FlareSolverr, nodriver (undetected Chrome over CDP) |
| Alerts | Anthropic API for the copy, Resend for delivery, gspread for the Google Sheets log |
| Runtime | Docker and docker-compose, Xvfb, multi-arch image for Raspberry Pi 4/5 |
| Tests | pytest, unit and live integration |