Tacnode™
Back to Blog
AI Engineering

Agent Double-Spend: Two Agents, One Budget

Agent double-spend: two AI agents check one budget, both see room, both spend. Why provider budget alerts miss it, and how reserve-then-commit stops it.

Haitao Wang
Haitao Wang
Engineering
11 min read
One budget bar with a long spent segment and a cap line. A bracket shows both agents’ checks measuring the same room, and their spends of 1,800 and 2,200 stack 1,000 past the cap.

TL;DR: Agent double-spend happens when two or more agents check the same budget, each sees room, and each spends, so the total lands past the cap. Provider budgets don’t catch it: cloud budget data updates hours apart, and ad platforms treat daily budgets as averages. The fix is reserve-then-commit: each agent takes a hold against the budget in one atomic step before it spends, then converts the hold to actual spend or releases it.

You gave three infrastructure agents one project budget and a simple rule: check what’s left before you launch anything. Each agent follows the rule. At month end the project is over budget anyway, and every agent’s log shows a check that passed.

The checks were right. They were just right at the same time. Each agent read the same remaining amount, each decided its own launch fit, and nobody told the others. Spend-tracking dashboards caught it the next day, after the instances had been running all night.

This is the money version of a race condition, and it’s the one that gets noticed first, because it shows up on an invoice. This post covers why it happens, why the guardrails most teams reach for don’t stop it, and the reserve-then-commit pattern that does.

What Is Agent Double-Spend?

Agent double-spend is when two or more AI agents spend from the same budget concurrently, each after checking that enough remains, so their combined spend exceeds the budget. Every individual check was correct when it ran; the overshoot comes from no agent seeing the others’ spend before committing its own.

The name borrows from payments, where double-spend means spending the same funds twice. With agents it’s usually not one actor spending twice but several actors each spending the same remaining headroom once. The budget is a shared counter that can’t be merged after the fact, which makes it unmergeable state: you can’t reconcile two launches that already happened back under the cap.

Three Ways It Happens

Two agents, one envelope. The classic case. A search-ads agent and a social-ads agent share one campaign budget. A procurement agent and a travel agent share one team’s monthly spend. Each checks, each spends.

One agent, retried. A tool call that launches instances or buys inventory times out after the provider accepted it. The framework retries, and the purchase happens twice. Only one agent is involved, but the effect is the same. This one is solved with idempotency keys, not budget coordination.

Delegation. A lead agent hands sub-tasks to workers and tells each one “you can spend up to the remaining budget.” Every worker gets the whole headroom. Unless the lead carves the budget into separate allocations, the workers are back in case one.

Why Provider Budgets Don’t Catch It

The natural first guardrail is the provider’s own budget feature. It wasn’t built for this.

Cloud budgets run on delayed data. AWS documents that “AWS Budgets information is updated up to three times a day,” with updates typically “8–12 hours after the previous update.” The same page warns: “You might incur additional costs or usage that exceed your budget notification threshold before AWS Budgets can notify you.” An agent checking that number before launching is checking a value that may be half a day old.

Ad budgets are averages. Google Ads tells advertisers: “On a given day, your campaign might spend up to twice your average daily budget to take advantage of fluctuations of traffic.” The cap applies over the month: “you will have spent no more than 30.4 times your average daily budget” (Google Ads Help). That pacing is built around one campaign’s traffic, not around a plan that spans several of your agents. It means the platform won’t stop two of your agents from jointly overshooting a daily plan you set above them.

Prompts aren’t controls. “Never exceed the budget” in a system prompt makes each agent careful about its own spend. It can’t make the agent aware of another agent’s spend that hasn’t been recorded yet.

All three fail the same way: the check reads a value that doesn’t include spend already in flight.

A Worked Example

An illustrative scenario with constructed numbers. A project has a monthly cloud budget of 20,000. Spend recorded so far is 17,000, leaving 3,000. Two infrastructure agents pick up separate jobs.

