Skip to content

nille/boligwatch

Repository files navigation

BoligWatch

CI Python 3.10+ Ruff License: MIT

CLI tool and MCP server that monitors boligportal.dk for new rental listings in Denmark. Polls the same public search API the website uses — no account or API key required.

Requirements

  • Python 3.10+
  • CLI mode works with stdlib only (zero required dependencies)
  • pip install curl_cffi — recommended, bypasses Cloudflare bot protection (see Cloudflare bypass)
  • pip install mcp for MCP server mode

Installation

# Clone the repository
git clone https://github.com/your-username/boligwatch.git
cd boligwatch

# (Optional) Create and activate a virtual environment
python3 -m venv .venv
source .venv/bin/activate

# CLI mode works out of the box with stdlib only
python boligwatch.py --help

# Recommended: install curl_cffi to bypass Cloudflare bot protection
pip install curl_cffi

# For MCP server mode, install the MCP SDK
pip install mcp

# Generate a config file and edit it to match your search
python boligwatch.py --init-config

Quick start

# Run once — shows new listings (no filters unless config file provides them)
python boligwatch.py

# Use a saved config for your regular search
python boligwatch.py --config boligwatch_config.json

# Watch mode — poll every 5 minutes
python boligwatch.py --config boligwatch_config.json --watch

# Ad-hoc search with inline filters
python boligwatch.py --city københavn --rooms-min 3 --max-rent 16000 --balcony

How it works

BoligWatch fetches listings from boligportal.dk's search API, filters them according to your criteria, and tracks which ones you've already seen in a local JSON file. On each run it only shows new listings — ones not previously seen.

The tracker also detects re-listed apartments: if a landlord re-publishes a listing with the same ID but a newer advertised_date, it resurfaces as new. This prevents missed opportunities when listings are taken down and re-posted. The seen file stores the advertised_date alongside the seen timestamp for comparison. Legacy entries (from older versions) are read transparently — re-listing detection is skipped for entries without a stored advertised_date.

There are three ways to use it:

  1. CLI — run once or in watch mode, pipe JSON to other tools
  2. MCP server — expose search and tracking as tools for Claude Code, Claude Desktop, or any MCP client
  3. Skill — a ready-made skill that teaches Claude to translate natural-language apartment queries into MCP tool calls

CLI reference

Modes

Flag Description
(default) Run once, print new listings, mark them as seen
--watch, -w Poll continuously (default: every 300s)
--interval N, -i N Poll interval in seconds (used with --watch)
--json Output new listings as a JSON array, mark as seen
--peek Output new listings as JSON, do NOT mark as seen (retry-safe)
--mcp Start as MCP server (stdio transport)

Seen-listing management

Flag Description
--mark-seen ID [ID ...] Mark specific listing IDs as seen
--reset Clear all seen-listing history before running
--seen-file PATH Custom path for the seen-listings tracker (default: .boligwatch_seen.json)

Config and logging

Flag Description
--config PATH, -c PATH Path to a JSON config file
--init-config Generate a config template at --config path or default location
--log-file PATH Write log to file
--verbose, -v Verbose (DEBUG-level) logging

Search filters

All filters default to no limit (unset). See Filter behavior for how filters interact with the config file.

Location

Flag Type Default Description
--city NAME string (repeatable) ["k\u00f8benhavn"] City to search. Can be repeated: --city k\u00f8benhavn --city frederiksberg
--bbox S,W,N,E floats none Bounding box as min_lat,min_lng,max_lat,max_lng. Replaces --city when set

City names use lowercase Danish with original characters: k\u00f8benhavn, frederiksberg, aarhus, odense, aalborg.

Size and price

Flag Type Default Description
--rooms-min N int none Minimum number of rooms
--rooms-max N int none Maximum number of rooms
--max-rent N int none Maximum monthly rent in DKK
--min-size N int none Minimum size in m\u00b2
--min-rental-period N int none Minimum lease length in months (12 = 1 year)
--max-available-from DATE YYYY-MM-DD none Latest move-in date

