Inspiration
UK public bodies publish thousands of tenders a week. Two or three are worth your time, and you can't tell which from the headline, whether you qualify is on page 14 of a selection questionnaire. £5m employers' liability, ISO 27001, two years of comparable references.
So bid teams either set keyword alerts and drown, or stop looking and miss the one they'd have won. The expensive part was never writing the bid. It's the triage: read everything, check it against a fixed set of rules, decide go or no-go. That's judgement plus grind, which is exactly what an agent should own. I've been thinking about this problem for a while, and Zentrow is the first agent I've built.
What it does
Give it a company number or a website, and it runs without you.
An onboarding agent enriches from Companies House, filed accounts, your website and any certificates on file, then interviews you only about the gaps it couldn't fill. Finishing that publishes an event; the matcher wakes on it and starts working before you ask for anything.
It reads every open tender in full, annexes included, and returns Bid, Conditional ("upload your ISO 27001 and this becomes a bid"), or No-bid with the number you're short by. You schedule a report once and the shortlist keeps arriving daily or weekly, the agent carries on after you close the tab.
How I've built it
Four autonomous services over one database, all on Google Cloud in the UK region. A Cloud Run job syncs OCDS daily from both official APIs. Gemini 3.5 Flash reads the tender PDFs by URI and extracts structured requirements; embeddings sit in AlloyDB behind a ScaNN index. Two Google ADK agents handle onboarding and matching, woken by Pub/Sub, with Cloud Scheduler driving delivery through the Gmail API.
The matcher agent runs in three stages and the separation is the point.
Shortlist — vector search narrows the open corpus. Cheap, no model.
Gate — plain Python, no LLM. PASS/FAIL/UNKNOWN on every mandatory requirement: turnover, insurance limits, certifications, expiry against contract start. The verdict is computed, not generated, so it can't hallucinate a pass and you can check any call by hand against the clause.
Adjudicate — Gemini, doing the part nothing else can. Gemini isn't decoration here, and it isn't writing the verdict either. It does the two jobs that are genuinely language problems. First, reading procurement prose: "the supplier shall hold public liability cover to a minimum of five million pounds" has to become a comparable number, and the same requirement is phrased forty different ways across forty authorities. Regex can't do that; a schema can't do that.
Second, and this is what made the product work, it's judging line of business. Vector similarity could tell us a fleet-admin firm looked close to a research-evaluation contract, because the language overlaps. It's a plausible neighbour and a terrible recommendation. Deciding whether a tender sits inside what a company actually does, versus something it merely shares vocabulary with, is a reasoning task about meaning. That's the agent's call, made against the enriched profile, and it's the difference between a filter and advice.
So: Python owns filtering and Gemini owns interpretation and judgement. Neither does the other's job.
Stack: Gemini 3.5 Flash on Vertex AI, gemini-embedding-001, Google ADK, AlloyDB + ScaNN, Cloud Run (Jobs + Services), Pub/Sub, Cloud Scheduler, Secret Manager, Gmail API, Next.js.
Challenges I ran into
The things that hurt weren't the hard problems. They were the quiet ones that either failed silently or waited for the worst possible moment to show up.
The clearest one: the matcher's vector search quietly didn't work for a while, and nothing told me. I'd embedded the tenders with one model and the company profiles with another, so I was comparing vectors from two unrelated spaces. That doesn't throw an error it just hands back reasonable-looking matches scoring around 0.03. Every tender looked mildly relevant, none looked right, and I nearly shipped it because it ran. Putting both sides in the same model pushed the scores up to a real 0.55 and above, and the shortlist snapped into focus.
The next thing I got wrong was trusting the model with the actual decision. My first matcher just asked Gemini to read the tender and the profile and say bid or no-bid. It was fluent, it was persuasive, and it would happily talk itself into bidding on work the company obviously couldn't do, at one point telling a fleet-management firm to bid on a National Trust research-evaluation contract, because on paper it cleared the boxes. I learned to split two questions it kept blurring: are you allowed to bid, and could you actually win this. The first is deterministic Python now, which the model can't overrule; the second is its own separate judgement. The recommendations only started feeling like advice once I started asking Gemini to explain, not only make the decision.
But the real running lesson was how much of "an agent that runs on its own" is small, unglamorous wiring that bites you late. My local setup script resolved paths differently under zsh than bash and silently broke every import. And the two worst ones landed while I was trying to do demo video: a Pub/Sub topic that had never actually been granted publish permission, so onboarding died with PERMISSION_DENIED the first time I clicked it live and right after that, a StaleSessionError, because the background enrichment and the chat window were both writing to the same agent session at once, a race that only shows up under real concurrent use and never in testing. Every fix was tiny. Making an agent autonomous turned out to be a long chain of these small, exact details. I had a big vision and almost gave up at the end, had to simplify everything I could to actually progress.
Accomplishments that I am proud of
The gate. A verdict a judge can verify by hand, in a system built around an LLM.
The agent runs unattended end to end, which enriches, interviews, matches, adjudicates, and emails with nobody watching.
And the line-of-business reasoning, which is the thing that turned a filter into something that reads like advice.
What I've learned
Let the model judge, not decide. Our first matcher asked Gemini for the whole verdict. Fluent, confident, but often wrong it would talk itself into a bid on work the company clearly couldn't do. Prompting didn't fix it. Moving pre filtering on hard attributes out of the model did, and trust went up the moment the model had hadle what it does beat.
Relevance isn't fit. Clearing the paperwork and being a credible bidder are different questions, and you need a model to tell them apart.
Autonomy is mostly plumbing. The reasoning is the interesting part; a lot of moving pieces, like scheduling, sending emails and keeping all in sync.
What's next for Zentrow
Better document coverage on the notices pipeline can reach, more tuning of the line-of-business reasoning against real company profiles, and tracking outcomes on tenders the app recommended so the agent learns what actually converts. I need to improve the speed of making the shortlist, now it's around 3 mins. As agent still sends it by email it's not visible to user, but still would love to improve. I also plan to look at past tenders the company has won to understand what it's really looking for. There's plenty still to improve, and I would love to take the project commercial at some point.
Built With
- alloydb-+-scann
- cloud-run-(jobs-+-services)
- cloud-scheduler
- gemini-3.5-flash-on-vertex-ai
- gemini-embedding-001
- gmail-api
- google-adk
- next
- pub/sub
- secret-manager
Log in or sign up for Devpost to join the conversation.