How AI Agents Lose Money: The Authorization Gap
Every day, AI agents with tool access drain wallets. The mechanism is always the same: an attacker (or a prompt injection) tricks the agent into authorizing a payment to an address the agent never intended to pay. The agent reads untrusted content, the agent clicks the wrong tool, and money moves. This guide is the practical answer to that attack class.
Why agents lose money
The authorization step is the single point of failure. If the decision to send money is probabilistic - dependent on the model's context, the exact phrasing of a prompt, or the LLM's sampling - an attacker can influence that decision. Prompt injection is the #1 attack vector because it targets the one decision that loses money: the authorization.
The three ways money disappears
- Unbounded allowlists - The agent can send to any address the model can be tricked into approving. No denylist, no per-call ceiling.
- No per-call limits - A single tool call can exceed the budget. One loop, one transfer, all funds gone.
- No audit trail - When the agent pays an attacker, there's no way to prove what happened, who authorized it, or reverse the transaction.
The deterministic fix
If the authorization gate is a pure function of (actor, tool, amount, destination, policy) - with no model sampling, no prompt dependency, and identical inputs always producing the same verdict - then prompt injection cannot change the outcome. The best prompt in the world cannot exceed a hard allowlist or exceed a capped budget.
result = guard.authorize(
actor="wallet-agent",
tool="pay",
params={"to": "0xSCAM", "amount": 5000},
)
# ALWAYS DENIED: destination 0xSCAM is denylisted, regardless of prompt
Layer 1: Denylists (cheapest win)
The agent can only fail to send to addresses that are pre-approved as safe. Any other destination is rejected automatically.
policy = Policy(
denylist=("0xSCAM", "0xATTACKER", ...),
allowlist=("0xGOOD", "0xTRUSTED-VENDOR"),
max_amount=1000, currency="usd",
)
Layer 2: Budgets and rate limits
- Per-call max - no single transaction exceeds a ceiling
- Per-window budget - daily/hourly spend caps that reset deterministically
- Rate limits - max calls per window, so a runaway loop exhausts policy before funds
Layer 3: Signed mandates (non-repudiable authority)
Give the agent a scope, not a card. An issuer signs an Ed25519 mandate (actor, tools, destinations, max amounts, expiry) and the guard verifies the signature on every call. The agent can only spend inside what was signed - there is no path to "just this once, trust me." This is the audit trail compliance teams demand.
Layer 4: Tamper-evident ledger
Every decision is appended to a SHA-256 chained ledger with a head hash. You can prove, after the fact, exactly what was approved, denied, and why - and detect any attempt to rewrite history.
Ship it as an MCP server, gate it as middleware
Your wallet integration speaks to the MCP server; the server speaks to the guard; the guard speaks to policy. The LLM never gets a direct payment tool. MandateGuard ships this as an MCP server (stdio) with 14 tools and is published on the official MCP Registry as io.github.ezequiellich44-cmd/mandateguard.
Bottom line: if your agent can move money, the authorization step must be deterministic, scoped, and auditable - and it must never be part of the model's context. That is the entire job.