Property type

Flag Type Default Description
--newbuild bool none Only new-build / project rentals (projektudlejning)
--social-housing bool none Only social housing (almen bolig)

Lifestyle

Flag Type Default Description
--pet-friendly bool none Only pet-friendly listings
--senior-friendly bool none Only senior-friendly listings
--student-only bool none Only student housing
--shareable bool none Only shareable apartments (delevenlig)

Facilities

Flag Type Default Description
--parking bool none Must have parking
--elevator bool none Must have elevator
--balcony bool none Must have balcony or terrace
--ev-charging bool none Must have EV charging station (ladestander)

Appliances

Flag Type Default Description
--furnished bool none Must be furnished
--dishwasher bool none Must have dishwasher
--washing-machine bool none Must have washing machine
--dryer bool none Must have dryer

Pagination

Flag Type Default Description
--max-pages N int 5 Maximum pages to fetch (18 listings per page, max 50)

CLI examples

# Pet-friendly 2-room apartments in Copenhagen under 12.000kr
python boligwatch.py --city k\u00f8benhavn --rooms-min 2 --rooms-max 2 --max-rent 12000 --pet-friendly

# Large apartments (100m2+) with balcony, available by August
python boligwatch.py --city k\u00f8benhavn --min-size 100 --balcony --max-available-from 2026-08-01

# Furnished places with dishwasher and washing machine in Frederiksberg
python boligwatch.py --city frederiksberg --furnished --dishwasher --washing-machine

# Social housing anywhere in the Copenhagen bounding box
python boligwatch.py --bbox 55.63,12.48,55.73,12.80 --social-housing

# New-build project rentals with EV charging and elevator
python boligwatch.py --city k\u00f8benhavn --newbuild --ev-charging --elevator

# Peek at new listings as JSON (safe to retry, doesn't mark seen)
python boligwatch.py --config boligwatch_config.json --peek

# Watch mode: poll every 2 minutes with a saved config
python boligwatch.py --config boligwatch_config.json --watch --interval 120

# Mark specific listings as seen
python boligwatch.py --mark-seen 5619969 2987892

# Pipe to jq for processing
python boligwatch.py --peek | jq '.[].url'

Config file

A config file saves your default search criteria so you don't have to pass CLI flags every time. CLI flags always override config values.

Generate a template

python boligwatch.py --init-config
# or specify a path:
python boligwatch.py --init-config --config my_search.json

Config file format

All fields are optional. Omit a field or set it to null for no limit. Unknown keys are logged as warnings to help catch typos.

{
  "categories": ["rental_apartment", "rental_house", "rental_townhouse"],
  "city_level_1": ["k\u00f8benhavn"],
  "city_level_2": null,
  "min_lat": null,
  "min_lng": null,
  "max_lat": null,
  "max_lng": null,
  "rooms_min": 3,
  "rooms_max": null,
  "max_rent": 17000,
  "min_size_m2": null,
  "min_rental_period": 12,
  "max_available_from": null,
  "pet_friendly": null,
  "balcony": null,
  "furnished": null,
  "parking": null,
  "elevator": null,
  "shareable": null,
  "student_only": null,
  "senior_friendly": null,
  "social_housing": null,
  "newbuild": null,
  "electric_charging_station": null,
  "dishwasher": null,
  "washing_machine": null,
  "dryer": null,
  "order": "DEFAULT",
  "max_pages": 5
}

Categories

The categories field controls which property types to include:

Value Danish Description
rental_apartment Lejligheder Apartments
rental_room V\u00e6relser Rooms
rental_house Huse Houses
rental_townhouse R\u00e6kkehuse Townhouses

Location: city vs bounding box

