Audit aid, not legal advice. This article and the kit it describes are engineering tools for defining and enforcing your own agent-payment policies. They don't certify compliance with any bank framework, and nothing here is legal counsel.
On Oct 9–10, 2026, six banks published a voluntary guardrail framework for "agentic commerce" — AI agents that initiate payments (reported by Forkast via OceanAlt). The framework names the three genuinely hard questions: agent identity verification, transaction authorization scope, and dispute resolution. Then, to its credit, it admits the whole thing is voluntary — self-enforced, with no unified audit, no cross-institution recognition, and no substantive penalties for violators.
Read that twice. The institutions that move the money published a framework with no enforcement mechanism. This is the October pattern: every mandate arrives without the machinery to enforce it. The White House SI Force told every AI company that incident disclosure is "not optional" — and named no penalties. A governor's AI kill-switch proposal is a commissioned design study, not a control you can deploy. The framework is a reference template. The enforcement has to live in your stack, or it doesn't exist.
A budget cap is not a scope
"The agent may spend $50 a month" is not a scope. It's a number on a dashboard. It answers "how much" while ignoring every question that matters when money actually moves:
- Should this transaction exist at all? (type — is
refund_customereven something this agent may initiate?) - With this counterparty? (merchant — the allowlist, not the vibe)
- Initiated by which agent? (identity — when the payee asks "who paid me," you need an answer)
- With what evidence left behind? (audit — the record you'll need when the charge is disputed)
A scope is a predicate over (agent, type, counterparty, amount, time). A cap is one input to that predicate. Teams that confuse the two discover the difference during their first dispute, when "we had a $50 cap" turns out to answer none of the investigator's questions.
Building block 1: the agent identity registry
Every agent gets a transaction identity before it can pay. Not a display name — an identity record with an owner, an allowlisted set of transaction types, and numeric bounds:
from dataclasses import dataclass @dataclass class AgentIdentity: agent_id: str owner: str # the human accountable for this agent allowed_types: set[str] # allowlisted transaction types per_tx_cap: float # max single transaction daily_cap: float # max per type per day approval_threshold: float # above this -> HOLD for a human REGISTRY: dict[str, AgentIdentity] = {} def register(agent: AgentIdentity) -> None: REGISTRY[agent.agent_id] = agent The rule is absolute: no registry entry, no payment. Unknown agents fail closed — as far as the payment path is concerned, they don't exist.
Building block 2: scope rules
Scope rules combine the allowlist with three numeric controls, each bounding a different failure mode:
- Per-transaction caps bound single-event blast radius. One bad decision costs at most X.
- Per-type daily caps bound accumulation. A thousand individually reasonable charges still can't exceed the daily budget for that type.
- Approval thresholds convert spending into requesting above a line. Cross the threshold and the decision becomes
HOLD_FOR_APPROVAL— the money doesn't move until a human says so.
Caps limit damage. Thresholds insert judgment. The allowlist defines the universe of permissible actions. You need all three; any two leave a hole.
Building block 3: the dispute-evidence log
Append-only and hash-chained. Every ALLOW, HOLD, and DENY lands here with an evidence_id — including the denies, especially the denies:
import hashlib, json, time LOG: list[dict] = [] def log_evidence(decision: str, agent_id: str, detail: dict) -> str: prev = LOG[-1]["hash"] if LOG else "GENESIS" record = { "evidence_id": f"ev_{len(LOG):06d}", "ts": time.time(), "decision": decision, "agent_id": agent_id, "detail": detail, "prev_hash": prev, } record["hash"] = hashlib.sha256( json.dumps(record, sort_keys=True).encode() ).hexdigest() LOG.append(record) return record["evidence_id"] The hash chain is the point: alter any row and the chain breaks. When a customer, a processor, or an auditor asks what happened, you hand them records, not recollections.
The decision engine: POST /authorize
Below is a simplified illustration of the validator that ships in the kit (the full FastAPI app carries the complete implementation with 17 passing tests). Check order is deliberate — cheapest and most decisive checks first, evidence written for every decision:
from fastapi import FastAPI, HTTPException from fastapi.responses import JSONResponse app = FastAPI() KILL_SWITCH = False # account-level kill switch @app.post("/authorize") def authorize(agent_id: str, tx_type: str, merchant: str, amount: float): # 1. Kill switch: everything stops, immediately. if KILL_SWITCH: ev = log_evidence("DENY", agent_id, {"reason": "kill_switch"}) raise HTTPException(status_code=402, detail={"evidence_id": ev}) # 2. Unknown agent: fail closed. agent = REGISTRY.get(agent_id) if agent is None: ev = log_evidence("DENY", agent_id, {"reason": "unknown_agent"}) raise HTTPException(status_code=402, detail={"evidence_id": ev}) # 3. Type not allowlisted: DENY. if tx_type not in agent.allowed_types: ev = log_evidence("DENY", agent_id, {"reason": "type_not_allowlisted", "tx_type": tx_type}) raise HTTPException(status_code=402, detail={"evidence_id": ev}) # 4. Per-transaction cap: DENY. if amount > agent.per_tx_cap: ev = log_evidence("DENY", agent_id, {"reason": "per_tx_cap", "amount": amount}) raise HTTPException(status_code=402, detail={"evidence_id": ev}) # 5. Approval threshold: HOLD for a human (202). if amount > agent.approval_threshold: ev = log_evidence("HOLD_FOR_APPROVAL", agent_id, {"reason": "approval_threshold", "amount": amount}) return JSONResponse( {"decision": "HOLD_FOR_APPROVAL", "evidence_id": ev}, status_code=202, ) # 6. Per-type daily cap: DENY. if daily_spend(agent_id, tx_type) + amount > agent.daily_cap: ev = log_evidence("DENY", agent_id, {"reason": "daily_cap", "amount": amount}) raise HTTPException(status_code=402, detail={"evidence_id": ev}) # 7. Otherwise: ALLOW. ev = log_evidence("ALLOW", agent_id, {"tx_type": tx_type, "merchant": merchant, "amount": amount}) return {"decision": "ALLOW", "evidence_id": ev} Three properties worth noting. Denies are hard 402s — HTTP's own "payment required," repurposed as the protocol's "no." Holds return 202 and the money waits for a human; nothing auto-retries its way into an approval. And evidence is written before the response is built, so the log is complete even for decisions the caller never sees.
A dispute, walked through
It's Tuesday. A customer writes: "Which agent charged me $18.40 on Sunday?"
Without an evidence log, this is archaeology — grepping request logs, correlating timestamps, guessing. With one, it's a lookup:
- The charge carries its
evidence_id(it's on the receipt and in your ledger). - You pull the record:
agent_id, transaction type, merchant, amount, the exact rule that fired, timestamp — and the hash chain verifies intact. - You forward the record to the customer. The conversation is now about a specific decision with a specific reason, not about vibes.
That's the whole pitch: disputes stop being mysteries and start being queries.
Get the kit
I packaged all of this as AgentScope: the FastAPI scope validator (17 passing tests), plus policy, identity-record, dispute-log, and review-checklist templates you can adapt to your own stack. $39 one-time. By Haku.
https://vittoriali.gumroad.com/l/xfugeg
Audit aid, not legal advice.

