yaybo
Look up a Danish address, see what the land register (Tingbogen) holds on it,
and keep the results in a local DuckDB database you can browse, query and
export from your terminal.
Install
uv tool install yaybo # or: pip install yaybo
Python 3.10 or newer. uvx yaybo runs it without installing anything.
Quickstart
yaybo # open the TUI
yaybo fetch "Prøvegade 1, 9999 Prøveby" # fetch one address
yaybo --version # which version is installed
Results go to out/tinglysning.duckdb. Looking the same address up again
replaces its rows rather than adding a second copy.
Where the data comes from
| source | what it provides | login needed |
|---|---|---|
| tinglysning.dk | owners, mortgages and charges, easements, past transfers | partly |
| Boligsiden | sale prices, price per m², and the BBR record (year built, rooms, walls, heating) | no |
| Danmarks Statistik | what each kind of realkredit loan cost month by month, used to read a bare interest rate as an F3 or a fixed loan | no |
| DAWA | address lookup and validation while you type | no |
Most of it is available without logging in. Logging in with MitID adds owners' dates of birth, everyone named on each mortgage, and the history of previous owners.
The TUI
Run yaybo with no arguments. Eight screens, all reading the same database:
| screen | key | what it is for |
|---|---|---|
| Properties | l |
everything you have fetched, searchable offline |
| Co-op shares | tab | what you have fetched from the other register |
| Buildings | g |
the properties grouped one building to a row |
| Search | / |
find an address, see what the registers hold at it |
| Queue | b |
fetch many properties in the background |
| Figures | k |
across a set of properties rather than about one |
| SQL | s |
query the database directly |
| Property | enter |
one property in full |
Properties is where it opens. Each row shows how stale it is, its valuation,
debt and loan-to-value, and whether it was fetched while logged in. Sort by any
column with o, filter with name:value, tick rows with space and act on the
lot. enter opens a property, f re-fetches, e exports.
Search resolves your typing against DAWA,
then asks the register which properties actually sit at that address – one for a
rented block, over a hundred for a block of owner-occupied flats. Tick the ones
you want and press f to hand them to the queue.
Property shows one property, tab by tab, read back out of the database, so it is instant and works offline:
- Oversigt – what it is, who owns it, what it is worth, what it owes
- Ejere · Hæftelser · Servitutter · Parter · Handler – the tables in full, with each charge's interest terms and the loan product it implies
- Forløb – sales, transfers, mortgages, easements and valuations on one timeline. The register keeps these as four separate lists
- Kurve – price per square metre over time
- Bygning – the BBR record
- Dokument – the signed attest itself
Co-op shares is the same idea for the andelsboligbog, which is a different
register about a different thing – see Andelsboliger. Each row
is one co-op share: its area, what is charged against it, and the association's
building. enter opens a share, with its charges and everyone named on them and
any notices; g opens the building, which is where a share's valuation and the
association's own mortgages live.
Buildings groups the properties one building to a row: how many of its
properties you hold, how many were fetched while logged in, and the medians
across them. enter goes to that building's properties.
Queue takes what Search hands it and fetches in the background, with a
progress bar and per-row status. Pause with space. Anything already fetched is
already saved, so a lapsed login partway through costs you nothing.
Figures answers questions about a set of properties rather than one: median price per m² by floor, valuations by building, owners by postcode. Narrow the set with dropdowns filled from the database, then pick a figure. Nothing is fetched – it describes only the properties you already hold.
SQL runs read-only queries against the whole database, with eight examples
ready to load and edit. ctrl+R runs, ctrl+E exports the result.
Press ctrl+L anywhere to log in with MitID, or to log out, and ctrl+G to
switch language.
English or Danish
The interface comes in both and opens in English. ctrl+G switches at any
point and rebuilds every screen in the chosen language, putting you back where
you were; the choice is remembered for next time.
The data is never translated. A register record is Danish — Ejerpantebrev,
Almindeligt salg, the wording of an easement — and reading one is a
Danish-language job however the buttons around it are labelled. Anglicising a
value would also make the database disagree with the register it came from, so
only the chrome moves: headings, column labels, tabs, footer keys and the
sentences a screen writes about what it is showing.
The command line and the exports are English throughout and are not affected by the setting.
On the command line
yaybo # the TUI
yaybo --version # the installed version
yaybo fetch ADDRESS [ADDRESS ...] # look addresses up and store them
yaybo export # write what is stored to a spreadsheet
yaybo login --user YourMitIDUserID
yaybo status # is the session good, and for how long
yaybo keepalive [MINUTES] # hold it open without another trip to the phone
yaybo backfill # re-derive stored tables, fetching nothing
yaybo logout
fetch takes several addresses at once and pauses between them. One that
cannot be resolved is reported and skipped; the rest still get fetched.
yaybo fetch "Prøvegade 1, 9999 Prøveby" "Prøvevej 2, 9999 Prøveby"
Useful fetch options:
| option | what it does |
|---|---|
--format LIST |
duckdb (default), csv, xlsx, or a comma-separated list |
--limit N |
most properties to fetch (default 25, 0 for no limit) |
--anonymous |
ignore any cached session and use only the public lookup |
--delay SECONDS |
pause between fetches (default 1.0) |
--outdir DIR |
where results go (default out/, which is git-ignored) |
--no-andele |
skip the andelsboligbog; co-op blocks give only the association's property |
--no-boligsiden |
skip sale prices, BBR data and equity |
--no-laantype |
skip estimating each charge's loan type |
--keepalive [MIN] |
hold the session open afterwards (default 60) |
--debug and --db PATH work before or after the subcommand.
export writes what is already stored, rather than what was just fetched:
yaybo export # everything -> exports/*.xlsx
yaybo export --format csv --name priser --query "SELECT ..."
yaybo export --query-file report.sql --name rapport
It prints the written path on stdout and nothing else, so file=$(yaybo export)
gives you the file.
fetch and the TUI share the same pipeline, so they cannot drift apart.
backfill rebuilds every table derived from a stored document – charges,
easements, the people named on them, previous owners – with no login and no
requests to the register. Run it after upgrading, when a reader has improved.
The data
One DuckDB file, fifteen tables. Twelve of them are keyed on the property; the other three are the andelsboligbog, which is a different register about a different thing – see Andelsboliger below.
erDiagram
ejendomme ||--o{ ejere : owns
ejendomme ||--o{ haeftelser : "charged with"
ejendomme ||--o{ servitutter : "burdened by"
ejendomme ||--o{ bygninger : "built on"
ejendomme ||--o{ handelshistorik : "sold as"
ejendomme ||--o{ adkomsthistorik : "transferred as"
ejendomme ||--o| attester : "documented by"
ejendomme ||--o{ dokument_parter : "named on"
haeftelser ||--o{ underpant : "pledged as"
haeftelser ||--o{ dokument_parter : names
servitutter ||--o{ dokument_parter : names
adkomsthistorik ||--o{ adkomsthistorik_ejere : names
ejendomme ||--o{ andele : "shares in"
andele ||--o{ andel_haeftelser : "charged with"
andele ||--o{ andel_meddelelser : "noted on"
ejendomme {
varchar uuid PK
varchar adresse
bigint ejendomsvurdering_dkk
bigint samlet_gaeld_dkk "derived"
bigint frivaerdi_dkk "derived"
double belaaningsgrad_pct "derived"
boolean beriget "fetched while logged in"
}
ejere {
varchar ejendom_uuid PK, FK
bigint nummer PK
varchar navn
date foedselsdato "login only"
varchar cvr
}
haeftelser {
varchar ejendom_uuid PK, FK
varchar dokument_uuid PK
varchar dokument_version PK
bigint prioritet
bigint hovedstol_dkk
double rentesats_pct
varchar laantype_estimat "estimated"
}
servitutter {
varchar ejendom_uuid PK, FK
varchar dokument_uuid PK
varchar dokument_version PK
varchar dokumenttype
varchar tekst
}
dokument_parter {
varchar ejendom_uuid PK, FK
varchar dokument_uuid PK, FK
varchar dokumentart PK
varchar rolle PK
bigint nummer PK
varchar navn
date foedselsdato "login only"
}
underpant {
varchar ejendom_uuid PK, FK
varchar haeftelse_uuid PK, FK
varchar rettighed_uuid PK
bigint beloeb_dkk
}
handelshistorik {
varchar ejendom_uuid PK, FK
varchar registrering_id PK
date dato
bigint beloeb_dkk
bigint pris_pr_m2
}
bygninger {
varchar ejendom_uuid PK, FK
varchar bygning_nr PK
bigint opfoerelsesaar
bigint boligareal_m2
varchar varmeinstallation
}
adkomsthistorik {
varchar ejendom_uuid PK, FK
bigint post_nummer PK
date dato
bigint koebesum_dkk
}
adkomsthistorik_ejere {
varchar ejendom_uuid PK, FK
bigint post_nummer PK, FK
bigint nummer PK
varchar navn
}
attester {
varchar ejendom_uuid PK, FK
varchar dokument "as signed"
json dokument_json "queryable"
}
rentestatistik {
varchar maaned PK
varchar rentfix_kode PK
varchar laantype
double effektiv_rente_pct
}
andele {
varchar uuid PK "andelsboligbogen"
varchar adresse
varchar ejendom_uuid FK "the association's building"
bigint boligareal_m2 "Boligsiden; the book records none"
bigint samlet_gaeld_dkk "derived: this share only"
}
andel_haeftelser {
varchar andel_uuid PK, FK
varchar dokument_uuid PK
varchar dokument_version PK
varchar dokumenttype
bigint hovedstol_dkk
}
andel_meddelelser {
varchar andel_uuid PK, FK
varchar dato_loebenummer PK
varchar dokumenttype
varchar debitorer "the andelshaver"
varchar disponenter "who may act for them"
}
| table | one row per | needs login |
|---|---|---|
ejendomme |
property, with valuation, debt and equity | no |
ejere |
current owner | no |
haeftelser |
mortgage or charge, with interest terms and estimated loan type | no |
servitutter |
easement, and what it is about | no |
dokument_parter |
person named on a document, with their role and date of birth or CVR | yes |
underpant |
deed pledged on in its own right | no |
handelshistorik |
recorded sale, with price per m² | no |
bygninger |
building in the BBR record | no |
adkomsthistorik |
past transfer, with what was paid | yes |
adkomsthistorik_ejere |
person named in one of those transfers | yes |
attester |
the property's whole register document, signed and as JSON | yes |
rentestatistik |
month of DST realkredit rates | no |
andele |
co-op share, from the andelsboligbog | no |
andel_haeftelser |
charge registered against one share | no |
andel_meddelelser |
notice noted on a share: death, bankruptcy, seizure | no |
rentestatistik is not about any one property. It is the rate series
laantype_estimat was matched against, kept so an estimate can be checked.
The full column-level schema is in schema.dbml, generated from the code so it cannot fall behind it. Paste it into dbdiagram.io for a browsable diagram.
Andelsboliger
Tinglysning is four registers, not one, and two of them matter here. The tingbog records real property. The andelsboligbog records shares in housing associations, and they disagree about what a co-op building is — correctly, in both cases:
- To the tingbog, a co-op block is one property, owned by the association,
however many doors it has. That is the row in
ejendomme. - To the andelsboligbog, the same block is one entry per flat. Those are
the rows in
andele.
Both are fetched, and andele.ejendom_uuid joins the second to the first. A
lookup that finds shares says so: found 1 property and 10 co-op shares.
What the second book actually holds is much less than the first. A share is not
land, so it has no valuation, no matrikel, no registered area and no
easements — only its address, whatever is charged against it, and any
notices. The area and the coordinates on an andele row come from Boligsiden
rather than the register.
There is no owner of record. The andelsboligbog registers rights over a share, not title to one; who holds an andel is the association's record, not the register's. Two places name people anyway:
andel_haeftelser.kreditorer. Most charges on a share are an ejerpantebrev — a deed the owner issues to themselves and then pledges to a bank — so its creditor is in practice the andelshaver. That is an inference from the instrument, not something the register states.andel_meddelelser.debitorerand.disponenter. A notice is the register recording that something has happened to the andelshaver rather than to the flat: a death, a bankruptcy, a court removing their power to dispose of it. It names them, and whoever may now act for them.
Neither carries a date of birth. Those come from the CPR numbers printed on a signed attest, and no attest for a share has been read here — see below.
Three things worth knowing before querying it:
- A flat missing from
andeleis not evidence it is not an andel. A share only enters the book once something is registered against it. andele.samlet_gaeld_dkkis not what living there owes. It totals what is charged against that share alone. An andelshaver also owes their portion of the association's own mortgage, which is registered against the building in the tingbog and is nowhere in this table.- There is no sale price for a share, and this deliberately stores none.
Boligsiden reports the building's own sale against every door in the block —
the same date and amount on all of them — and divides it by each flat's area
into a price per m² that describes nothing. That sale is a fact about the
building, and is stored as one, on the
ejendommerow.
One thing is known to exist and is not read here: a logged-in session can
fetch a signed andelsboligbogsattest (rest/andelsbolig/...) and search the
book by person name and date of birth. By analogy with the tingbog that attest
would carry parties' CPR-derived birth dates. It is unverified and nothing here
depends on it.
In the TUI, Co-op shares lists the shares held; enter opens one - its charges,
everyone named on them, and its notices - and g opens the association's
property.
yaybo fetch --no-andele skips the second book and behaves as earlier versions
did. yaybo backfill cannot rebuild these two tables — the register stores no
signed document for a share, so there is nothing to re-derive them from — and
leaves them untouched.
Every table has a primary key, so a row is identifiable and a re-run replaces rather than duplicates. Relationships are drawn above but not enforced: DuckDB cannot add a foreign key to an existing table, so enforcing them would leave every database created before this version permanently unable to catch up.
Exporting
Press e on any screen, pass --format to fetch, or run yaybo export
against what is already stored.
| format | shape |
|---|---|
| DuckDB | one table each, as they already are |
| Excel | one sheet per table, header row frozen |
| CSV | one file per table, in a folder named after the address |
Column order follows the schema, so the same table exported twice has the same columns in the same places.
Using it with an AI agent
The TUI is for people. Agents should use the CLI, which is non-interactive apart from the login.
Install the plugin (no clone needed)
In Claude Code – terminal or desktop – type:
/plugin marketplace add kiliantscherny/yaybo
/plugin install yaybo@yaybo
That is the whole setup. It works from any folder, and gives the agent the CLI reference, the data model and the schema. The only thing you need on your machine is uv – it installs Python itself, so that is not a separate prerequisite:
curl -LsSf https://astral.sh/uv/install.sh | sh # macOS and Linux
powershell -c "irm https://astral.sh/uv/install.ps1 | iex" # Windows
Then just ask, in plain language:
What does the land register hold on Prøvegade 1 and Prøvevej 2 in 9999 Prøveby? Put it in a spreadsheet.
The agent fetches with yaybo fetch, queries the database, and hands back an
Excel file. Fetched data lands in out/ in whatever folder you are working in,
so it is worth making one and staying in it:
mkdir ~/property-lookups && cd ~/property-lookups
Or clone the repository
Cloning gets you the source. To use the skill from a clone without installing it, point Claude Code at the plugin directory:
git clone https://github.com/kiliantscherny/yaybo.git
cd yaybo
claude --plugin-dir ./plugin
Either way the agent has:
| file | what it gives the agent |
|---|---|
| AGENTS.md | the project, its rules, and what not to do (CLAUDE.md symlinks to it) |
| schema.dbml | every table, column, type and key, generated from the code |
plugin/skills/danish-property-records/ |
the CLI, the data model and worked queries, loaded only when needed |
What you are taking on
Logging in means logging in as you, to a government register, with MitID.
mitid-client
The MitID login is a separate library: mitid-client. It knows nothing about property – it is a Python stand-in for MitID's JavaScript core client, the NemLog-in broker that fronts the Danish public sector, a store for keeping a login's cookies between runs, and two ways of showing a login to whoever is doing it: a few lines on stderr, or a Textual screen.
from mitid.brokers import nemlogin
from mitid.ui.tui import MitIDLoginScreen
session = nemlogin.new_session()
result = await self.push_screen_wait(
MitIDLoginScreen(partial(nemlogin.log_in, session, START_URL))
)
Point it at any NemLog-in-protected URL and it returns the session cookie that URL was guarding. It installs as a dependency of this, so there is nothing to do about it. It is worth knowing about separately because the login is the reusable half.
Contributing
See CONTRIBUTING.md. Releases are described in RELEASING.md, and changes in CHANGELOG.md.
MIT licensed.
Metadata
Release files for yaybo 1.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| yaybo-1.1.0.tar.gz | 209.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| yaybo-1.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 391.8 kB
Release files / yaybo-1.1.0.tar.gz
| Download URL | yaybo-1.1.0.tar.gz |
|---|---|
| Size | 209.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
497329004f935e620e64c58ebb27e9843a53c2536fb3f8269a90a55dba41b429
|
|
BLAKE2b-256 checksum How to use checksums |
1fb037dabbecfff05e2a771cc87166f8b8fd02f0148a746daf37fc73e0bc3261
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / yaybo-1.1.0-py3-none-any.whl
| Download URL | yaybo-1.1.0-py3-none-any.whl |
|---|---|
| Size | 182.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
98f73e7744867806f0256e572496a2d466594a8adea587e003b83065ae8a5831
|
|
BLAKE2b-256 checksum How to use checksums |
6b2ef3a2888e2d8272d452f1803cc6acb7f7b75cbc541b1fc086e266f0caa3bc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|