Telegram bot that scans Amsterdam rental listings and sends a message when a new listing matches a user's filters.
Supported sources:
- Pararius
- Funda
- Kamernet
- Huurwoningen
- VVA
- Roofz
The bot stores user filters and already-seen listings in SQLite, so duplicate listings are not sent twice.
- Runs every enabled platform on the same fast scan cadence
- Checks both the Amsterdam Pararius search page and Pararius' public newest-rentals feed
- Lets each Telegram user save their own Kamernet property types, rent, bedroom/room, and surface-area filters
- Sends new listings directly in Telegram
- Can automatically reply to new matching Kamernet listings when explicitly enabled
- Supports an on-demand scan with
/test
- Python 3.13.7, managed by
uv uv0.8.15 for local development- A Telegram bot token from BotFather
If you already have the folder locally, just open it in VS Code or your terminal.
Use uv from the project root. The lockfile is part of the supply-chain protection for this bot, so install with --locked:
uv sync --lockedActivate the virtual environment if you want to run commands manually:
Windows PowerShell:
.\.venv\Scripts\Activate.ps1macOS/Linux:
source .venv/bin/activateRoofz requires browser automation. Run this once after installing dependencies:
# For Roofz (Playwright)
playwright install chromiumCreate a .env file in the project root with the following content:
TELEGRAM_TOKEN=123456789:replace-with-your-real-token
FAST_POLL_INTERVAL_SECONDS=60
SCRAPER_TIMEOUT_SECONDS=45
KAMERNET_AUTOREPLY_TIMEOUT_SECONDS=45
KAMERNET_AUTOREPLY_MAX_PER_SCAN=2
KAMERNET_MAX_PAGES_PER_SCAN=3
KAMERNET_AUTOREPLY_STORAGE_STATE_PATH=
KAMERNET_AUTOREPLY_EMAIL=
KAMERNET_AUTOREPLY_PASSWORD=
KAMERNET_AUTOREPLY_HEADLESS=true
KAMERNET_AUTOREPLY_DRY_RUN=false
PARARIUS_SCRAPER_TIMEOUT_SECONDS=20
FUNDA_SCRAPER_TIMEOUT_SECONDS=25
FUNDA_PYFUNDA_TIMEOUT_SECONDS=12
FUNDA_PYFUNDA_MAX_RETRIES=2
FUNDA_PYFUNDA_RETRY_BACKOFF_SECONDS=0.1
FUNDA_MAX_BACKGROUND_THREADS=1
ROOFZ_SCRAPER_TIMEOUT_SECONDS=90
ROOFZ_ENABLED=true
VVA_MAX_PAGES_PER_SCAN=1
MAX_CONCURRENT_USERS_PER_JOB=3
DB_PATH=listings.db
SQLITE_BUSY_TIMEOUT_MS=5000
TELEGRAM_ALLOWED_CHAT_IDS=123456789Environment variables:
TELEGRAM_TOKEN: required, Telegram bot token from BotFatherFAST_POLL_INTERVAL_SECONDS: optional, scan interval for every enabled platform, defaults toPARARIUS_POLL_INTERVAL_SECONDSwhen set, otherwise the lower ofPOLL_INTERVAL_SECONDSand60POLL_INTERVAL_SECONDS: optional, legacy fallback used only to deriveFAST_POLL_INTERVAL_SECONDS, defaults to300PARARIUS_POLL_INTERVAL_SECONDS: optional, legacy fallback forFAST_POLL_INTERVAL_SECONDSROOFZ_POLL_INTERVAL_SECONDS: deprecated; Roofz now uses the same fast interval as every other enabled platformSCRAPER_TIMEOUT_SECONDS: optional, timeout for general scrapers, defaults to45KAMERNET_AUTOREPLY_TIMEOUT_SECONDS: optional, timeout for Kamernet reply browser actions, defaults to45KAMERNET_AUTOREPLY_MAX_PER_SCAN: optional, max automatic Kamernet replies per scan per user, defaults to2KAMERNET_MAX_PAGES_PER_SCAN: optional, max Kamernet search pages fetched per scan, defaults to3KAMERNET_AUTOREPLY_STORAGE_STATE_PATH: optional, Playwright login-session file for Kamernet. If unset, it defaults tokamernet_storage_state.jsonlocally, or next toDB_PATHwhenDB_PATHincludes a directory.KAMERNET_AUTOREPLY_EMAIL: optional, Kamernet email used to log in if no valid storage state is availableKAMERNET_AUTOREPLY_PASSWORD: optional, Kamernet password used withKAMERNET_AUTOREPLY_EMAILKAMERNET_AUTOREPLY_HEADLESS: optional, set tofalseto show the browser while debugging, defaults totrueKAMERNET_AUTOREPLY_DRY_RUN: optional, records auto-reply attempts without submitting them, defaults tofalsePARARIUS_SCRAPER_TIMEOUT_SECONDS: optional, timeout for Pararius, defaults to20FUNDA_SCRAPER_TIMEOUT_SECONDS: optional, outer timeout for Funda scans, defaults to25FUNDA_PYFUNDA_TIMEOUT_SECONDS: optional, timeout passed topyfunda, defaults to12FUNDA_PYFUNDA_MAX_RETRIES: optional, retry count passed topyfunda, defaults to2FUNDA_PYFUNDA_RETRY_BACKOFF_SECONDS: optional, retry backoff passed topyfunda, defaults to0.1FUNDA_MAX_BACKGROUND_THREADS: optional, max activepyfundaworker threads, defaults to1ROOFZ_SCRAPER_TIMEOUT_SECONDS: optional, timeout for Roofz browser automation, defaults to90ROOFZ_ENABLED: optional, set tofalseto disable Roofz, defaults totrueVVA_MAX_PAGES_PER_SCAN: optional, max VVA result pages fetched per scan, defaults to1MAX_CONCURRENT_USERS_PER_JOB: optional, active users scanned in parallel per scheduled job, defaults to3DB_PATH: optional, SQLite database path, defaults tolistings.dbSQLITE_BUSY_TIMEOUT_MS: optional, SQLite lock wait timeout, defaults to5000TELEGRAM_ALLOWED_CHAT_IDS: optional, comma-separated Telegram chat IDs allowed to use the bot. Leave empty for local unrestricted use.
python main.pyExpected startup message:
Bot started. Press Ctrl+C to stop.
On first boot the bot automatically creates the SQLite database and its tables.
- Open your bot in Telegram.
- Send
/start. - Send
/searchto configure:- Kamernet property types. Tap one or more types, then tap
Done. - max monthly rent
- minimum bedrooms/rooms
- minimum surface area in square meters
- Kamernet property types. Tap one or more types, then tap
- Send
/testto trigger an immediate scan.
After that, the scheduled scanner will keep running in the background while the process stays alive.
/start- initialize the bot and show help/help- show available commands and options/commands- same as/help/search- save or update filters/filters- show current filters/test- run a scan immediately/autoreply_on- automatically reply to new matching Kamernet listings/autoreply_off- disable Kamernet auto-reply/autoreply_status- show Kamernet auto-reply state and attempt counts/autoreply_template- show or set the Kamernet reply text/pause- pause notifications/resume- resume notifications/clear- clear the seen listings database/cancel- cancel the filter setup flow
/search options:
- Kamernet property types:
Any property type,Room,Apartment,Studio,Anti-squat,Student Housing,Furnished,Short Term,Long Term - Max rent: number in EUR, or
0for no limit - Minimum bedrooms/rooms: number, or
0for no minimum - Minimum size: number in m2, or
0for no minimum
Kamernet auto-reply is disabled by default and only runs after /autoreply_on.
The reply loop only fires for listings that already passed your saved filters and were deduplicated as new for your Telegram user. It also keeps a separate kamernet_auto_replies table so the same listing is not auto-replied twice.
Set your message with:
/autoreply_template Hello, I am interested in this place and would like to schedule a viewing. Kind regards
Optional placeholders are available in the template: {title}, {price}, {address}, {url}, {city}, {rooms}, and {size}.
The auto-reply browser needs a normal logged-in Kamernet session. You can either provide KAMERNET_AUTOREPLY_EMAIL and KAMERNET_AUTOREPLY_PASSWORD in .env, or point KAMERNET_AUTOREPLY_STORAGE_STATE_PATH at an existing Playwright storage-state file. On the VPS, leave KAMERNET_AUTOREPLY_STORAGE_STATE_PATH unset unless you need a custom path, so the saved session lives beside the production database and survives deploys. If Kamernet asks for human verification, login, or payment/premium access, the bot stops that reply attempt and sends a Telegram failure notice instead of trying to bypass it.
main.pystarts the Telegram application.bot.pyregisters commands and schedules the recurring scan job.scanner.pyruns selected scrapers for each active user, with all enabled source groups scheduled on the same fast cadence.db.pystores filters and deduplicates listings in SQLite.
.
|-- bot.py
|-- config.py
|-- db.py
|-- kamernet_autoreply.py
|-- main.py
|-- pyproject.toml
|-- scanner.py
|-- scrapers/
| |-- base.py
| |-- funda.py
| |-- http_clients.py
| |-- huurwoningen.py
| |-- kamernet.py
| |-- pararius.py
| |-- roofz.py
| `-- vva.py
`-- uv.lock
Your .env file is missing or the token is empty.
- Make sure you ran
/startand/search - Run
/testto check whether listings are available right now - Verify that your Kamernet property types, rent, bedroom/room, and size filters are not too restrictive
Delete listings.db, or use /clear to clear previously seen listings.
The bot does not depend on VS Code. Any terminal is fine as long as the virtual environment is active and .env is configured.
The bot uses Telegram polling, so the VPS does not need a public web port for the bot. Keep SSH open for deployment and make sure the droplet can make outbound HTTPS requests.
This setup assumes:
- Ubuntu droplet
- SSH access as
root - Your local
.envcontains a validTELEGRAM_TOKEN - Your local
.envcontainsTELEGRAM_ALLOWED_CHAT_IDSif the VPS bot should be private - The VPS should start with a fresh SQLite database
From the project root:
.\scripts\deploy.ps1 -Host YOUR_DROPLET_IPIf your SSH key is not the default key:
.\scripts\deploy.ps1 -Host YOUR_DROPLET_IP -IdentityFile C:\path\to\keyThe deploy script uploads the project to /opt/amsterdam-house-bot, uploads .env to /etc/amsterdam-house-bot/bot.env, creates /var/lib/amsterdam-house-bot/listings.db on first boot, installs dependencies, and starts a systemd service.
The bootstrap pins uv to version 0.8.15, verifies the downloaded binary checksum, installs Python 3.13.7, and runs uv sync --locked so Python dependencies come from uv.lock. The local .env, .venv, .git, __pycache__, local database files, and the default Kamernet storage-state file are excluded from the code archive. The .env file is uploaded separately as the service environment file. During VPS setup, any DB_PATH= value from .env is removed so production always uses /var/lib/amsterdam-house-bot/listings.db and deployments do not reset sent-listing history. If an older deploy created /opt/amsterdam-house-bot/listings.db, the bootstrap script migrates it before replacing app files.
SSH into the droplet:
ssh root@YOUR_DROPLET_IPCheck status:
systemctl status amsterdam-house-botFollow logs:
journalctl -u amsterdam-house-bot -fRestart the bot:
systemctl restart amsterdam-house-botStop the bot:
systemctl stop amsterdam-house-botShow recent logs:
journalctl -u amsterdam-house-bot -n 100 --no-pagerOpen Telegram and send:
/start
/search
/test
The bot will continue running after you close your terminal because systemd owns the process.