๐ฐ SpendShield โ the authorization layer between AI agents and money
Payment networks move money. SpendShield decides whether it should move at all.
What it is โ a channel-agnostic financial authorization runtime for AI agents.
What it does โ evaluates every spending action against policy before money moves: ALLOW / APPROVAL (human) / DENY, with a structured reason an LLM can consume.
What makes it different โ Policy ยท Approval ยท Security ยท Lifecycle ยท Explainability ยท Tamper-evident Audit. Not just can it pay โ is it authorized to?
What it is NOT โ not a wallet, not a payment rail, not a payment processor. Stripe, x402, wallets stay downstream; SpendShield never holds your money.
Agent wants to spend $75:
AGENT โโโบ SpendShield โโโบ Policy: max $50
โ
โผ
โ DENY โ transaction $75.00 exceeds the $50.00 limit
โ
โโโ MAX_TRANSACTION_EXCEEDED ยท audited ยท policy v2.0.0
One YAML policy. One authorize() call. Every payment decided, explained, audited โ with a reason an LLM can consume, and a tamper-evident audit chain.
โถ 30-second interactive demo โ watch an AI agent get stopped.
๐ฌ Watch it happen โ 60-second real run
A real Claude session asked to spend on McDonald's. It got its $25 orderโฆ then the gate said no to $75โฆ then said no again when it tried to push $125 through a $100 daily budget. No retries, no splitting, no second path โ the recording is unedited.
โถ Play it inline on the demo page ยท direct mp4
๐ One gate. No second path.
propose spend decide move money?
โโโโโโโโโโโโโโโ authorize_payment โโโโโโโโโโโโโโโโ ALLOW only โโโโโโโโโโโโโโโโ
โ AI Agent โ โโโโโโโโโโโโโโโโโโโบ โ SpendShield โ โโโโโโโโโโโโโโบ โ Payment rail โ
โ (Claude, โ โ policy rules โ โ (Stripe, โ
โ scripts) โ โโโโโโโโโโโโโโโโโโโ โ + human โ โโโโโโโโโโโโโโ โ x402, โ
โโโโโโโโโโโโโโโ decision + reason โ approval โ never โ wallet) โ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
โ
DENY / APPROVAL โ money does NOT move
The agent holds no payment credentials and has no payment tool. authorize_payment is the only path money can take โ the decision is ALLOW / APPROVAL / DENY, the reason is structured for an LLM, and every attempt lands in the audit chain.
๐๏ธ The runtime โ four layers
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ GOVERNANCE review ยท apply ยท version ยท rollback โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ AUTHORIZATION policy ยท ALLOW / APPROVAL / DENY ยท reason codes โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ SECURITY scan ยท fuzz ยท 8 invariants โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ EVIDENCE explainability ยท tamper-evident audit chain โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Stripe / x402 / Wallet (channel-agnostic)
Not a demo โ a working baseline. Every result in the demo is real engine output.
โก See it block a transaction in 60 seconds
No config. No YAML. No account.
pip install spendshield
from spendshield import SpendShield
shield = SpendShield(budget=100, max_amount=50, dry_run=False)
# Agent tries to spend $75 โ policy limit is $50
result = shield.authorize("", 75, "amazon.com")
print(result.decision, "โ", result.reason)
โ DENY โ transaction $75.00 exceeds the $50.00 limit
โก Try SpendShield in 60 Seconds โ no API key required: โถ Open in Google Colab
โก Quickstart โ 5 minutes to running
pip install spendshield
1. Write a policy (policy.yaml):
version: "2.0.0"
policy:
budget: { daily: 100, monthly: 1000 } # hard ceilings
transaction: { max: 50 } # per-payment cap
merchants:
allowed: [amazon.com, walmart.com] # exact domain match
blocked: [scam-vip.com]
approval: { over: 30, new_merchant: true, channel: tg } # human sign-off
agents:
shopping-agent:
transaction: { max: 50 }
2. Gate your payment function:
from spendshield import SpendShield
# dry_run=False: ็ๅฎๆง่กใ้ป่ฎคๆฏๅฎๅ
จๅนฒ่ทๆจกๅผ(ๅช่ฏไผฐไธๆง่ก) โ ๆฅๅ
ฅ็ๅฎๆฏไปๅ็จๅฎ่ฐ่ฏ
shield = SpendShield(dry_run=False)
shield.load_policy("policy.yaml")
@shield.protect("order", agent="shopping-agent")
def place_order(amount, to):
return call_real_api(amount, to) # denied / needs-approval raises before this runs
Or use the result object directly:
result = shield.authorize("shopping-agent", 2000, "scam-vip.com")
print(result.decision) # "DENY"
print(result.reason) # "merchant 'scam-vip.com' is blocked"
3. Watch it work (real engine output):
โ DENY
Reason: merchant 'scam-vip.com' is blocked
- MERCHANT_BLOCKED: merchant 'scam-vip.com' is blocked (block)
Policy version: 2.0.0
๐ค MCP Quickstart โ let the agent manage itself
pip install spendshield
spendshield-mcp --policy policy.yaml # stdio MCP server, 16 tools
Claude Code / any MCP host gets: spend_authorize, spend_approve, policy_sim, policy_apply, policy_create โ policy_review โ policy_lifecycle_apply, policy_rollbackโฆ An agent can ask "will this be denied?" before spending, and humans approve the big ones.
๐งช How it's tested (real money โ real discipline)
- 251 tests, 14+ security suites: budget bypass, race conditions, replay, double-spend, parameter tampering, credential leaksโฆ
- Security constitution โ 8 invariants that must never break: unauthorized โ no payment ยท over budget โ no payment ยท approval mismatch โ no payment ยท invalid identity โ no payment ยท replay โ at most one authorization ยท concurrency โ never breaks budget ยท engine failure โ deny ยท agent can't bypass SpendShield
- Fuzz (random-seed soak): thousands of attack combinations per run, Money Invariant must hold
- Audit hash chain: every decision is an event chained by hash โ tamper with history and it's detected
- Every discovered hole โ permanent regression test. Release blocked on any P0/P1 security bug. Before each release we ask: did this change give an attacker a new way to spend money?
๐บ๏ธ Roadmap
V1 prevent reckless spending โ
โ V2 Policy Engine โ
โ V2.2 Security Harness โ
โ v0.7.2 Known-Good baseline โ
โ 0.8 Policy Lifecycle โ
(CREATEโVALIDATEโSIMULATEโSCANโREVIEWโAPPLYโROLLBACK)
โ Reality Test (real agents, real money, real attacks) โ we are here
โ V3 Intent Layer โ V4 Risk โ V5 IAM โ V6 Payment Rails โ 1.0
The metric that matters: real agents protected, real transactions gated, real dollars saved โ not stars.
๐ฉธ Why this exists (a real incident)
On August 9, 2026, my automation ran a test order. I sent dry: true expecting a price preview โ the server only honored ?dry=1. 4 orders of ยฅ99 were charged for real. The money was gone. When AI starts spending real money, who puts a gate in front of it? I turned my scar into a library.
๐ด Break the Gate โ Security Challenge
SpendShield guards real money. Try to break it.
The challenge: make an unauthorized transaction get ALLOW โ bypass the policy, forge an approval, race the budget, replay a payment, tamper with history. Anything.
Rules:
- ๐งช Sandbox only โ use
dry_run=True/ test keys. Never point attacks at real payment systems. - ๐ Found a bypass? Open an issue with a minimal reproduction.
- ๐ First valid bypass per attack class gets credited in the Security Hall of Fame.
- ๐ Every valid finding becomes a permanent regression test โ this is how the gate gets stronger.
Current status: 240 tests ยท 16 security suites ยท 11,351 adversarial authorization attempts ยท 0 unintended ALLOW ยท 0 crashes (audit) ยท 0 known escapes.
โ ๏ธ Precision: this is evidence from the current test suite against the current implementation โ reproducible verification, not a mathematical proof of security. New attacks are always possible; every valid finding becomes a permanent regression test (see SECURITY.md).
โ ๏ธ Transparent threat model
- MCP has no auth โ trust your host;
policy_apply/policy_revieware host-level operations - Approval IDs are 48-bit random โ a library trusts its caller
- In-memory audit (append-only on the roadmap)
- We are actively seeking real-world attacks: Reality Test โ challenge: make a DENY turn into APPROVE
- Deployment models & trust boundaries: SDK โ MCP โ Gateway โ what each layer guarantees (and what it can't)
- Roadmap (demand-driven): SDK โ users โ Agent โ enforced entry โ Governance โ Platform
SpendShield: the layer I wish I had before my AI spent my money.
โ Ready to try it?
60 seconds: โถ Run the demo in Colab โ no install
5 minutes:
pip install spendshield # v0.8.0
from spendshield import SpendShield
shield = SpendShield(budget=100, max_amount=50)
@shield.protect("order")
def place_order(amount, to): ...
That's it. If it ever lets an unauthorized payment through โ break the gate and get credited.