You can filter by city name or bounding box, but not both. When bounding box coordinates are set, city filters are ignored.

  • City: "city_level_1": ["k\u00f8benhavn", "frederiksberg"]
  • Bounding box: Set all four of min_lat, min_lng, max_lat, max_lng
  • Sub-city: "city_level_2": ["amager"] (narrows within city_level_1)

MCP server

BoligWatch runs as a local MCP server, exposing search and tracking as tools for any MCP client.

Setup

Install the MCP SDK (the CLI works without it):

pip install mcp

Add to your MCP client config (.mcp.json, Claude Desktop config, etc.):

{
  "mcpServers": {
    "boligwatch": {
      "command": "python3",
      "args": [
        "/path/to/boligwatch.py", "--mcp",
        "--config", "/path/to/boligwatch_config.json"
      ]
    }
  }
}

Tools

Tool Description
search_listings Search with filters, returns all matches as JSON
get_new_listings Like search, but only returns listings not previously seen (peek by default)
mark_seen Mark listing IDs as seen
reset_seen Clear all seen-listing history
get_seen_stats Get tracker statistics (count, file path)

Filter behavior

The same logic applies to both CLI and MCP:

  • No filters passed = uses the full saved search from your config file (your monitoring query)
  • Any filter passed = starts from a clean slate. Only structural settings (categories, location, order, max_pages) carry over from the config. Restrictive filters (rent, rooms, size, features) are stripped — if you ask "find apartments over 200m\u00b2" it won't silently cap at 17,000 kr because of your config.

This means python boligwatch.py --peek uses your saved search, but python boligwatch.py --min-size 200 won't silently cap at 17,000 kr from the config. Same for MCP: get_new_listings({}) = saved search, search_listings({min_size_m2: 200}) = clean slate.

MCP tool parameters

Both search_listings and get_new_listings accept the same filter parameters:

Parameter Type Default Description
cities string[] from config City names (e.g. ["k\u00f8benhavn"])
min_lat float from config Bounding box south latitude
min_lng float from config Bounding box west longitude
max_lat float from config Bounding box north latitude
max_lng float from config Bounding box east longitude
rooms_min int no limit Minimum rooms
rooms_max int no limit Maximum rooms
max_rent int no limit Maximum monthly rent (DKK)
min_size_m2 int no limit Minimum size (m\u00b2)
min_rental_period int no limit Minimum lease (months)
max_available_from string no limit Latest move-in date (YYYY-MM-DD)
pet_friendly bool no filter Pet-friendly only
balcony bool no filter Has balcony/terrace
furnished bool no filter Furnished only
parking bool no filter Has parking
elevator bool no filter Has elevator
shareable bool no filter Shareable (delevenlig)
student_only bool no filter Student-only
senior_friendly bool no filter Senior-friendly
social_housing bool no filter Social housing (almen bolig)
newbuild bool no filter New-build / project rental
electric_charging_station bool no filter EV charging
dishwasher bool no filter Has dishwasher
washing_machine bool no filter Has washing machine
dryer bool no filter Has dryer
max_pages int 5 Pages to fetch (18 per page, max 50)

get_new_listings also accepts:

Parameter Type Default Description
mark_as_seen bool false If true, mark returned listings as seen

MCP tool call examples

Search for 3+ room apartments in Copenhagen under 16.000kr:

{
  "tool": "search_listings",
  "arguments": {
    "cities": ["k\u00f8benhavn"],
    "rooms_min": 3,
    "max_rent": 16000
  }
}

Find furnished apartments with washing machine and dishwasher:

{
  "tool": "search_listings",
  "arguments": {
    "cities": ["k\u00f8benhavn"],
    "furnished": true,
    "washing_machine": true,
    "dishwasher": true
  }
}

Large apartments (150m\u00b2+) anywhere in the search area:

{
  "tool": "search_listings",
  "arguments": {
    "min_size_m2": 150,
    "max_pages": 10
  }
}

New-build projects with EV charging, available by September:

{
  "tool": "search_listings",
  "arguments": {
    "newbuild": true,
    "electric_charging_station": true,
    "max_available_from": "2026-09-01"
  }
}

