Vergabeautomat

German public contracts are spread across the EU tender database and the federal notice service. Vergabeautomat reads both every day and helps companies find relevant public tenders.

My own product. I do all of it: product, code, infrastructure, support.

Status on 30 September 2026
Stage
Live, pre-revenue
Site
vergabeautomat.de
Not yet
Revenue. There is no paying customer.
A single tender notice. A badge above the title reads TED (EU) plus BKMS (DE). The decision card is headed Ihre Entscheidung and carries two buttons, Bieten and Nicht bieten. Below it a panel headed Warum wird diese Ausschreibung angezeigt? says there is no overall score and no ranking, only the individual signals behind this notice's visibility, and shows four bars for how far each one matched: CPV at 50 per cent, Semantik at 50 per cent, Region at 80 per cent and Wert at 70 per cent.
One notice. Each bar shows how far one signal matched, and there is no total. The decision is two buttons.
The tender notice list in the product's German interface. Each visible row gives the notice title, the buying authority, the region, the estimated contract value, the deadline and an assignment status reading Nicht zugewiesen; most rows also carry a procurement category and a NUTS code. Values ascend down the rows in frame, from 3,586 to 141,015 euro. No column carries a score, a match percentage or a rank. Two black bars cover the signed-in account's e-mail address.
The notice list, sorted by value because I clicked that column. No column carries a score or a rank, and the account e-mail is redacted.

Sources

Two integrations cover the two halves of the market. TED is the EU’s tender database for contracts above the EU threshold, and it publishes 700,000+ notices a year across the EU. Below the threshold, German contracts go to the federal notice service, BKMS, which 100+ platforms must publish to by law. One integration covers the German half, so I did not write a parser per portal.

Search

Search runs three arms in one PostgreSQL database: exact words, a substring arm that matches each word’s German stem prefix, and meaning through pgvector. PostgreSQL does not split German compounds, so a search for Automatisierung could not find Prozessautomatisierung until the substring arm existed. Adding a keyword score to a similarity score put the notices containing the exact words typed at the bottom of the list, so the arms are now fused by rank position. The embedding model runs in my own container, which keeps the cost per search inside the product’s economics.

Trade-offs

I removed the relevance verdict

Five weighted signals used to add up to one verdict on each notice. The only relevance labels were ones I wrote myself, for one company profile, and I could not validate the weighting against them well enough to defend it. I removed the verdict and kept the signals visible, so the user of the product sees why a notice appeared. It makes the screen busier and the first impression weaker.

A missing value says so

When a notice omits the contract value, the value signal says it cannot be computed instead of scoring zero. A zero would count against the notice for information the authority did not publish.

No confidence threshold for codes

The product suggests procurement codes from a company’s own description. The first measurement had tested a different design from the one that shipped. Measuring the shipped one showed correct and wrong codes scoring in overlapping bands, and the top-scoring candidate was wrong. No threshold can separate them, so the screen pre-selects nothing and the company chooses.

Built with

Code
TypeScript, Next.js
Database
PostgreSQL with pgvector
Embeddings
Self-hosted, in my own container
Runs on
A VPS I provision and operate. Docker, Coolify, GitHub Actions
Records
51 decision records and 316 test files, on 4 October 2026