Virtual card limits for an AI agent: what Stripe enforces
To cap an AI agent's virtual card, set Stripe Issuing spending_controls on the card, and add a real-time authorization webhook if you want to decide each purchase yourself within Stripe's 2-second limit. Neither knows what the agent spends on LLM calls. Mandare's card rail puts both under one signed cap, but it is tested against a simulated Stripe, not a live test-mode account.
Start with Stripe's spending controls on the card
The limits on a virtual card decide how much damage an agent can do with it. In Stripe Issuing, those limits are spending_controls, which Stripe's spending controls page (read 9 October 2026) says you can set on both cards and cardholders. They can block merchant categories, countries and card presence, and "set spending limits per authorization or per month".
The amount rules live in spending_limits. This is the page's own example for one card:
curl https://api.stripe.com/v1/issuing/cards/{{ISSUINGCARD_ID}} \
-u "<<YOUR_SECRET_KEY>>:" \
-d "spending_controls[allowed_categories][0]=car_rental_agencies" \
-d "spending_controls[spending_limits][0][amount]=8000" \
-d "spending_controls[spending_limits][0][interval]=per_authorization"
Three details from the same page matter for an agent.
- Defaults exist. "If you don't set
spending_limits, a default spending limit of 500 USD per day applies to the newly created card," and an unconfigurable 10000 USD applies to each authorization. - Limits are scoped. "Spending limits alone don't block categories and should be used with either
allowed_categoriesorblocked_categories." A cardholder's limits apply across all of their cards. - They are not exact. "Spending aggregation is done on a best-effort basis," with a delay of up to 30 seconds, and "Additional tips and fees can be posted at a later time, causing a spending limit to be exceeded."
For many agents this is enough. One card per agent, a category allow-list, a low weekly amount: the network enforces it and nothing of yours needs to be running. If that covers the job, use it and stop there.
Add a real-time authorization webhook when a static limit is not enough
Static limits cannot express "this agent may spend more on this merchant than on that one", or "ask a person above a threshold". For that, Stripe's real-time authorizations page describes a synchronous webhook. When a card is used, "Stripe creates an issuing_authorization.request and sends it to your configured endpoint for your approval."
The response is JSON with a required approved boolean, and the Stripe-Version header is required. An optional amount is accepted only "if the authorization's pending_request.is_amount_controllable property is true". Stripe's own timing rule:
If Stripe doesn't receive your approve or decline response within 2 seconds, the
Authorizationis automatically approved or declined based on your timeout settings
Autopilot, the fallback described on that page, is marked public preview. The same page says to monitor request_history.reason: the values webhook_error and webhook_timeout mean "Stripe isn't receiving your responses", and it warns not to "rely on approved to gauge integration health because Autopilot can approve on your behalf during an error condition".
Two consequences follow for anyone who writes this endpoint.
- The timeout default is a policy decision you make in the dashboard. If it approves, an outage of your webhook becomes an open card.
- Spending controls run first. The spending controls page says they "can decline a purchase before the
issuing_authorization.requestis sent". A purchase your webhook never sees is a purchase it cannot log, so the webhook's records are incomplete by design.
A card cap and an LLM budget are two separate meters
An agent with a company card usually also holds an LLM API key. The card's spending_limits count card purchases. The provider's budget counts tokens or dollars on the key. Nothing joins them, so a "€20 per day" intention becomes two limits that each hold €20 and can add up to €40.
Reasons to want one total are practical. A finance team approves one number per agent per day. An incident review wants one timeline. A kill switch should end both. The daily spending limit guide covers the LLM side on its own: which provider and gateway limits exist, and what they cover.
A single enforcement point has to see both kinds of spend, and it has to count a pending amount before the money moves. The simplest design is a shared counter that both the LLM request path and the card authorization path reserve against inside one transaction. The next section shows one implementation of that.
One signed cap over LLM calls and card authorizations
Mandare's card rail (@mandarelabs/card-rail) mounts on the same gateway process as the LLM proxy. Per the repository's docs/CARD-RAIL.md, one spend scope with rails: ["gateway", "card"] gives "one cap that BOTH rails draw down", and the card decision is made on Stripe's real-time authorization webhook, "approved or declined at the network". The card is created through a mandate-checked door operation, and the creation is a ledger entry. The webhook signature is checked over the exact raw bytes of the request.
The repository ships a demo, run with pnpm demo:card. It uses a simulated card network: signed webhooks against the real verification path, no Stripe account needed. Captured output from docs/demos/S5-card-demo.txt, cuts marked:
[agent] LLM call 6/6 → 200 (settled €2.50 of the €20 cap)
€15.00 of the mandate consumed on the gateway rail.
[card] €4.20 at ACME SaaS → issuing_authorization.request → APPROVED (15.00 + 4.20 ≤ 20.00)
[card] €3.00 at ACME SaaS → issuing_authorization.request → DECLINED AT THE NETWORK (19.20 + 3.00 > 20.00)
the refusal is a ledger entry, not a vanished toast.
…
$ mandare verify --db ledger.db --spend
…
chain: VALID — every entry hash-linked and door-signed
…
spend:
mnd_dev_c31077f1caa2: settled 19.20 EUR · open reservations 0.00 EUR · 7 call(s) · 2 refused
rails: llm settled 15.00 EUR (0 refused) · card settled 4.20 EUR (2 declined) · one cap governs both
trail: (last 12 of 21 entries)
…
#16 2026-09-27T05:32:12.803Z CARD reserve 4.20 EUR (network authorization)
#17 2026-09-27T05:32:12.804Z CARD settle 4.20 EUR ← authorization APPROVED
#18 2026-09-27T05:32:12.805Z CARD refused 3.00 EUR ← DECLINED at the networkRead the arithmetic. Six LLM calls at €2.50 settle €15.00. A €4.20 purchase fits under €20 and is approved. A €3.00 purchase would reach €22.20, so it is declined, and the decline is entry #18. The second refusal in the count is a €0.50 attempt after mandare kill, which revokes the agent and its card locally and cancels the card at Stripe on a best-effort basis, as the CLI reference describes. The same mechanism, applied to LLM calls alone, is walked through in a runaway loop dying at €20, and the demos are listed on the demos page.
What this does not cover, as of v0.1.0
The claims above hold for @mandarelabs/cli 0.1.0 (the version npm view returned on 9 October 2026), checked against the public repository at commit 327442e. The limits are part of the claim.
- The Stripe side is simulated. The demo and CI use a mock that signs webhooks the way Stripe does. The live Stripe test-mode run has not happened. Read the demo as proof of the decision logic, not of behaviour against Stripe's production network.
- There is no card product. Stripe issues the cards, and Mandare never holds funds. The rail needs your Stripe Issuing account:
STRIPE_WEBHOOK_SECRET(without it the rail does not mount),STRIPE_SECRET_KEYandSTRIPE_CARDHOLDER_ID. - Timeouts are your obligation.
docs/CARD-RAIL.mdtells operators to set the Issuing timeout default to DECLINE and not to "rely on Autopilot approving". A decline caused bywebhook_timeout"is invisible to the local ledger", so reconciliation against Stripe's authorization list is manual. - Settlement is approximate. The document lists "Settlement true-up" as a scheduled gap: version 0 settles at the authorized amount at decision time, and real captures, including partial reversals, reconcile in a later release. Stripe's own warning about later tips and fees applies here too.
- Humans cannot answer in 2 seconds. Over-threshold purchases are declined immediately while an approval push goes out. A granted approval becomes a single-use waiver (default 10 minutes), and the person retries the purchase. Waivers live in memory, so a restart drops them.
- Out-of-process cards. The card registry learns about cards created outside the door only on restart; their authorizations decline until then.
- The LLM side depends on a correct price table. The LLM proxy handles Anthropic
POST /v1/messagesand OpenAI or OpenRouterPOST /v1/chat/completions, and refuses models without a price.
Stripe's spending controls remain the better choice when one card needs one static rule, since they need no process of yours to stay up. They also remain the backstop beneath any webhook. Both can run together: the Stripe limits stay as a ceiling, and the shared cap decides inside it. The threat model lists what the card path defends against and what it does not, and the governance tools comparison places this next to other ways to cap and audit agents.
Questions
How can you limit what an AI agent spends on a virtual card?
Create the card with Stripe Issuing spending_controls: a spending_limits amount with an interval such as per_authorization, weekly or monthly, plus allowed_categories. For per-purchase logic, answer the issuing_authorization.request webhook with approved: true or false inside 2 seconds.
What happens if the real-time authorization webhook is down?
Stripe's documentation says that if no response arrives within 2 seconds, the authorization is approved or declined based on your timeout settings, or Autopilot settings if configured. Set the timeout default to decline, and watch request_history.reason for webhook_timeout.
Can an agent exceed a Stripe Issuing spending limit?
Yes, in documented cases. Stripe says spend aggregation is best-effort with a delay of up to 30 seconds, and that tips and fees posted later can cause a limit to be exceeded. A tight limit is a ceiling to plan around, not an exact meter.
Does a card limit cover the agent's LLM API spend?
No. Card spending controls count purchases on that card. LLM calls billed to an API key are a separate meter, so a card cap and a provider budget are two half-budgets unless something above both enforces one total.
Is Mandare a card issuer?
No. Stripe issues the cards. The card rail works through the operator's own Stripe Issuing account (a Stripe secret key and cardholder id) and makes the approve or decline decision on Stripe's webhook. As of v0.1.0 it is tested against a Stripe mock that signs webhooks like Stripe; a live Stripe test-mode run has not happened.
Sources
- Issuing spending controls · Stripe
- Issuing real-time authorizations · Stripe
Run it yourself: 3 commands, no API keys.
github.com/mandarelabs/mandare →