Inspiration

Every company pays invoices, and every company has the same quiet weakness: the checks are advisory. Someone flags a problem, the payment goes out anyway, and the flag ends up in a log nobody reads.

I wanted to find out whether a fleet of agents could actually hold a line — not recommend, not annotate, but stop.

Three days before this deadline I stopped adding features and read all 1,900 lines of my own system, start to finish, checking every assumption against what production was really doing. What I found was that my own fraud agent couldn't stop anything. It found duplicate invoices, said so clearly, recorded the anomaly — and the payment went out anyway. The agent hadn't failed. It had no power.

That bug became the whole point of the project.

What it does

Aegis Ledger runs procure-to-pay end to end with five specialised agents:

  • Procurement matches an invoice against its purchase order. It cannot read banking data.
  • Compliance screens for duplicates and anomalies, and writes a verdict that other agents must obey.
  • Finance validates budget and schedules payment — but reads Compliance's verdict before it runs.
  • Negotiation settles invoice-versus-PO differences within a fixed 10% authority, counter-proposing the midpoint, and escalates to a human beyond that.
  • Sourcing watches stock levels and opens purchase cases on its own initiative, with no human uploading anything.

Every agent is catalogued in a registry with the exact data scopes it holds, and every sensitive tool checks that registry at runtime. po.adjust, payment.write and case.create each belong to exactly one agent. Authority is enforced server-side, not requested in a prompt.

How we built it

The fleet runs on Google ADK (LlmAgent + Runner) with Gemini 3.7 Flash through Vertex AI, deployed on Cloud Run. Firestore holds the agent registry, the per-case memory bank and the audit trail. Cloud Tasks carries the asynchronous handoffs, Cloud Scheduler triggers autonomous sourcing, and Model Armor screens every invoice's free text for prompt injection before any agent sees it.

Two more Google pieces earn their place. Gemma 4 acts as a cheap first-pass filter for the Sourcing Agent before Gemini spends reasoning on the final decision — confirmed in Cloud Trace as a real 200ms round trip, not a simulation. And the Sourcing Agent queries purchase history live over a self-hosted mcp-clickhouse MCP server it runs as its own subprocess, exploring the database on its own: listing tables, then querying, then deciding.

Everything is traced end to end with OpenTelemetry into Cloud Trace.

Challenges we ran into

The audit turned up four bugs, and reading them together they were all the same bug: every agent worked, and the system around them didn't respect what they decided.

Compliance had no veto. Case completion checked two booleans — finance_done and compliance_done — and compliance_done only means "Compliance ran", not "Compliance approved". The agent's actual conclusion went to an audit log nothing read. My first fix only stopped the case from completing; testing live showed the payment had already been scheduled thirty seconds earlier. Blocking the paperwork is useless once the money has left.

The agent flagged itself. Compliance and Finance were queued with the same delay, so dispatch jitter decided who ran first. When Finance won, Compliance found that case's own scheduled payment and declared it a duplicate of itself. Identical inputs, opposite verdicts — cases 162f228f and 5092f315 prove it.

Money arrived as a tool parameter. The settlement amount came in as a function argument, meaning the model read it from the prompt and typed it. Ninety-nine times out of a hundred it types the right number. I would rather not learn what the hundredth costs.

A blocking analytics write sat on the request path. I measured ClickHouse taking 37.6 seconds to wake from auto-suspend — inside an async handler, unguarded. The first invoice of any session froze the entire event loop.

Accomplishments that we're proud of

The fraud agent can actually stop a payment. Submit the same invoice twice and the second case lands in compliance_hold with a finance_skipped event in its audit trail. The payment is never scheduled. That is the difference between an advisory agent and a governing one, and it is visible live in the demo.

Zero-trust that is enforced, not declared. Calling propose_settlement while impersonating the Procurement Agent returns a denial with a data_scope_denied log. The registry is consulted at runtime, inside the tool, so no prompt can talk its way past it.

59 tests over deterministic logic, running in 1.5 seconds with no cloud credentials and no model calls. I started the audit with zero.

Every scenario verified against production, not mocked — the clean path, the duplicate block, an in-margin settlement at $4,985, an out-of-margin escalation, a poisoned invoice stopped by Model Armor, and an autonomous case opened by Cloud Scheduler with no human involved.

What we learned

Give every agent's verdict an edge. If an agent can flag but not block, it is a commentator, not a control. Trace the path from each conclusion to something that actually changes; if it ends in a log line, you have theatre.

Never let a consequential value arrive as a tool parameter. Read it from state and use the model's version only to cross-check. The model should decide what to do, not restate the facts it acts on.

Test the seams, not the agents. You cannot unit-test an LLM, but you can test every decision your system makes about the LLM's answer — and that is where the bugs actually were.

What's next for Aegis Ledger

Semantic duplicate detection with embeddings, so a fraudster changing one character in a vendor name doesn't walk straight through an exact-match check. And Document AI, so the fleet reads a real invoice PDF instead of a filled-in form.

None of this made the demo flashier. The system does exactly what it did before in the happy path. But it now does something it genuinely could not do a week ago: it stops.

Built With

Share this project:

Updates