← Sol Thiessen

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.

Source on GitHub ↗

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

  1. SearchOne adapter per portal
  2. FilterRooms, rent in CHF, postcode 80xx
  3. Seen?Constant-time lookup of listing IDs
  4. DedupSame flat on another portal?
  5. NotifyClaude writes the email, Resend sends it
  6. 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

PortalProtectionClient
FlatfoxNone, public JSON APIhttpx
NewHomeCloudflare challengenodriver, then an in-page fetch
ComparisDataDomenodriver, headful Chrome
HomegateCloudflare + DataDomeFlareSolverr + nodriver
ImmoScout24Cloudflare + DataDomeFlareSolverr + nodriver

Hard parts

A

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.

B

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.

C

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.

D

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

LanguagePython 3.11, uv
Fetchinghttpx, FlareSolverr, nodriver (undetected Chrome over CDP)
AlertsAnthropic API for the copy, Resend for delivery, gspread for the Google Sheets log
RuntimeDocker and docker-compose, Xvfb, multi-arch image for Raspberry Pi 4/5
Testspytest, unit and live integration
© 2026 Sol ThiessenBack to the start