Inspiration

Finding an apartment in Hamburg is brutal. Listings on WG-Gesucht and Kleinanzeigen get 100+ applicants within minutes of going live — if you're not in the first wave, you don't get a reply. I spent weeks refreshing tabs during my internship search before realizing a bot could do it better, faster, and at 3am.

What it does

A renter fills in their budget, move-in window, preferred districts, and a short profile. The bot then:

  • logs into WG-Gesucht with a saved session
  • crawls fresh WG-Gesucht and Kleinanzeigen listings every 10 minutes
  • applies deterministic hard filters (rent, dates, district, recency)
  • sends survivors to OpenAI for fit-scoring and scam detection
  • pushes only the keepers to Telegram with pros, cons, and a recommendation score

The result: one low-noise feed instead of manually refreshing five tabs.

How we built it

  • Python 3.11+ as the runtime
  • Playwright (Chromium) for browser automation — needed because WG-Gesucht's filters are JS-driven dropdowns, not URL params
  • OpenAI API for structured listing analysis against a renter profile prompt
  • Telegram Bot API for instant push delivery
  • Streamlit for a no-code config editor so non-technical users can tune filters
  • TOML + dotenv to cleanly split user preferences from secrets
  • Cron on a Linux VPS for 10-minute polling
def process(listings, seen):
    for listing in listings:
        if listing.id in seen: continue
        if not run_checks(listing).passed: continue
        analysis = analyze(listing, scrape_details(listing.url))
        send(format_listing_with_ai(listing, analysis))

Challenges we ran into

  • Session expiry: WG-Gesucht silently invalidates Playwright storage state every few days. Solved with a headless ensure_session() check + auto-relogin using saved credentials.
  • Crash-safe dedupe: Early versions re-spammed users when a crash happened mid-run. Fixed by persisting seen IDs before the AI/Telegram step.
  • District filter false negatives: WG-Gesucht's card UI sometimes shows only a street name, not a district. Documented as a known limitation rather than over-engineering a geocoder.
  • Rate limits: OpenAI calls capped per run and parallelised via ThreadPoolExecutor.

Accomplishments that we're proud of

  • This is exactly how I found my Hamburg internship apartment.
  • One unified pipeline across two completely different listing sites.
  • Drop-in Streamlit UI so someone non-technical can run it.
  • Zero false re-notifications since the dedupe rewrite.

What we learned

  • Browser automation beats API scraping when sites lock things down.
  • Splitting deterministic filters from AI scoring keeps the bot useful even when the AI is off or down.
  • Persistence ordering matters more than retry logic.

What's next

  • Score-threshold gate (only notify if recommendation_score ≥ N)
  • AI-drafted application messages with a Telegram approve/edit button
  • Berlin and Munich support

Built With

Share this project:

Updates