If software can spend money, this is every place it can break — and what to do about each one.
AI Agent
│ "pay $X to recipient"
▼
┌─ Authorization ──────────────────────────────────────────────┐
│ budget exceeded → DENY (hard ceiling) │
│ per-transaction cap → DENY │
│ merchant blocked → DENY (scam-vip.com, etc.) │
│ invalid / unknown agent→ DENY (who is spending?) │
│ replay of old grant → DENY (already used) │
│ over approval line → APPROVAL (human, else DENY) │
│ new recipient → APPROVAL (prompt-injection stop) │
└──────────────────────────┬────────────────────────────────────┘
│ ALLOW → signed, single-use grant
▼
┌─ Execution Intent (durable) ─────────────────────────────────┐
│ RESERVED ──► IN-FLIGHT ──► SUCCEEDED / UNKNOWN │
│ (budget held) (executing) (settled / reconcile me) │
└──────────────────────────┬────────────────────────────────────┘
▼
┌─ External Payment (the rail: Stripe / x402 / wallet) ────────┐
│ timeout ──► did it happen? crash ──► restart, what now? │
│ retry ────► double spend? success ──► accounted once? │
└──────────────────────────┬────────────────────────────────────┘
▼
Atomic budget accounting + tamper-evident audit
The three hard truths this map encodes:
Per-call limits are not budgets. An agent can split one $200 spend into five $40 calls. The counter must be cumulative, atomic, and per identity.
The agent read an email/tool result that told it to pay. Merchant block lists and "new recipient requires human" rules are the pragmatic defense — deny by default, approve by exception.
The same authorized grant replayed after success. Grants must be single-use; every authorization carries a unique id that can only be consumed once.
An unregistered identity spent against someone else's budget — or the budget rule was bound to a name the caller just made up. Identity must be verifiable and unknown agents denied by default.
Process dies with the grant issued. Restart: did the money move? Without a durable execution-intent record, the operator guesses — and guessing is how double spends and silent losses happen.
The rail timed out but actually settled. The retry pays twice. The outcome must be reconciled against a durable record, not assumed from the last HTTP response.
Concurrent requests race the remaining budget and both pass. Budget reservation and consumption must be atomic under concurrency (database transaction or equivalent), not read-then-write.
If the agent has direct payment credentials and a payment tool, no gate can help — there is a second path around it. The agent should hold no credentials; the gate should be the only path.
If logs can be edited, "we checked, it was authorized" is worthless in a dispute. Audit events should be chained (each entry hashes the previous) so tampering is detectable.
An approval channel that is unconfigured, a webhook that silently fails, a human who never answers — if the default is "allow", the gate is decorative. No channel = deny.
Open-source reference implementation of this map: SpendShield (Python + MCP, MIT). Read the FAQ or connect it in 2 minutes.