TL;DR. Twenty agents close a $48M company's July books under one human controller. The Gemini agents that read invoices can only file schema-locked findings; the posting engine has no LLM in its import graph and no permission to open a document; every amount that posts comes from human-approved templates plus deterministic recomputation. Live and unedited on camera: a $4,880 misstatement caught at the row level, a $48,500 invoice-embedded payment redirect refused with zero ledger writes, and an $86,450 payroll accrual posted exactly once through a kill -9 — with 19 of the 20 agents running as Cloud Run services against Firestore and Gemini on Vertex AI.

In the video (one continuous live take): 0:35 the fleet and the registry of 20 · 1:05 the $4,880 tie-out — reported vs recomputed, the exact missing row · 1:35 the four approved journal-entry templates · 2:05 the poisoned invoice read live by Gemini vision · 2:35 taint chips, "0 journal entries written", then kill -9 and the stolen idempotency claim · 3:05 the hash-chained ledger and the GL walk · 3:30 the scribe's three-month recurring-variance briefing · 3:50 the Google Cloud Run console — all 20 services, project countinghouse-a09ef.

Inspiration

Larkstone Roasting Co. — fictional, $48M in revenue, 335 employees — has a finance department of three people, and one of them runs the month-end close alone. That controller is the Unlikely Hero this was built for: not a department with a close team, but one person at a company too small to have one, doing twenty people's close with a fleet.

We looked at what the market sells that controller. Every close-automation product we surveyed — a dozen funded players — pitches more autonomy: agents that sit inside the general ledger, agents that post, agents that approve. The governance story is permissions, human sign-off, and certifications. Policy, not architecture. Meanwhile invoice-embedded prompt injection is a documented, in-the-wild attack class, and prompt injection has topped OWASP's LLM list two editions running. A publicly traded close vendor launched its agentic product this spring by naming "AI's governance and trust gap" as the blocker. That sentence is the brief. We wanted to build the mechanism, not the policy.

The domain has had the answer for five hundred years. Segregation of duties: the person who opens the mail is never the person who signs the checks. We asked what it would mean to enforce that in code, between agents, on every database call.

What it does

Countinghouse closes Larkstone's July books with twenty agents across five roles, running as separate processes against Firestore, watched by one controller from a read-only workspace.

  • Tie-out checkers (×7, no LLM) never trust the close packet. They recompute it — row-level AP↔GL reconciliation that names the exact unposted subledger row, payroll register re-sums, trial-balance integrity, a continuous hash-chain verification, and a materiality-driven flux scan that opens variance cases. That is the four live lanes plus the chain verifier; the cash, revenue, and depreciation lanes are pre-seeded green in the fixture universe, and the README says so in as many words.
  • Variance investigators (×10, Gemini via Google ADK, zero tools) read the evidence — including the actual invoice image as a multimodal part — and emit exactly one thing: a schema-validated finding. Explanation, confidence, quoted evidence, document ids, and one opaque template id. They own no collection. The capability matrix lets them write their finding and nothing else; they cannot touch the deterministic evidence the engine trusts.
  • The posting engine (×1, no LLM in its import graph — a test walks the graph) matches the template id against four human-approved journal-entry templates, runs the gates — context, preconditions, a typed amount-source allowlist, balance, caps, quarantine, period lock — posts exactly once inside one atomic transaction, re-runs the tie-out as a postcondition, and stops after two strikes. It has no verb at all on the documents collection. No model-authored number ever posts. No template can touch cash.
  • The supervisor registers every instance with version and approval provenance, watches per-instance heartbeats, releases the leases of dead workers so a respawn resumes from the checkpoint, and flips the fleet to deterministic template mode when Gemini is down or the token budget is spent — the close narrows to what code can prove; it never stalls silently.
  • The scribe writes deterministic close memos that pin the ledger's head hash (the memo anchors the chain) and cross-links recurring variances across months, so the third occurrence of an accrual miss arrives with its history.
  • The controller is a matrix row of her own: the only identity that can lock the period. Agents structurally cannot.

