💰 SpendShield — Policy & Authorization Layer for AI Payments
Let AI agents spend real money — without spending it recklessly.
SpendShield is the policy and authorization layer between AI agents and money. It is not a wallet and not a payment processor: it sits at Agent ↔ Money and decides should this payment happen? using deterministic, explainable rules. Payment providers (Stripe, x402, wallets, cards) stay downstream — SpendShield never holds your money.
HUMAN
│ delegation
▼
AGENT IDENTITY ──► INTENT ──► POLICY ENGINE ──► RISK ──► AUTHORIZATION ──► APPROVAL
│
Stripe / x402 / Wallet ◄─┘
│
REAL MONEY
│
AUDIT
🩸 Why this project exists (a real incident)
On August 9, 2026, my automation system 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.
This is not just my problem. AI agents are about to order food, top up accounts, and call paid APIs on your behalf. When AI starts spending real money, who puts a gate in front of it?
I turned my scar into a library.
🎯 Three-way decisions, not just allow/deny
Every authorization returns a structured result — the reason is part of the product:
from spendshield import SpendShield
shield = SpendShield()
shield.load_policy("policy.yaml")
result = shield.authorize(agent="shopping-agent", amount=75, to="amazon.com")
❌ DENY
Reason: transaction $75.00 exceeds the $50.00 limit
- max_transaction: transaction $75.00 exceeds the $50.00 limit (block)
| Decision | Meaning |
|---|---|
| ✅ ALLOW | Safe to pay. Budget is reserved. |
| ⏸️ APPROVAL | Needs human sign-off. shield.approve(id) re-evaluates with current rules. |
| ❌ DENY | Blocked, with structured RuleHits explaining exactly why. |
📜 Policy DSL — rules as YAML
One policy, one place. Agent rules override global rules (CSS-like merge):
version: "2.0.0"
policy:
budget: { daily: 100, monthly: 1000 }
transaction: { max: 50 }
merchants:
allowed: [amazon.com, walmart.com] # exact domain match, subdomains ok
blocked: []
approval: { over: 30, new_merchant: true, channel: tg }
rate_limit: { window_s: 3600, max_calls: 5, max_total: 300 }
agents:
shopping-agent:
transaction: { max: 50 }
Before spending, simulate — never touches money:
from spendshield.policy import PolicySimulator
sim = PolicySimulator(policy_raw=policy)
sim.sweep("shopping-agent", "amazon.com", [20, 30, 50, 51, 80])
# {20: ALLOW, 30: ALLOW, 50: APPROVAL, 51: DENY, 80: DENY}
🛡️ Security Constitution — 8 invariants
The whole system is built around invariants that must never be violated, no matter how the code evolves:
- Unauthorized → no payment
- Over budget → no payment (
spent <= budgetalways) - Approval mismatch → no payment
- Invalid identity → no payment
- Replay → at most one valid authorization
- Concurrency → never breaks budget
- Engine failure → deny by default (fail-closed)
- An agent cannot obtain the ability to bypass SpendShield
V3+ layers (Intent, Risk) must not break these 8 rules.
🧪 Tested like it guards real money
- 186 tests, all green — including 14 security/attack suites
- 8 attack surfaces: budget_bypass, race_condition, replay_attack, double_spend, policy_bypass, approval_bypass, parameter_tampering, credential_leak
- Security constitution tests (the 8 invariants above, verified under fuzz + concurrency)
- Fuzzing: thousands of random attack combinations, Money Invariant must hold
- Simulator ↔ real engine differential: 800 random requests, decisions must match exactly
- Migration property tests: random V1 configs migrate without breaking intent
- Every discovered hole → fix → permanent regression test. Release discipline: any P0/P1 security bug blocks release. Before each release we ask: "did this change give an attacker a new way to spend money?"
🧩 MCP — for AI agents themselves
10 tools over stdio JSON-RPC (python -m spendshield.mcp --policy policy.yaml):
spend_authorize / spend_approve / spend_reject / policy_sim / policy_apply / spend_protect / spend_status / spend_audit / spend_reset / secret_get
⚠️ Threat model & known limits (transparent)
- MCP has no authentication — trust your host.
policy_apply/spend_approveare host-level operations. - Approval IDs are 48-bit random — a library trusts its caller.
- In-memory audit can be modified by code with process access (append-only audit is on the V8 roadmap).
- Denormal amounts (< 1e-9) are accepted but harmless (no money impact).
🗺️ Roadmap
V1 prevent reckless spending ✅ → V2 Policy Engine ✅ → V2.1 Simulator ✅
→ V2.2 Security Harness ✅ → engine switch ✅ → MCP ✅ → V2 Hardening 🔄
→ V3 Intent Layer (anti prompt-injection) → V4 Risk Engine (deterministic → ML)
→ V5 Agent Identity / Delegation (IAM) → V6 Payment Rails
→ V7 Dashboard → V8 Enterprise
The metric that matters: real agents protected, real transactions gated, real dollars saved. Not stars.
🚀 Quick start
pip install spendshield
from spendshield import SpendShield
guard = SpendShield(dry_run=True) # dry-run on by default
guard.load_policy("examples/policy.v2.yaml")
@guard.protect("order")
def place_order(amount, to):
return call_real_api(amount, to) # denied/needs-approval raises before this runs
V1-style constructor config still works — it is auto-migrated to the V2 engine:
guard = SpendShield(budget=200, dry_run=True, whitelist=["McDonald's"])
🔑 Secret vault
Keys encrypted at rest (Fernet), master key never on disk, key access passes the same gates and is fully audited:
from spendshield import SpendShield, KeyVault
vault = KeyVault("vault.json", master_key=os.environ["SPENDGUARD_MASTER_KEY"])
guard = SpendShield(key_vault=vault, approval="console")
sk = guard.get_secret("mcd_sk", agent="mcd_bot") # gated + audited
SpendShield: the layer I wish I had before my AI spent my money.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file spendshield-0.7.0.tar.gz.
File metadata
- Download URL: spendshield-0.7.0.tar.gz
- Upload date:
- Size: 46.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
93471bc809c18e939df01cbfc8360033306dd47b910ee103b17d8e522c2bcbf9
|
|
| MD5 |
2b826d6536a21fb525a439aed0675ada
|
|
| BLAKE2b-256 |
8a835104cb5eb46c4afbb3c160efee4080d2ab7619f13154410ad3bc58092f1a
|
File details
Details for the file spendshield-0.7.0-py3-none-any.whl.
File metadata
- Download URL: spendshield-0.7.0-py3-none-any.whl
- Upload date:
- Size: 40.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
250f5fbf9b4ad569c04f35595525301cffa1a5055615212d6c2692d8b876f168
|
|
| MD5 |
bbe05ef43ea9dffb084c26e421e458ec
|
|
| BLAKE2b-256 |
68ee93e031eecc9bc3312ff4c7ed8552f48eaea13a24f3973a0698518c0b4fe4
|