Here's a problem that sounds made-up until it lands on your bill: an AI agent gets stuck in a loop overnight and quietly spends real money, sometimes thousands of dollars, before anyone's awake to notice. It's not hypothetical; it has happened to people, and as agents get more autonomous it'll happen more.
AI agents have started spending real money: x402 micropayments, paid APIs, LLM tokens. The payment frameworks that give them wallets (AWS Bedrock AgentCore Payments, x402, Stripe/Privy) all enforce a spending limit per session. That's the easy half. The hard half is the one nobody ships: an org-wide, cross-agent, cross-region budget that cannot be overspent, with an audit trail that survives a finance review.
"Cannot be overspent" is a strong claim. Most systems enforce it in application code (read the balance, check it, write the spend), and that check is a race waiting to happen. I wanted the guarantee to come from the database, not from application luck. This post is about why that led me to Amazon Aurora DSQL, and the one design decision that everyone (including me, at first) gets wrong.
The invariant
A spend that would breach a budget cap fails the database transaction.
Concretely: a spend is an atomic, double-entry write that debits the agent's budget and credits the vendor in one ACID transaction. If two agents race the last dollar, exactly one commits; the other's transaction fails and is recorded as a denial. The balance never goes negative. There is no window in which an overspend is briefly visible.
Why Aurora DSQL specifically
I evaluated all three eligible AWS databases against one question: can the database itself reject the overspend under concurrent, multi-region writes?
DynamoDB global tables are eventually consistent: last-writer-wins across regions. Two agents in two regions can both "succeed" and the budget silently goes negative during replication. Out.
Aurora PostgreSQL Global Database is strongly consistent but single-writer: one primary region, read-only replicas. You can't have agents in
us-east-1andus-east-2both writing the same budget with a consistent outcome.Aurora DSQL is the only one that offers active-active, multi-region strong consistency. A writer in each region hitting the same balance resolves to one consistent result. It's PostgreSQL- compatible, uses optimistic concurrency control (OCC) with snapshot isolation, and AWS markets it for exactly this: global-scale financial transactions.
So the database isn't a storage detail here; it is the safety feature. Swap it out and the core guarantee breaks.
The non-obvious part: DSQL has no row locks
This is where the naive design fails silently.
In normal PostgreSQL, you'd enforce this with a pessimistic lock:
BEGIN;
SELECT balance FROM accounts WHERE id = $1 FOR UPDATE; -- serialize everyone here
-- check balance, then insert the spend
COMMIT;
DSQL has no SELECT … FOR UPDATE. There are no pessimistic row locks at all; it's pure optimistic concurrency. The textbook spend gate literally cannot be written. If you port it straight over, the FOR UPDATE is rejected, and if you drop it, you have a race.
It gets worse if you reach for the "clean" accounting design. The orthodox double-entry ledger is append-only: you only ever INSERT immutable entries and derive the balance with SUM(). That is beautiful, and under DSQL's snapshot isolation it is unsafe. Two concurrent spends each read the same snapshot ("balance = $1"), each INSERT a different new row, and neither transaction touches a row the other wrote, so OCC sees no conflict, both commit, and the budget goes negative. The purest design quietly breaks the one invariant you care about.
The fix: make the contended row the lock
OCC only conflicts when two transactions write the same row. So you have to give them one to fight over. The design that actually holds:
Keep the immutable, append-only
entriestable (that's the audit log and the hash chain).Also keep a mutable
balanceon the account, and have every spendUPDATEit in the same transaction.
// inside one transaction
await tx.insertEntry(debit); // append-only, immutable
await tx.insertEntry(credit);
await tx.updateAccount(budget.id, budget.balanceMicro - amount, debit.hash); // the contended row
Now two racing spends both UPDATE accounts WHERE id = <budget>. DSQL detects the write-write conflict and the loser fails with SQLSTATE 40001 (serialization failure). The application catches 40001, retries against the fresh balance, and this time the cap check sees $0 and denies. The mutable balance row isn't a denormalization shortcut; it is the lock, reconstructed out of OCC because no real lock exists.
The same trick scales up the budget hierarchy. Caps cascade org → team → agent, and a spend rolls up every ancestor's balance in the same transaction. So two agents in different teams racing the shared org budget both write the org row and serialize on it. The hierarchy gives you the conflict points for free.
for (let attempt = 1; ; attempt++) {
try {
return await store.transaction((tx) => settle(tx, request));
} catch (err) {
if (isConflict(err) && attempt <= maxRetries) { await backoff(attempt); continue; }
throw err;
}
}
Proving it on the real cluster
A claim like "never overspends" is worthless without an adversarial test on real infrastructure. So the most important test in the repo spins up 6 agents across us-east-1 and us-east-2 and races them all at one near-empty budget on the live multi-region cluster. The assertions:
exactly what fits the budget commits, the rest deny;
the denials are driven by real OCC
40001s (not application-side rejections);the balance is floored at zero, never negative;
the double-entry sum is zero and the hash chain verifies.
It passes, repeatedly, across regions. That's the difference between "we validate spends" and "the database refuses to let the budget break."
Architecture
The domain core is pure and dependency-free: the ledger, policy engine, hierarchy walk, and hash chain don't import pg or anything AWS. A thin adapter implements a generic Store interface over Aurora DSQL using the pg driver and the DSQL Node connector (which mints IAM auth tokens automatically, no hand-rolled signing). The same spend() logic runs against an in-memory OCC model for offline tests and against live DSQL in production. Swap the adapter, the core is untouched. The frontend is Next.js + Tailwind on Vercel; a natural-language query box turns "how much did marketing's agents spend on data APIs?" into a constrained, parameterized query over the ledger (never raw SQL).
What I'd tell the next person building on DSQL
You can't lock. Design for it. If your correctness depends on serializing access to a value, you need a single contended row and an OCC retry loop, not a lock.
Append-only is not automatically safe. Under snapshot isolation, two inserts don't conflict. If an invariant spans rows, give the writers a shared row to collide on.
Treat
40001as normal control flow, not an error. Retry with backoff; deny when the retry shows there's genuinely no budget left.Keep the core on GA features. Strong consistency, OCC, ACID, JSON columns, the IAM-aware Node connector are all GA. I kept preview features (CDC → Kinesis streaming) as a degradable layer so the guarantee never depends on preview.
The result is a budget that the database itself won't let you break, and an audit trail that can answer a CFO's question in plain English. That's only possible because of how Aurora DSQL handles concurrency, which is the whole point.
If you want to poke at it yourself: the live console runs on the real multi-region cluster, the SDK is trystub on npm (drop it in with npm i trystub), and the full source (including that cross-region overspend test) is on GitHub.
I'm happy to talk through any of it.
Built with Amazon Aurora DSQL, Next.js, and Vercel for the H0 hackathon. #H0Hackathon
