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.
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.
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
The product


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.