text
t = 0.0 s    Agent A reads remaining = 3,000 (estimate for its batch job: 1,800)
t = 0.6 s    Agent B reads remaining = 3,000 (estimate for its load test: 2,200)
t = 11.3 s   Agent A launches instances; estimated cost 1,800
t = 14.9 s   Agent B launches instances; estimated cost 2,200
             Committed: 4,000 against 3,000 of room
t + 9 h      Provider budget report shows spend past the cap

The overshoot is 1,000 in this example, and nothing in either trace is wrong. Notice also when the first signal arrives: hours later, from the provider, after both jobs have been running.

Why “Decrement After Spending” Isn’t Enough

The next fix most teams try is to keep their own spend counter and decrement it after each purchase. That’s better than reading the provider’s number, because it updates immediately. It doesn’t close the race.

The decrement happens after the spend. Agent B’s check at t = 0.6 s runs before Agent A has spent anything, so there’s nothing to decrement yet. Moving the check closer to the spend shrinks the window; it doesn’t remove it. Both agents can still check, both pass, and both spend before either writes.

What has to change is the order: record the intent to spend before spending, in the same atomic step as the check.

What Digital Cash Already Worked Out

The double-spend problem is older than agents, and the Bitcoin whitepaper states it in terms that fit them: “The only way to confirm the absence of a transaction is to be aware of all transactions.” The common solution it describes is “a trusted central authority, or mint, that checks every transaction for double spending,” and “In the mint based model, the mint was aware of all transactions and decided which arrived first.” The paper’s objection to the mint is trust: “the fate of the entire money system depends on the company running the mint.” Its own solution removes the trusted party. Transactions are publicly announced, and a peer-to-peer distributed timestamp server gives participants a way to “agree on a single history of the order in which they were received” (Bitcoin whitepaper).

Your agents don’t need the timestamp server, because the trust problem doesn’t apply: you own the budget, so you can run the mint. They need the mint’s property: one place that sees every spend before it happens and decides which arrived first. A provider dashboard doesn’t see spend in flight, and a counter decremented afterward sees it too late. A reservation that every spend must pass through is the mint.

Reserve-Then-Commit

This is the standard escrow shape, applied to agents. It has three steps.

1. Reserve. Before spending, the agent asks for a hold of its estimated amount. The hold succeeds only if spend plus outstanding holds plus this request stays under the cap, and the check and the hold are one statement.

sql
WITH e AS (
  UPDATE budget_envelopes
  SET    reserved = reserved + 1800
  WHERE  envelope_id = 'proj-17-oct'
    AND  spent + reserved + 1800 <= cap
  RETURNING envelope_id
)
INSERT INTO budget_holds (hold_id, envelope_id, agent_id, amount, status, expires_at)
SELECT 'h-9a1', envelope_id, 'agent-a', 1800, 'held', now() + interval '30 minutes'
FROM   e
RETURNING hold_id;
-- no row returned: no room; the agent re-plans or asks for more budget

In PostgreSQL at the default Read Committed level, if Agent B’s reservation arrives while Agent A’s is in progress, B’s update waits for A to commit, then the WHERE clause is re-evaluated against the updated row. With A’s 1,800 now reserved, B’s 2,200 no longer fits, and B gets no hold.

2. Spend. The agent calls the external system, a cloud API, an ad platform or a supplier, passing the hold id as the idempotency key so a retry can’t buy twice.

3. Commit or release. When the actual amount is known, convert the hold to spend. If the agent decides not to proceed, or the call fails, release the hold.

sql
WITH h AS (
  UPDATE budget_holds
  SET    status = 'committed', actual = 1650
  WHERE  hold_id = 'h-9a1' AND status = 'held' AND expires_at > now()
  RETURNING envelope_id, amount, actual
)
UPDATE budget_envelopes e
SET    reserved = e.reserved - h.amount,
       spent    = e.spent + h.actual
FROM   h
WHERE  e.envelope_id = h.envelope_id;

The status = ’held’ and expires_at conditions matter. A sweeper job releases holds past expires_at, so a stalled agent doesn’t lock up budget forever. The sweeper uses the same conditional shape, flipping held to released and subtracting from reserved in one statement, so a commit and a release racing on one hold can’t both apply: the second waits, re-checks status = ’held’ and updates nothing. If the agent comes back after its hold expired, the commit updates nothing, and that’s the signal to record the spend as an overage and alert rather than pretend it fit.

