TrustGate

authorization between an AI agent and a payment provider
3fields an agent may set
5MCP tools
0of them can move money
53guards deleted, all caught
16attacks refused, with tests

1 · The trust boundary

The agent proposes three fields. Everything that decides what the money does is derived server-side. There is no amount field for injected text to fill.

AI buyer agent untrusted by design sku quantity purpose and nothing else TRUST BOUNDARY the only arrow it crosses TrustGate derives, then decides merchant · amount · currency from the catalogue, never the agent policy · delegation · approval evaluated server-side a human pays Razorpay Test Mode executes a bounded order payment.captured signed — the only thing that moves money the browser callback is verified, then deliberately ignored

2 · Authorized is not the same as able to payTHE GAP

Most systems collapse these into one state. Here they are two, and the distance between them is a separate authority that must be issued, is single-use, expires in 15 minutes, and is bound to a hash of that exact purchase.

AUTHORIZED policy said yes Order creation allowed: No it can sit here indefinitely CHECKOUT AUTHORITY single-use · 15 minutes bound to a hash of this purchase revoked if the policy under it moves PROVIDER_PENDING one order, bounded CAPTURED only a signed event gets here the agent reaches this box ——> — and never any of these ————————————————————————————>

3 · Delegated authority between agentsTHE DIFFERENTIATOR

Every capability standard models attenuation as set intersection: a child is never wider than its parent. That is correct for permissions and wrong for money, because budgets add and sets do not. Two children each no wider than their parent can, between them, hold twice what the parent ever had.

Finance lead (human) grants ₹2,000 buyer-agent holds ₹2,000 · already promised ₹1,200 asks ₹1,200 asks ₹1,200 procurement-agent allocated ₹1,200 no wider than its parent — allowed renewals-agent DELEGATION_BUDGET_EXHAUSTED also no wider than its parent — refused anyway Every per-edge check passes. Together they would hold ₹2,400 of a ₹2,000 budget.

The refusal comes from ck_delegation_budget_partitioned, a check constraint on the parent's own row — the budget is partitioned, not compared. And revoking one hop ends every branch below it without touching a descendant: the leaf's own revoked_at is still None, and its next spend is refused anyway, because authority is re-derived from the whole chain on every spend and again at checkout. A signed token would have to be hunted down and recalled.

4 · The layers

Delete the agent entirely and everything below the dotted line is unchanged, and still correct. If the agent were the centre of this, the project would be arguing against itself.

agent/ thin, untrusted, replaceable mcp_server/ five tools, none can authorize, capture, refund, or reach a provider list_catalog · create_payment_request_tool · evaluate_payment_policy · request_user_approval · get_payment_status everything below here survives deleting the agent THE AUTHORIZATION CORE — this is the product policy_engine/ rule evaluation, atomic daily budget state_machine/ row-locked transitions delegation/ narrows per edge, partitions across siblings models/ invariants as database constraints evidence receipt above it: proposed / derived / provider outcome, per purchase Provider execution — a bounded order, then a signed event back

5 · Where each rule is enforced

One principle: enforce at the lowest layer that can hold it. A rule in application code is a rule a different query walks around — so most of them are not in code at all.

2 APPLICATION CODE schema forbids unknown fields · HMAC over raw bytes before parsing weakest: a different code path can miss it 4 TRANSACTION daily limit by conditional upsert · approval consumed once · FOR UPDATE on the authority and the tenant row holds under concurrency, inside one transaction 6 DATABASE CONSTRAINT composite foreign keys on (tenant_id, id) · partial unique index on live approvals · immutability trigger CHECK on payment state · CHECK that siblings cannot outspend their parent · one provider order per authority strongest: no query can get around it, including mine