Entry 009Guides · Demos6 min readBy Mandare Labs

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.

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 Authorization is 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.

  1. The timeout default is a policy decision you make in the dashboard. If it approves, an outage of your webhook becomes an open card.
  2. Spending controls run first. The spending controls page says they "can decline a purchase before the issuing_authorization.request is 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:

pnpm demo:cardsimulated Stripe
[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 network

Read 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.

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

  1. Issuing spending controls · Stripe
  2. Issuing real-time authorizations · Stripe

Run it yourself: 3 commands, no API keys.

github.com/mandarelabs/mandare →
← All journal entries