On camera we make the close harder three times, unedited: a trial-balance report that lies ($4,880 off; the recompute finds the row; the fix posts from the recompute, never from the model), a vendor invoice carrying a real, staged prompt-injection payment-redirect (contained by structure, escalated honestly, zero ledger writes — the attacker wanted $48,500 to cash; the only resolvable amount is the $3,900 intake stamp, and cash is unreachable), and a kill -9 of the posting engine after it has claimed its idempotency key and before it commits (the replacement steals the claim when the lease lapses and the $86,450 payroll accrual posts exactly once — the receipt on screen reads "2 claims on the key · 1 entry · 0 refused"). Then the scribe's briefing: the same utilities accrual missed three months running, with a recommendation the scribe files as data.

How we built it

  • Gemini Flash (3.5 on Vertex AI in the recorded take; 3.7 on the Gemini API rail) through Google ADK (LlmAgent, tools=[], an enforced output schema, temperature 0, token-capped, 15 s timeout), with the google-genai SDK types carrying the invoice PNG as an inline part. Every call draws a permit from a transactional token bucket in Firestore, so free-tier discipline is visible architecture. Recorded fixtures make the whole fleet run with no key; COUNTINGHOUSE_MODE=live is the take.
  • Firestore is the entire state bus: nineteen collections, a hash-chained postings ledger, idempotency keys, leases, the fleet registry, the budget, memos and priors. Every agent holds a ScopedFirestoreClient built from a single capability matrix (scopes.py) that whitelists role × collection × verb × doc id × update field and raises on anything else — inside transactions too. The same matrix renders the workspace's scope chips and generates the deployment's per-role service accounts.
  • Cloud Run runs the fleet as one image, one service per agent instance, one service account per role (20 services deployed to us-central1 on 2026-08-21), with the Gemini key in Secret Manager mounted only into the investigators — or, on the Vertex rail, no key at all and roles/aiplatform.user on the investigator account only. Every agent's heartbeat carries its runtime, so the workspace card stamps cloud run beside the container's pid when it runs there.
  • The books are generated, never typed. One fixtures module is the arithmetic truth; generate.py --self-test proves every invoice re-adds, clean books tie everywhere, and seeded books break at exactly the planted places by exactly the planted deltas. The invoice corpus is rendered from the same figures.
  • The controller workspace is a Vite + React + TypeScript single screen over Firestore listeners with one pure derivation layer and not a single write call. Firestore Security Rules make the browser read-only by Google's infrastructure, not by politeness.
  • Tests are the proof text. 474 Python tests in the repo — 154 Countinghouse fleet tests against the Firestore emulator, 53 on the generated Cloud Run deployment, the rest belonging to the frozen predecessor (see Lineage) — and 76 workspace tests. The crown jewel assumes the model is fully owned — an obedient-parrot provider that places the attacker's template, amount and bank details in every field it can reach — runs a full posting tick, and asserts the ledger head, the entries, and every GL balance are unchanged. Its siblings cover template-id smuggling (a real template named in the wrong context is refused by two independent gates), sign-flips, over-cap amounts, the three crash windows around the posting commit, and an import-graph walk that fails if the posting package ever pulls in a model client.

Challenges

Making "the model can't move money" a measurable property, not a vibe. The first version of the containment test was quietly checking that the model behaved well. We deleted it and wrote the version that assumes it is hostile. That forced the design to get stricter in three places we had not planned: the investigator's update on a case became field-masked (it may write its finding, never the probe receipt the engine trusts); the template library gained load-time invariants that kill the process on an unbalanced or cash-touching entry; and amounts stopped being anything a model could influence at all.

Posting an aggregate delta is a posting cannon. An external red-team review pointed out that "$4,880 off, post $4,880" is not how a controller thinks. We rebuilt the AP tie-out row-level: it reconciles subledger rows against GL interface batches and identifies the exact missing row, and the catch-up template's precondition demands exactly one identified row whose amount equals the delta — else escalate. The demo got more honest and more real at the same time.

