JONAS STAMM
— FIELD NOTES
technical-deep-diveAugust 24, 2026· 7 min read

The hard part of a tender radar is not the matching

Most of the work in shipping BauGPT's tender radar had nothing to do with matching. Matching worked early and was never the risky part. Everything after that wa...

JS
Jonas Stamm
Founder, BauGPT

Most of the work in shipping BauGPT's tender radar had nothing to do with matching. Matching worked early and was never the risky part. Everything after that was about being correct in ways a construction company can actually bet on: which deadline is the real deadline, which company is allowed to see a match, and what happens when the scoring budget runs out halfway through a sweep. That list is the whole post.

Here is what the feature does today. It watches public tenders, filters them against a company profile, scores how well each one fits, and sends a daily digest of the matches. Every match in that mail can be dismissed with one click, signed so the link works without anyone logging in first. The prefilter runs after each ingest window rather than on its own schedule. It ships gated behind the Business plan.

Now the unglamorous list of things that were wrong with it.

The submission deadline was being derived from a duration. It now gets read from the tender's published opening date instead. That is a small change in the code and a large change in the product, because a Bauunternehmen plans a week of Kalkulation around that date. A tender alert that is confidently wrong is worse than no alert. Send two of those and nobody in the office opens the mail again.

CAVEAT

This work lives in a private repo, so what follows describes behaviour, not code. Everything here is drawn from the commit history of the last weeks, not from memory.

The second cluster was the tenant boundary. Notifications were not scoped tightly enough to the company doing the acting, and that got tightened. The gate that decides whether a company profile should be notified at all now carries a test proving that an unclear case results in no mail rather than the wrong mail. The same change let a corrigendum, the correction a public buyer publishes after a tender is already out, trigger a fresh notification instead of being swallowed as a duplicate. In German public procurement the corrigendum often is the news.

The third cluster was money. Scoring tenders against a company profile costs something per tender, so the fit tool got a hard cap on what it may spend, and the profile scoping was narrowed. The budget counter was made atomic, scores that are not numbers get rejected outright, and repeated reference snippets no longer ride along. Matches that were deferred because the budget ran out are now swept back into scoring later instead of quietly disappearing. The sweep is bounded by relevance, the month-based prefilter is gone, and the counts are proven rather than assumed. Then the logging: the summary line for each sweep came back, and it stopped over-reporting starvation, which had been making a healthy pipeline look sick.

The fourth cluster was delivery, which is the part customers actually experience. Digest jobs were being sent to a queue name that had no queue behind it. The digest render was hardened against mail clients that fetch links before a human touches them, against serving stale content, and against outside text trying to act as instructions. Failures in the digest now surface instead of resulting in silence, and a tender with no named buyer renders a placeholder rather than a hole. Most of this landed on a single day, 19 August, including one change whose only job was to catch what reviewing each task in isolation had missed, because every piece looked healthy on its own and the trouble lived in the gaps.

The second half of the work was Baurecht evals, which is where BauGPT either earns trust with German building law or does not. The structural fixes there were about who gets to decide. The judge model is now required, so a case passes only when the judge says it passes, with no fallback path quietly declaring success. And the Baurecht screen was demoted from scorer to diagnostic. It tells you something is worth looking at, it does not decide the grade.

That demotion came out of a specific embarrassment. A new family of harder legacy cases went in, with a rubric of its own and one flagship case to anchor it, and the corpus was brought up to a floor of eight cases so the family could not pass on a lucky single example. Then four of its traps turned out to be firing on answers that were actually correct, and those got removed. An eval that punishes a right answer is a bug in the eval.

"An eval that punishes a right answer is a bug in the eval."

Running in parallel to all of that was the structured import for construction position data, which is where construction software goes to suffer. Field mapping for one record type had gaps, and a code for the quantity breakdown was missing entirely. A later review round found holes in the quantities and a bad error path for the older version of the format. None of these fail loudly. They produce an import that looks complete, with a few positions silently thinner than the original file, which is the exact failure mode a Kalkulator will not catch until the bid is already out the door.

One more from that corner, and it is my favourite kind of fix. The sanitiser that scrubs test fixtures now refuses to run at all when it cannot find the secrets it is supposed to scrub against, rather than doing its best and passing. Customer tender documents and cost data are the input here. A sanitiser that degrades quietly is a leak with a delay on it.

Both of those come from the same place. Construction data is not a feed of posts, it is a bid file that somebody signs their name under. A position list that imports almost completely looks like a success in a dashboard and like a lost tender in the Kalkulation. So the pattern repeating across these commits is always the same: stop guessing values that exist in the source, fail loudly instead of degrading, and never let a helper quietly turn into a weaker version of itself. Read the deadline from the field, prove the gate blocks, review across tasks and not only inside them, let the judge decide.

The same instinct showed up on the enterprise side in the same window. Seat limits and cooldown on the directory sync now warn by default instead of blocking. That one is worth saying out loud, because it looks like the opposite of everything above. An internal quota should not be the thing that stops a customer's employee directory from syncing on a Monday morning. Fail closed on permissions and correctness, fail open on our own housekeeping.

If you are a construction company evaluating AI vendors, this is the layer worth asking about. Anyone can demo a chat window that answers a VOB question in fluent German. Ask them where the submission deadline comes from, what happens when their scoring budget runs out mid-sweep, whether a corrigendum reaches you, and whether their evals have ever marked one of their own correct answers as wrong. The answers tell you more than the demo does, because the demo is the easy half.

— END · Field Notes —

Keep reading

All writing →
technical-deep-dive · Aug 31, 2026
01

The hard part of a tender radar is not the matching

Most of the work in shipping BauGPT's Ausschreibungs-Radar had nothing to do with matching. Deciding whether a public tender fits a Bau company's profile was th...

7 MIN READ
technical-deep-dive · Aug 20, 2026
02

What Bau Enterprises Actually Made Us Build

Most of what we shipped on BauGPT in the last weeks had nothing to do with the model. It was folders, permissions, single sign-on, and Word files that open corr...

7 MIN READ
technical-deep-dive · Aug 20, 2026
03

Wide tables, umlauts, and who is allowed to write into a folder

If you want to know whether an AI product is really running inside a construction company, don't ask about the model. Ask what broke last week. For us the answe...

7 MIN READ
— THE NEWSLETTER

One note a week.
No fluff, just what works.

AI engineering, growth hacks, and messy lessons from shipping BauGPT. Unsubscribe anytime. I'll even miss you.

FIELD NOTES · NEXT ISSUE DROPS MONDAY
↳ No spam. One note weekly.