Anatomy of a Governed Finance-Agent Platform Artifi founder details the architecture of a governed finance-agent platform where autonomous agents process supplier bills, classify bank transactions, and prepare month-end closes in production. The platform ensures agents never write directly to ledgers, routing all writes through a risk-scored approval gateway to prevent compounding errors and maintain auditability. Key design choices include an event-driven queue with Postgres LISTEN/NOTIFY for low-latency processing and explicit state machines for governance. This is part 1 of Building the Agent Platform, a three-part long read on how a production finance-agent platform actually works. Part 2: Agent blocks. Part 3: What production finance agents do all day. Everyone is building AI agents this year. Very few of those agents are allowed to touch a general ledger — and the distance between "an agent that drafts things in a sandbox" and "an agent whose output an auditor will examine" is almost entirely architecture, not prompts. For the past year I've been building Artifi, a finance platform where autonomous agents process supplier bills, classify bank transactions, reconcile payments, run billing, and prepare the month-end close — in production, on real companies' books. This article is the architecture write-up I wish I'd had when I started: the runtime, the control surfaces, the cost mechanics, and the failure modes, with real numbers throughout. Nothing here is theoretical; every mechanism exists because something broke without it. The one constraint that shapes everything A finance agent's write target is a double-entry ledger. That single fact cascades into every design decision, because a ledger has properties most agent playgrounds don't: Mistakes compound. A misposted invoice doesn't just sit there; it flows into VAT returns, aging reports, and the P&L. Everything must be attributable. "Who posted this, based on what document, approved by whom" is not a nice-to-have; it's what an audit is . Some actions are irreversible in practice. You can reverse a journal entry, but you cannot un-send a payment or un-file a tax return. So the platform's core principle is: agents never write directly. Every write goes through the same gateway humans use, gets risk-scored, and either flows low risk , waits for one approval moderate , or waits for multi-party approval high . The agent doesn't know it's being governed; it just calls submit like everyone else. That's the whole trick, and the rest of this article is the machinery that makes it work at acceptable cost and reliability. Events, not cron The runtime is a queue. Five kinds of sources feed events into one table: inbound email, schedules, connector syncs bank data arriving , downstream rules, and agents themselves more on that later . A single long-lived worker — the event processor — consumes the queue and dispatches agents. Why a queue instead of cron jobs calling agent scripts? Because a queue gives you, for free: retry with backoff ours: 60s → 300s → 900s, three attempts , an audit trail of every trigger, deduplication SHA-256 of the payload, unique per organization and event type — and deliberately only active events block duplicates, so a failed event can be retried , backpressure, and the ability to investigate and replay any failure. Cron gives you none of that; it gives you a log line. The latency trick worth stealing: Postgres LISTEN/NOTIFY . An AFTER INSERT trigger on the events table fires a notification; the worker holds a dedicated listen connection and wakes immediately, with a 30-second fallback poll in case the listen connection drops. Event pickup went from ~2.5 seconds 5-second polling to ~100ms, with no new infrastructure — no Redis, no Kafka, just the database you already have. For a finance workload's volumes, this is enough, and every component you don't add is a component that can't fail. Every event and every agent run is a state machine with honest states. An event can be pending , processing , completed , failed , ignored no agent wanted it , expired 7 days , or duplicate . An agent instance can be pending , running , completed , failed , cancelled , timeout — and one more that carries most of the governance weight: waiting approval . When an agent finishes its work but its submitted writes are sitting in an approval lane, the instance isn't done — it's waiting for a human. The workflow engine completes or fails the instance when the human decides. That bidirectional linkage agent runs know their workflows, workflows know their agent is what lets you answer "what did this agent actually change, and who signed it?" in one query. Agents are files, not database rows Every agent is a directory in the repo: a config.yaml and a prompt.md . The config declares what the agent is allowed to be : agent type: bank transaction processor model tier: standard fast / standard / advanced execution mode: code llm / code / hybrid max concurrent runs: 3 timeout minutes: 30 allowed workflows: - transaction.post - bank statement line.match allowed tools: - search - list entities - submit trigger events: - connector.data received - bank statement.manual import On startup the worker syncs files to the database. Two fields are never overwritten by sync — the agent's API key and its enabled flag — so operators keep runtime control while everything else lives in git. This buys you the things engineers already trust: version control for prompts, code review for behavior changes "this PR makes the bill processor stop auto-creating vendors" is a reviewable diff , and identical definitions in every environment. Notice what the config amounts to: least privilege for a non-human worker. allowed workflows is the agent's write permission set. allowed tools is its read surface. trigger events is when it's allowed to exist. max concurrent runs is how many of it may exist at once. If you've done IAM design, this is familiar — and that familiarity is the point. Agents are colleagues with narrow job descriptions, not a general intelligence with a database connection. Three execution modes, because $3 per bank statement is absurd The most consequential lesson in the whole platform, and it's about money: most finance work is deterministic, and paying an LLM to do deterministic work is engineering malpractice. Agents run in one of three modes: Early on, the bank transaction processor was an LLM agent: ~$3 per statement run. Rewritten as a code agent that calls a small model Haiku only for the genuinely fuzzy 10% — "which vendor is 'AMZN Mktp DE 2K4' really?" — it costs about $0.01 per run. That's a 99.7% cost reduction with higher reliability, because the deterministic 90% now behaves identically every time. Two other agents configuration, master data got the same treatment for ~90% savings. The execution mode field means switching an agent between modes is a config change, not a rewrite. The heuristic that emerged: LLM for reading, code for arithmetic, human for judgment above a threshold. Document extraction, classification of ambiguous text — LLM. Matching, posting rules, date math, batch assembly — code. Anything the agent isn't confident about, or that crosses a risk line — route to a person the next section . Systems that get this split wrong in either direction fail differently: all-LLM systems are expensive and non-deterministic where they least can afford it; all-code systems can't read an invoice. Model tiers follow the same logic: fast Haiku-class for extraction and classification, standard Sonnet-class for judgment-heavy work like GL account selection, advanced reserved for the rare genuinely hard reasoning. Each targeted small-model call costs around $0.001. The 664-tool problem Here's a scaling problem nobody warns you about. Our MCP server exposes roughly 664 tools — the full finance surface: master data, transactions, banking, reconciliation, reporting. Each tool definition costs 500–1,000 tokens of context. Give an agent all of them and you've spent 300K–650K input tokens per request before the agent has read a single word of the actual task — plus you've bought worse tool selection and slower inference. The fix is boring and essential: allow-lists. The bill processor needs exactly 9 tools; with the allow-list applied, its tool context is ~4.5K tokens. That's a two-orders-of-magnitude reduction, applied at the SDK level by computing the complement everything not allowed becomes explicitly disallowed and additionally blocking 16 built-in tools — Bash, file access, web fetch — that no autonomous finance agent has any business holding. Two production lessons from this machinery, offered as warnings: The naming trap. The SDK namespaces tools as mcp