Crash windows are not one window. A SIGKILL before the idempotency commit, on the ambiguous commit return, and after the commit are three different failure modes. We moved the idempotency verdict inside the posting transaction with the entry, the chain-head bump and the GL updates, so there is no instant at which the ledger and the exactly-once record disagree, and wrote one test per window.

Where the boundary is enforced — and where it is not. We say it plainly because an independent review pass made us: Google Cloud enforces one service account per role, read-only vs read-write Firestore IAM, secret-scoped access to the Gemini key (investigators only), internal-only Cloud Run ingress, and read-only Security Rules for the browser workspace. Google Cloud does not enforce the per-collection rule the demo is about — Firestore has no per-collection IAM — so "the poster can never read an invoice" is enforced by our ScopedFirestoreClient wrapper and its tests, generated into the cloud artifacts, and a process holding its role's credential could bypass it. Closing that gap is a mediating state-bus service holding the only database credential (or custom-token identities under the generated rules); it is designed and documented, not built for this entry. The README section "What enforces the boundary, and what does not" is the full statement.

Free-tier arithmetic shaped the architecture. Twenty instances heartbeating into a 20k-writes-per-day quota means diff-only receipt writes, a mode switch that writes only on change, and a token bucket with a reserve so memo prose can never starve an investigation.

Accomplishments that we're proud of

  • Segregation of duties as a code property: the agent that reads the invoice cannot post; the engine that posts cannot read the invoice; the controller alone can lock the period — each a test, not a sentence.
  • A containment story that does not depend on detection. The taint scan is advisory; the structure holds even if it misses.
  • All three scenarios passing end-to-end against real Firestore in 95 seconds, with a live Gemini vision call naming the right template and the exact cents from a rendered invoice.
  • A generated, self-proving dataset. No hand-edited number anywhere.
  • An honest-limits section we would stand behind in an audit.

What we learned

The model does the reading a human would do; the engine does the posting a human would fear. Determinism before intelligence: get the control plane right and adding AI is optional — get it wrong and AI only accelerates the failure. We also learned that every strong security claim we made got narrower as we tested it, and better for it: "privilege separation" became "an in-process capability matrix mapped to per-role service accounts"; "the model can't cause a posting" became "the model can only nominate from the approved library, and code decides, gates, and re-verifies"; "tamper-proof" became "tamper-evident, with the memo as the anchor." The narrower sentence is the one a controller can sign.

What's next

Per-role Firebase Auth claims so the generated Security Rules enforce the matrix on a token path, not just in-process; a mediating state-bus service for true per-collection enforcement with server SDKs; approval gates with the controller's decision written back through her own scoped role; an ERP intake adapter so the untrusted zone is fed by a real AP feed; and exporting the case trail and ledger as OpenTelemetry spans, which is a mapping rather than a redesign.

Lineage (disclosure)

Countinghouse re-domains Warden, an incident-response agent fleet this studio built earlier in the same submission window; the scoped client and capability matrix, hash chain, idempotency, leases, supervisor and budget governor came across and were re-domained onto month-end close, and everything the finance domain forced — row-level tie-out, the amount-source allowlist, field-masked findings, the cashless template invariants, the period lock, the generated books, and the controller workspace — is new. The prompt-injection patterns credit the openly licensed deepset/prompt-injections dataset.

Try it

uv sync
docker compose -f docker-compose.dev.yml up -d      # Firestore emulator
uv run pytest tests/ch -q                             # 154 Countinghouse tests (pytest -q runs all 474, incl. the predecessor's)
uv run python scripts/ch_e2e.py                       # the three scenarios, emulator
CH_TARGET=real GOOGLE_CLOUD_PROJECT=<your-project> uv run python scripts/ch_e2e.py   # on Google Cloud
cd workspace && npm i && npm run dev                  # http://localhost:5173/?mock=1

Built With

Share this project:

Updates

Submission history