Key information for deciding which place for sale to buy for home, starting with the finances of the bostadsrättsförening (BRF) that owns the building. For any BRF it answers: where does the money go, how much debt and cash does it have, is a stambyte coming, and how do its costs compare with other BRFs.
The analysis follows the money (what came in, what went out, what happened to the cash and the loans) rather than the accounting result, and every number links back to the page of the document it came from.
raw/ original documents, unchanged, one folder per BRF (orgnr) and year; only
metadata.yaml is in git, the documents themselves are kept locally (storage TBD)
annual_reviews/brfs/<orgnr>/<fiscal year>/ årsredovisningar + metadata.yaml (source URL, sha256)
protocols/brfs/<orgnr>/<meeting year>/ stämma protocols
maintenance_plans/brfs/<orgnr>/ underhållsplaner
annual_reviews/brfs/ extracted, verified values (CSV); data model in annual_reviews/brfs/README.md
market/ Booli sales, current listings and apartment registers per BRF; see market/README.md
schools/ Skolverket's school index per school unit, 2023–2026; see schools/README.md
index.csv one row per BRF: size, build year, coverage, stambyte summary (build_index.py)
coverage.md which annual reports are held / extracted / verified, BRF × last 5 years (generated)
tracked.csv the BRFs we want covered, kept by hand (also before any report is found)
<orgnr>/profile.yaml per-BRF facts, major works, derived stambyte assessment, data notes
tools/brf/ import, PDF reading/OCR, verification, comparison and index scripts
tools/booli/ scrape.py + refresh.py: refresh market/booli/ from booli.se; import_from_data_repo.py: the original copy
tools/readmodel/ builds the API's read model (one finished document per response row) from the repo data
tools/cloud/ provision.py: blue/green deployment of the read model to the cloud DB
api/ read-only HTTP API (Azure Functions, Python), spec in api/openapi.json
infra/ main.bicep: the Azure resources behind the API
.cache/pdfwords/ OCR cache for scanned pages (safe to delete; rebuilt on demand)
.claude/skills/ the method, as three skills
.claude/skills/extract-brf-annual-review/): import a report and turn it into rows in
financial_events, financial_states, property_states and general_states, each citing file, page and box..claude/skills/verify-brf-data/): every value is found where it is cited, cash and debt chain
from one year-end to the next, totals equal their parts, and the report’s own cash flow is reproduced..claude/skills/compare-brfs/): BRFs side by side, per m² or as shares of income or costs.python3 tools/brf/verify_sources.py annual_reviews/brfs/712400-1467 --years 2015-2025 --quiet
python3 tools/brf/validate.py annual_reviews/brfs/712400-1467 --years 2016-2025
python3 tools/brf/compare.py 712400-1467=Sjötungan 712400-0089=Bergfoten --years 2024-2025
python3 tools/brf/build_index.py # also rewrites coverage.md
annual_reviews/brfs/coverage.md shows, for every tracked BRF and each of the
last five fiscal years, whether its annual report is held, extracted and verified. It is regenerated by
import_document.py after every report import and by build_index.py; add a new BRF to tracked.csv.
Requires Python 3 with pdfplumber, PyYAML and pytesseract, plus tesseract (with Swedish) and poppler.
| BRFs | basis | |
|---|---|---|
| in Sweden | ≈ 31 000 active (33 850 registered) | hittabrf.se, undated; active = owns and manages a property. The Riksbank (2018, from Bolagsverket) gave 32 000 registered, 26 000 active |
| in Stockholm (kommun) | ≈ 5 750 active (9 976 in Stockholms län) | brfregistret.se, 2022-02-16; active = owns or manages a residential building and is VAT-registered. hittabrf.se gives only 3 645 active plus 493 registered, undated |
| in Tyresö | ≈ 93 | listed for the postort Tyresö on svenskbrf.se; not an official count |
| tracked for annual reports | 14 | 12 in Tyresö, Karlsön and Passadvinden in Stockholm; annual_reviews/brfs/tracked.csv |
| with Booli market data | ≈ 3 100 | every BRF with an apartment for sale in Stockholm, Nacka, Solna and Tyresö (GET /brfs) |
| 10 years of årsredovisningar downloaded | 2 | Sjötungan, Gäddan; the others have 3–9 of their last 10 fiscal years; the rest is not public |
| structured data extracted | 13 | at least the latest report of each; not Passadvinden (no public report) |
| verified | 13 | every extracted year passes verify-brf-data: 51 of the 70 reports of the last 5 fiscal years, all that are held; 6 BRFs over the full 5 years (Björktrasten, Laxtrappan, Bergfoten, Gäddan, Sjötungan, Skogsgläntan); see annual_reviews/brfs/coverage.md |
Coverage rule: download the annual reports of the last 10 fiscal years where they are public, and extract
and verify the last 5 fiscal years, plus the closing balances of the year before as the opening balance
(so the cash and debt checks cover all five). More is kept where it already exists.
annual_reviews/brfs/index.csv lists every BRF with its coverage.
| BRF | orgnr | extracted and verified (chained) | gaps (not public) |
|---|---|---|---|
| HSB Brf Sjötungan i Tyresö | 712400-1467 | 2016–2025 (2015 opening) | — |
| HSB Brf Gäddan i Tyresö | 769604-8383 | 2016–2025 (2015 opening) | — |
| HSB Brf Bergfoten i Tyresö | 712400-0089 | 2020–2025 (2019 opening) | 2016–2018 |
| Brf Björktrasten | 769610-4251 | 2021–2025 (2020 opening) | 2016 |
| HSB Brf Skogsgläntan i Tyresö | 769628-4574 | 2021–2025 (2020 opening) | 2016–2018 |
| HSB Brf Pluto i Tyresö (Jul–Jun) | 712400-2309 | 2020/21–2024/25 (2019/20 opening) | 2015/16–2016/17 |
| Brf Silvertärnan | 769611-6131 | 2020, 2023–2024 (2019 opening; 2023 unchained) | 2016–2018, 2025 (2021–2022 downloaded, not yet extracted) |
| Brf Laxtrappan | 716421-3436 | 2023–2024 (2023 unchained) | 2016 (2017–2022 and 2025 downloaded, not yet extracted) |
| HSB Brf Siken i Tyresö (May–Apr) | 712400-1426 | 2024/25 (within-year checks only) | 2015/16–2017/18 (2018/19–2023/24 downloaded, not yet extracted) |
| HSB Brf Rödingen i Tyresö | 769616-3463 | 2021–2022 (2021 unchained) | 2020, 2023–2025 (members only; 2016–2019 downloaded, not yet extracted) |
| Brf Stortorpet 1 | 769626-9864 | 2021, 2025 (both unchained) | 2018 (allabrf login), 2022–2024 (no source found) |
| Brf Odlingslotten i Tyresö | 769629-8590 | 2025 | 2021–2024 (allabrf and Booli: login) |
A read-only HTTP API over the data, built for LLM agents as its main client: the OpenAPI spec at
/openapi.json describes every field with its unit and meaning. GET /advertisements keeps the contract of
the data repo’s Real Estate Data API (same parameters, sorts, errors and rows), which is being migrated here
one endpoint at a time. It serves the same listings: every one in the Booli snapshot of Stockholm, Nacka,
Solna and Tyresö (market/booli/), and GET /cities, /kommuns and /areas serve the same geo hierarchy
(tools/readmodel/geo.py), and GET /sales/stats/avg-price-per-sqm the same sales statistics (every year/rooms
combination precomputed, tools/readmodel/sales_stats.py). tools/cloud/parity.py compares every listing, geo row
and statistic, field by field,
with the data repo’s live API. Known differences: about 150 more listings linked to their BRF here (and through
them the cooperative-stock fields of their område, kommun and city), street_name without a trailing space,
±1 on a trend that falls on a rounding tie, a BRF row’s område chosen by most listings, and two schools at one
address tying for fifth-nearest. With --data-repo-linking only the ties and those choices remain.
| base URL | https://homesight-api.azurewebsites.net |
| changes | CHANGELOG.md |
| resource group | homesight (Sweden Central), subscription “Azure subscription 1” |
| endpoints (0.11.6) | GET /llms.txt, GET /health, GET /openapi.json, GET /brfs, GET /brfs/{orgnr}, GET /brfs/{orgnr}/financials, GET /advertisements, GET /cities, GET /kommuns, GET /areas, GET /sales/stats/avg-price-per-sqm |
Same principles as the data repo.
tools/readmodel/, a port of the data repo’s import_booli.py with the same
column names. A request is one indexed query with no joins and no computation (a few RU, see below).tools/readmodel/financials.py turns the extracted, verified report values into one
financial_year document per BRF and year (income, costs, interest, loans, cash, per-m² figures, ratios, and
each number’s page and box in the report), served by GET /brfs/{orgnr}/financials; the latest verified
year is also copied onto every listing of the BRF as brf_report_*, with brf_land_tenure and the stambyte
assessment. After extracting a report, provision + flip publishes it.api/advertisements.py); photos, attributes and per-year history are stored but not indexed.homesight_blue and homesight_green. provision rebuilds the inactive one
from the repo and verifies it (counts, every sort, a filter and a radius query, run with the API’s own
query code); flip makes it live; the previous one stays as rollback. infra/main.bicepparam records
which one is live.provision.Cost. Both tiers bill per operation and cost nothing while idle: Cosmos DB for NoSQL, serverless, and
Azure Functions, Flex Consumption (no always-ready instances, at most 10 instances). A /advertisements
request costs 4–8 RU (about 5 RU on average over all 49 sorts). Access is identity-based (Cosmos key auth
is off); all endpoints are public, with no key.
# one-off: tools
python3.12 -m venv .venv && .venv/bin/pip install -r tools/requirements.txt
# parity: every listing against the data repo's live API (about 3 minutes)
.venv/bin/python tools/cloud/parity.py
# data: rebuild the inactive database, verify it, make it live (flip again to roll back)
.venv/bin/python tools/cloud/provision.py status
.venv/bin/python tools/cloud/provision.py provision
.venv/bin/python tools/cloud/provision.py flip
# infrastructure (idempotent)
az deployment group create -g homesight -f infra/main.bicep -p infra/main.bicepparam
# code (Azure installs requirements.txt: remote build)
(cd api && zip -r ../api.zip *.py host.json requirements.txt openapi.json llms.txt)
az functionapp deployment source config-zip -g homesight -n homesight-api --src api.zip --build-remote true
When code and data change together, deploy code that reads both document shapes first, then flip: Flex Consumption has no deployment slots, so code is not blue/green.
The API version is API_VERSION in api/function_app.py. The spec is served by the API itself (always
matching the deployed code), with its version from API_VERSION and its server URL from the
PUBLIC_BASE_URL app setting (the Bicep publicBaseUrl parameter; change it when a custom domain is added).