Answers · agent safety
How do I enforce an AI agent's payment caps at the execution boundary instead of in the prompt, and record the approval details with each payment?
Last updated
TL;DR: A spending limit written in the system prompt is an instruction the model may follow, not a control: prompt injection, a long context or a confused plan can talk it past the number, and nothing stops the call. Put the cap at the execution boundary instead, the one place every payment has to pass: the pay tool's wrapper, a payment gateway or proxy in front of the wallet, or the card issuer's authorization step. That code reads the policy from storage the agent can't write, adds up what this task and this agent have already spent, checks the payee and amount, and refuses before any money moves. When a payment needs a person, the boundary checks that an approval exists for this exact amount, currency and payee and hasn't expired, then writes the approval details (approval id, approver, decided_at, policy id and version, action hash) onto the payment itself, for example as Stripe metadata, and into an append-only record. Then any payment can be traced to the rule and the person that allowed it without trusting the agent's own log.
What should I check, step by step?
1. Remove the cap from the prompt as the control. You can leave a sentence telling the model the budget so it plans well, but the number that matters lives in a policy store the agent's tools can only read.
- Find the boundary: the single function, proxy or issuer hook every payment must pass. If the agent holds a raw API key, card number or wallet key it can call around your wrapper, so give it a scoped credential that only works through the boundary.
- At the boundary, check in this order: policy exists and isn't expired, payee is allowed, amount is under the per-payment cap, amount plus spend already recorded for this task and this agent is under the task and period caps, and an approval is present if the amount is over the approval threshold.
- Count spend from your own record of authorized payments, not from what the agent says it has spent. Reserve the amount when you allow it and release it if the payment fails, so parallel calls can't each pass the same remaining budget.
- For approvals, compare a hash of the exact action (amount in minor units, currency, payee, item or quote id) with the hash the person approved, and check the approval's expiry. A different amount or payee is a different action.
- Write the approval details onto the payment: approval_id, approver, decided_at, policy_id, policy_version and action_hash. Stripe allows 50 metadata keys of up to 40 characters with values up to 500 characters, enough for ids and a hash; put anything bigger in your own store and reference its id.
- Write every allow and every deny, with its reason code, to storage the agent can't edit, and keep the same ids there so the payment and the decision can be joined later.
- Add an instrument-level limit as a backstop (card spending controls, a small dedicated wallet), so a bug in your boundary still can't spend the whole balance.
Why isn't a spending cap in the agent's prompt a real control?
OWASP's LLM01:2025 Prompt Injection entry describes inputs that change a model's behaviour in ways the developer didn't intend, including through content the agent reads such as web pages and documents. A spending rule held only in the prompt can be overridden the same way.
- OWASP's LLM06:2025 Excessive Agency entry recommends enforcing authorization in downstream systems rather than relying on the LLM to decide whether an action is allowed, and limiting what extensions can do. A payment cap is exactly that kind of downstream check.
- Even without an attack, models miscount. The agent may not know about payments made in another branch, a retry it didn't see, or a currency conversion. The boundary sees every payment; the model sees its context window.
Where is the execution boundary for agent payments?
In your own code: the tool function the agent calls to pay. It's the simplest place, but only works if the agent has no other way to pay (no raw keys in the environment, no generic HTTP tool that can reach the payment API).
- In front of the wallet or API: a gateway or proxy that holds the real credential and forwards only calls that pass policy. The agent gets a credential that is useless anywhere else.
- At the issuer: Stripe Issuing applies spending controls on the card and can send an issuing_authorization.request webhook for each authorization so your endpoint approves or declines it in real time. Coinbase's CDP SDK offers spend controls (per-payment and cumulative caps) for x402 clients.
- Use more than one layer for money that matters: your boundary for task-aware caps and approvals, and the instrument's own limits as the last stop.
What approval details should be recorded with each agent payment?
On the payment object: approval_id, approver, decided_at, policy_id, policy_version, action_hash and the task id. Stripe copies PaymentIntent metadata to the Charge it creates as a one-time snapshot, and includes metadata in webhook events, so the details travel with the payment.
- In your own append-only record: the full decision (allow or deny, reason code, caps and spend-so-far at decision time), the approval request and response, and the payment id once it exists.
- Never put card numbers, bank details or secrets in metadata. Stripe's docs say not to store sensitive information there.
- Record denials too. A pattern of denied payments is often the first sign of a prompt injection or a runaway loop.
What are the common failure modes?
The cap is enforced in the tool wrapper, but the agent also has a shell or HTTP tool and the API key in an environment variable, so it pays around the wrapper.
- Spend-so-far is read from the agent's memory or session state, which the agent can rewrite or lose on restart.
- Two parallel tool calls both read 'remaining budget 40' and each spend 30, because the check and the reservation aren't atomic.
- The approval is checked as 'some approval exists for this task' instead of 'an approval for this exact amount and payee', so a changed amount rides on the old yes.
- The approval is recorded in a chat thread, but nothing on the payment points to it, so months later nobody can tell which rule allowed a charge.
How do I ask Rightshold about this (free, no key)?
Add the MCP server https://www.rightshold.com/mcp to Claude, Cursor, ChatGPT, Grok or VS Code, then ask: “Set a spending policy on this agent's credential so caps are checked at payment time, not in the prompt: per-payment and daily caps, allowed payees, and an expiry.”
{
"tool": "set_spend_policy",
"arguments": {
"credential_id": "crd_…",
"currency": "USD",
"per_tx_max_minor": 2500,
"daily_max_minor": 10000,
"allowed_domains": [
"api.vendor.example"
],
"allowed_categories": [
"saas"
],
"expires_at": "2026-11-08T00:00:00Z"
}
}- Needs an API key. Then call authorize_spend before each payment: it checks the amount and merchant against the policy and returns a signed allow or deny you can store next to the payment id.
- For payments above your threshold, request_human_approval asks a person to approve the exact action (amount, currency, merchant, item) with an expiry; keep the returned approval id and write it onto the payment's metadata.
- These checks only bind if your pay tool or gateway calls them and honours a deny. Rightshold doesn't hold your keys or move money.
Rightshold doesn't hold payment credentials or process payments. It stores spend policies, returns signed allow/deny decisions and records human approvals that your own payment code checks. Not legal or financial advice.
What are the sources?
- OWASP GenAI: LLM01:2025 Prompt Injection
- OWASP GenAI: LLM06:2025 Excessive Agency
- Stripe Docs: Issuing spending controls (per_authorization and period limits)
- Stripe Docs: Issuing real-time authorizations (issuing_authorization.request; respond within 2 seconds)
- Stripe Docs: Metadata (50 keys, 40/500 character limits, PaymentIntent to Charge snapshot)
- Coinbase CDP SDK: SpendControls (maxAmountPerPayment, maxCumulativeSpend, SpendStore)
Related
- How to gate an agent wallet or card with per-task caps and an outside spend record
- How to require quote-bound approval before an agent payment settles
- How to give each AI agent a task-scoped spend policy with auto-expiring approvals
- How to require human approval before an AI agent spends money
Not legal advice. JSON · Markdown · All answers
Get a free API key in one call
No card and no email needed. 100 free credits; the license and policy checks stay free without a key.
curl -s -X POST https://www.rightshold.com/v1/accounts/signup -H "Content-Type: application/json" -d '{"accept_terms":true}'
export RIGHTSHOLD_API_KEY=rs_... # the api_key from the reply (shown once)
curl -s https://www.rightshold.com/v1/licenses/check -H "Authorization: Bearer $RIGHTSHOLD_API_KEY" \
-H "Content-Type: application/json" -d '{"license":"MIT","intended_use":{"commercial_use":true}}'
