← Work

Vergabeautomat

German public contracts are advertised across the EU tender database and more than a hundred separate regional portals. Companies that could win them never see them. I built the thing that watches all of it and tells them which ones are worth the effort.

What it isA multi-tenant service, my own product
My roleEverything. Product, architecture, code, infrastructure, support
SourcesTED, the EU tender database, plus German regional portals
StackTypeScript · PostgreSQL with pgvector · Next.js · self-hosted embeddings
Runs onA VPS I provision and operate. Docker, Coolify, GitHub Actions
StatusLive at vergabeautomat.de

The constraint that mattered most

A bid costs a small company days of work. So the question is not whether the software can rank tenders. It is whether a sales lead will believe the ranking enough to skip the ones it rejected. If the answer is a single number with no explanation, they check the portals by hand anyway, and the product has changed nothing.

DecisionFive weighted signals, scored separately. Four of them are shown to the customer as their own bar, with the weight visible.
AlternativeAsk a model for one relevance score. Far less code, demos beautifully, and it is what most tools in this market do.
What it costMore interface, more explaining, and a worse first impression. One number looks confident. Five bars look like work.
What it boughtA customer can disagree with a specific signal, and I can change the weight instead of arguing.

The same logic drove a smaller decision I am more attached to. When a signal cannot be computed, because a notice omits the contract value for instance, it says so instead of scoring zero. A missing value and a bad value are different facts, and collapsing them is how a scoring system quietly starts lying.

How it works

TEDEU tender databaseREGIONAL PORTALS100+ German sourcesDE-DUPLICATEnightlySCORED PER CUSTOMERCPV code match.32meaning.27region.18contract value.13wording.10the first four are shownRANKED, WITH REASONSemail and dashboardSIGNAL CANNOT BE COMPUTEDsays so, never scores zero
Fig. 01 — the weights are the ones in production, and the customer sees four of the five

The product

The tender list, 2,734 notices, best matches first. Each row shows the buying authority, the deadline, the contract value and a badge naming the source it came from, TED or a regional portal.
Fig. 02 — the ranked list a customer opens in the morning. Every row names its source.
A single tender notice. A combined score of 81 per cent sits top right; below it the buying authority, the published date, the estimated value and the sources it was found in, then a generated plain-language summary of what the contract actually asks for.
Fig. 03 — one notice. The combined score, top right, is the number this page says should go.

For the technical reader

How it is built, enforced and checked

Architecture

Ports and adapters, so the scoring core has no idea where a notice came from or where the result goes. Embeddings run locally rather than through a paid API, because at this volume the per-call cost decides whether the product has a margin. 30 decision records explain the choices that are not obvious.

Enforcement

The deploy pipeline starts the background workers under a deliberately broken environment first, reproducing an outage I actually had, so the same failure cannot ship twice. A separate check asserts the AI tooling's own configuration: no network access, write access scoped to the workspace, no payment integration enabled. It exits non-zero and stops the build.

Checked

260 test files across the service and the interface. Tenant isolation is checked at the database layer rather than trusted at the query. There is a signed data-processing agreement template, because customers are companies and their tender history is commercially sensitive.

What is not finished

A dependency rule that describes the layering exists and runs on demand, and it does not gate the build yet. That is the next thing I am wiring in, and until it is wired in I would not claim the architecture is enforced here the way it is on UM Schedule. The product also still shows a combined score alongside the individual signals, which I have decided to remove and have not removed.

Happy to walk through any of these in detail, including the parts that went wrong.