Every hold leaves the held state exactly once. Whichever of commit or release arrives second updates nothing.

State diagram of a budget hold. A reserve creates a hold in the held state. From held, a commit moves it to committed and adds the actual amount to spend; a release or the expiry sweeper moves it to released and returns the amount. A late commit on a released hold updates nothing and raises an overage alert.

What Reserve-Then-Commit Doesn’t Solve

Be clear about the edges, because they’re real.

Metered spend keeps accruing. A launched instance costs money every hour it runs. The hold covers an estimate; the actual depends on how long the job runs. Pair holds with a runtime limit on what the agent launches, and reconcile holds against billing data when it arrives.

It coordinates your agents, not the provider. The provider knows nothing about your holds. A human launching instances from the console, or an agent that skips the reserve step, spends outside the envelope. The guarantee covers every spend that goes through the reserve tool, which is why the reserve tool should be the only spend path agents have.

Estimates can be wrong. If agents systematically under-estimate, the envelope fills with holds that commit higher than they reserved. Track estimate error per agent and pad accordingly.

What Card Controls Do and Don’t Cover

Some teams give each agent its own virtual card and set limits at the card issuer. A per-agent card is a clean way to cap any single agent. A limit evaluated on aggregated spend still trails the spend itself. Stripe’s Issuing docs, for example, say “Spending aggregation is done on a best-effort basis. You might notice a delay of up to 30 seconds between spend occurrence and spend aggregation,” and that tips and fees “can be posted at a later time, causing a spending limit to be exceeded” (Stripe spending controls).

The issuer’s real-time authorization hook is where reserve-then-commit fits. With Stripe’s synchronous webhook, “you can approve or decline authorization requests in real time,” and if Stripe doesn’t receive an answer “within 2 seconds, the Authorization is automatically approved or declined based on your timeout settings” (Stripe real-time authorizations). Run the reserve statement inside that webhook against the shared envelope, approve only if the hold succeeds, and set the timeout to decline, so a slow check can’t turn into an unchecked spend.

Splitting Budgets for Delegation

For the delegation case, the lead agent shouldn’t pass “the remaining budget” to workers. It should reserve a sub-envelope for each worker using the same atomic step, then let each worker reserve against its own sub-envelope. Unused sub-budget is released back when the worker finishes. This turns one contended counter into several uncontended ones, which also reduces how often agents wait on each other. For more on per-agent caps, see agent spend limits.

Where Tacnode Fits

Reserve-then-commit needs a store that holds the envelopes and holds and runs the check and the hold as one transaction, under concurrency from every agent that can spend.

The Tacnode Context Lake™ holds the decision context for that loop. Envelopes, holds and per-agent limits are state Tacnode holds, so the reserve and commit steps run with full ACID transactions, and serializable isolation is the default. Two agents reserving against the same envelope at the same moment can’t both get holds that together exceed the cap; one sees the other’s hold and re-plans.

The spend itself still happens in the provider’s system. Tacnode enforces nothing inside a cloud bill or an ad platform. What it does for that data is give the reserve decision accurate context: usage your systems already capture arrives by change data capture, and the reserve check reads it in the same query as the envelope’s spend and open holds, instead of fetching it from a separate system at a separate moment. Each hold, commit and release is written back and becomes context for the next agent’s reservation.

FAQ

AI AgentsAgent SpendBudgetsConcurrencyTransactions
Haitao Wang

Written by Haitao Wang

Nearly two decades at Microsoft, including the SQL Server engine. Leads engineering for the Tacnode Context Lake™.

Fresh Context Newsletter

The argument behind the posts — what breaks when AI acts on stale data, from the team building the Context Lake. About twice a month.

Subscribe on LinkedIn

Ready to see Tacnode Context Lake™ in action?

Book a demo and discover how Tacnode can power your AI-native applications.

Book a Demo