Check for unseen listings (peek mode):

{
  "tool": "get_new_listings",
  "arguments": {}
}

Check for new listings and mark them as seen:

{
  "tool": "get_new_listings",
  "arguments": {
    "mark_as_seen": true
  }
}

Mark specific listings as reviewed:

{
  "tool": "mark_seen",
  "arguments": {
    "ids": [5619969, 2987892]
  }
}

Skill: natural-language queries

A ready-made skill is included at skills/bolig-watch/SKILL.md that teaches Claude to translate natural-language apartment queries into MCP tool calls.

Example queries

Once the MCP server and skill are configured, you can ask Claude things like:

Find me a 3-room apartment in Copenhagen under 15.000kr
Are there any pet-friendly places with a balcony in Frederiksberg?
Show me the largest apartments available right now
Any new listings since last time?
Find furnished apartments with a washing machine, at least 70m2
What's available in Amager for under 12k with at least 2 rooms?
Show me new-build projects with EV charging and elevator
Find social housing apartments in Copenhagen available before August
I've looked at those listings, mark them as seen
How many listings have I seen so far?

Scheduled polling with Claude Code

Use Claude Code's loop command to have Claude poll for new listings automatically:

/loop 5m check for new boligportal listings. Summarize anything new.

Browser automation with Playwright MCP

Combine BoligWatch with Playwright MCP and the Playwright MCP Bridge Chrome extension. Together, they let Claude control your actual browser with your logged-in boligportal.dk session.

This means Claude can navigate to listings, read details, contact landlords, fill in forms, and send messages.

Setup

  1. Install the Playwright MCP Bridge extension in Chrome
  2. Add both MCP servers to your config:
{
  "mcpServers": {
    "boligwatch": {
      "command": "python3",
      "args": ["/path/to/boligwatch.py", "--mcp", "--config", "/path/to/config.json"]
    },
    "playwright": {
      "command": "npx",
      "args": ["@playwright/mcp@latest", "--extension"]
    }
  }
}

Autonomous apartment hunting

/loop 5m check for new boligportal listings with the boligwatch MCP.
For each new listing, open it in the browser with Playwright, save a PDF,
contact the landlord with a message in Danish, and notify me on Slack.

Because the extension bridges into your existing browser session, Claude authenticates as you — no separate login flow, no stored credentials.

Cloudflare bypass

Boligportal.dk uses Cloudflare bot protection, which can block requests from standard HTTP clients like Python's urllib with an HTTP 403 and a JavaScript challenge. When this happens, the API becomes unreachable.

Installing curl_cffi enables Chrome TLS fingerprint impersonation, which bypasses the challenge transparently:

pip install curl_cffi

When curl_cffi is installed, BoligWatch automatically uses it for all API requests. When it's not installed, BoligWatch falls back to stdlib urllib — which works fine when Cloudflare isn't actively challenging requests.

Both backends retry on HTTP 403, 429, and 5xx errors with exponential backoff.

Listing output format

Each listing returned by the API includes:

Field Type Description
id int Unique listing ID
url string Direct link to the listing
title string Listing title
city string City name
city_area string Sub-area (e.g. "K\u00f8benhavn S", "Amager")
postal_code string Postal code
street_name string Street name
street_number string Street number
rooms float Number of rooms
size_m2 float Size in square meters
monthly_rent float Monthly rent
monthly_rent_currency string Currency (typically "kr")
deposit float Deposit amount
prepaid_rent float Prepaid rent
available_from string Move-in date (YYYY-MM-DD)
advertised_date string When the listing was posted
category string Property type
energy_rating string Energy label (e.g. "A2010", "C")
features object Boolean feature flags (pet_friendly, elevator, etc.)

License

MIT

About

CLI tool and MCP server that monitors boligportal.dk for new rental listings in Denmark. Includes an agent skill for natural-language apartment search.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Languages