Inspiration
You agree to buy 10 monitors at $180 each. That agreement is a purchase order. The vendor then invoices you at $210.
Catching that means someone opening the invoice, finding the PO, comparing every line, spotting a $30-per-unit markup, and writing the dispute. Times hundreds of invoices a month. It's 15+ hours a week of work that finds overcharges after the money has already left.
That's not a chatbot problem. Nobody wants to ask about invoices - they want the reconciliation to have already happened. So I built an agent that does the work and only interrupts you when the money genuinely warrants it.
What it does
A vendor document lands in a Cloud Storage bucket. That arrival is the trigger - there's no button to press.
- Gemma triage - a cheap open model answers one question first: is this a procurement document at all? A confident "no" declines it before anything expensive runs.
- MultimodalVisionAgent - Gemini 3.5 Flash reads the scan into a schema-enforced extraction: vendor, line items, unit prices, tax, totals, signature.
- ContractAuditorAgent - pulls the matching purchase order from Firestore and reconciles every billed line, flagging price inflation, quantity mismatches, and items no PO approved.
- DiscrepancyDispatcherAgent - decides, and writes the document that carries the decision out.
The $500 variance threshold is the autonomy boundary:
| Outcome | When | Human |
|---|---|---|
AUTO_APPROVED_PAYOUT |
Totals match the contract | No |
GENERATED_DISCREPANCY_REPORT |
Variance ≤ $500, nothing unauthorized | No |
ESCALATED_TO_HUMAN_FINANCE |
Variance > $500, unauthorized item, or no matching PO | Yes |
Below the line Documa drafts and dispatches the vendor dispute itself. Above it, it stops and asks - with the evidence already assembled. People stop reviewing invoices and start reviewing exceptions.
How I built it
Three agents run as a sequential pipeline on the official Google Antigravity SDK harness,
each handing typed state to the next through a shared AgentState, with every step recorded in
an execution log returned with the response.
Gemini 3.5 Flash runs on Vertex AI with schema-enforced structured output, so the model fills a Pydantic contract rather than emitting JSON I have to parse out of prose. Firestore holds purchase orders, audit logs and dispute records. Cloud Storage takes intake, Eventarc turns a file arriving into an invocation, and the whole thing runs on Cloud Run, scaled to zero. Cloud Build and Artifact Registry handle the pipeline. The frontend is hand-written HTML and CSS - no framework, no CDN, no build step.
Challenges I ran into
An invoice is untrusted input. The Antigravity harness enables filesystem and shell tools by default - unacceptable for an agent whose input is a third-party document that could carry text aimed at the model. I disabled all twelve non-terminal tools, leaving it able only to perform inference. That fix then broke structured output, because a blanket deny also blocked the harness's own terminal tool; the mechanism that emits structured output. The lesson was that a security control has to be scoped to the threat, not applied with a hammer.
Graceful degradation turned out to be a liability. My first version caught every exception and fell back to demo data. It felt robust and was the most dangerous thing in the codebase: a broken model call was indistinguishable from a successful one, and any unrecognised upload came back as a confident $3,250 invoice that had never been read. Provenance is now a first-class field, and a strict mode makes failure raise instead of degrade.
Wiring up a button found a data-corruption bug. The human sign-off control claimed it updated
Firestore without calling any API. Connecting it broke the app instantly: the handler wrote the
human's ruling into action_taken, a field typed to an enum with no such member, so one click
permanently corrupted the record and every later read returned 500. The root error was
conceptual - conflating what the fleet decided with what a human later ruled.
Two Vertex AI gotchas cost hours. Application Default Credentials need a quota project set
explicitly, or requests are attributed to a shared Google project and every model returns 404.
And Gemini 3.x publisher models are served from the global endpoint, not a region -
us-central1 returns 404 for gemini-3.5-flash.
Accomplishments that I'm proud of
The minor-overcharge case: a real $300 overcharge caught, disputed, and formally resolved with nobody in the loop. That's the difference between an agent that reports a problem and one that resolves it.
And the honesty of the thing. Every figure on the site is real; the landing page counters read live from the Firestore audit trail rather than hardcoded copy, and the deployed service runs in strict mode so it cannot show a simulated number.
What I learned
Cheap models earn their place in front of expensive ones. Putting Gemma in front of Gemini to answer one narrow question turned model selection into an architectural decision, not just a quality one.
Autonomy needs a stated boundary. The hardest design question wasn't what the agent could do but what it should do without asking. Making the threshold explicit, configurable and visible turned "how autonomous is it?" from a vague claim into a number a finance team can agree to.
Test what you ship, not what you hope. My suite had quietly started making live Vertex calls and opening real Firestore clients; 16m48s per run, costing money, failing offline. Isolating it properly brought it to 0.38 seconds.
What's next for Documa
Authentication and per-tenant purchase orders - it's a hackathon build with open CORS and no auth today. Direct ERP write-back instead of CSV export. Multi-page PDF contracts with clause-level reconciliation, not just line items. And a learning loop where human overrides feed back into the threshold, so the autonomy boundary tunes itself to each finance team's risk appetite.
Log in or sign up for Devpost to join the conversation.