Guide · 8 min read
How to give an AI agent a Stripe API key without losing $4,000 to a stuck loop
An autonomous agent with a live Stripe key is a financial weapon you handed to a process that doesn't sleep. This is a practical guide to the five controls you need in place before that key leaves your password manager — what Stripe gives you out of the box, what it doesn't, and how to assemble the rest.
The failure mode is concrete
You wire an agent to handle billing edge cases. It's allowed to issue refunds for support tickets it judges valid. One night a rule misfires, the agent gets stuck on a customer cohort, and by 9 a.m. it has issued 4,300 refunds in 90 seconds against a single SKU. There is no policy on the key. There is no per-day cap. The audit log is just stripe events list, and you have to reconstruct the loop from event timestamps.
That exact failure mode — runaway agent against a long-lived key — is on Stripe's record. Their open-source stripe/ai repo carries an issue titled "Governance layer for Stripe agent payments" (issue #356). The demand is documented; the platform-side answer hasn't shipped.
Until it does, every team handing an agent a Stripe key is rolling its own controls. Here's what those controls actually are.
What "give it a key" usually looks like today
The shortest path is also the worst one: a long-lived secret key (sk_live_…) in an environment variable, owned by every process that imports the Stripe SDK. The agent gets a string, the string gets full account access, and trust is implied by "the agent will only do what its prompt says."
If your agent never makes a mistake, this is fine. If it does — through a bad tool call, a prompt-injected support ticket, an upstream model change, or a stuck retry loop — the blast radius is your entire Stripe account.
The slightly-better path is a Stripe Restricted Key: a secret key with per-resource read/write toggles. You can grant charges: write without granting customers: write. This is the right starting point, but it's not enough. Restricted keys answer which API surfaces, not how much, against which records, or for how long.
Five controls you actually need
For an autonomous agent on a production payments key, the minimum control set is:
- Per-vendor daily spend cap. A hard ceiling on dollars moved per UTC day, enforced before the call leaves your network. If today's running total plus the next call's amount exceeds the cap, the call returns 402 to the agent and your Slack pings.
- Endpoint allowlist. Not "Charges resource" — the specific paths.
POST /v1/refundsis allowed;POST /v1/transfersis not.POST /v1/payment_intentswithcapture_method=manualis allowed; immediate-capture is not. - Customer / merchant scope. The agent can act on customers it created (or a pre-approved cohort), not on every customer in the account. This is the difference between "the agent refunded one customer twice" and "the agent refunded the whole cohort."
- Mid-run revocation. The ability to disable the agent's key in <10 seconds without rotating the secret in the agent's config or restarting it. The agent's next call returns 401, the agent reports back to its operator, and the loop terminates cleanly. Today this means an on-call engineer logging into the Stripe dashboard. That is a 60-to-90-second window of continued spend.
- Per-call audit log. A queryable record — outside Stripe — of every call the agent made on this key, with the policy decision (allowed / blocked / capped), the request body, the response status, and the dollars moved. This needs to exist before the incident, not after, because it's how you reconstruct what happened in a 90-second window with thousands of events.
Note what's not on this list: rate limiting (Stripe already enforces theirs), idempotency keys (you should use them anyway, but they don't bound spend), retries (agent SDK problem, not key problem), or "human in the loop" (a checkpoint pattern, not a security control — checkpoints fail open under retry pressure).
What Stripe gives you, and what's missing
Mapping the five controls to what's available out-of-the-box on Stripe today:
| Control | Available today? | Mechanism |
|---|---|---|
| Endpoint allowlist (resource-level) | Yes | Restricted Key per-resource toggles |
| Endpoint allowlist (path-level) | No | — |
| Per-day spend cap | No | — |
| Customer / merchant scope | Partial | Connected accounts on Stripe Connect; not for vanilla accounts |
| Mid-run revocation (< 10s) | Partial | Dashboard rotation, requires human; SDK keeps cached key |
| Per-call audit log (with $ totals) | Partial | events stream is the closest, missing policy decision + per-key tagging |
Three of the six are partial or missing. None of them are unsafe to ship — they're just not in the platform yet, so you build them yourself or you accept the risk.
How to assemble the rest today
You have two real options: a thin wrapper around the Stripe SDK in your own code, or a reverse proxy that sits between your agent and api.stripe.com. Both work. The trade-off is where the policy lives and how brittle it is to drift.
Option 1: SDK wrapper
Subclass or wrap the Stripe client. Before every charges.create, refunds.create, or transfers.create call, look up the running daily total, check the cap, log the decision. On exceeded cap, raise an exception the agent must handle. On allowed call, write to your audit table.
// pseudo-Node, the shape, not production code
class CappedStripe {
constructor(real, policy, audit) {
this.real = real; this.policy = policy; this.audit = audit;
}
async refund(args) {
const today = await this.audit.spentToday('refunds');
if (today + args.amount > this.policy.daily_usd_cap_cents) {
this.audit.log({ decision: 'capped', args });
throw new Error('daily refund cap exceeded');
}
const r = await this.real.refunds.create(args);
this.audit.log({ decision: 'allowed', args, response: r });
return r;
}
}
Wrapper pros: tightly typed, sits inside your error-handling, doesn't add a network hop. Wrapper cons: every code path must use the wrapper (one missed import = bypass), the policy lives in N codebases if you have N agent runners, mid-run revocation requires a config-reload mechanism that survives restart, and there's no audit when the agent talks to Stripe through a tool the wrapper doesn't intercept.
Option 2: Reverse proxy
Stand up a service the agent talks to instead of api.stripe.com. The agent's SDK config changes one URL — the base URL — and nothing else. The proxy looks up the policy attached to the agent's key, checks the cap, forwards the call, parses the cost out of Stripe's response (amount, amount_refunded, etc.), updates the running total, and logs the decision in its own database.
// agent code — single line change
const stripe = new Stripe(process.env.STRIPE_KEY, {
- // talks to api.stripe.com
+ host: 'proxy.keybrake.com', protocol: 'https', port: 443,
});
Proxy pros: every call is routed through the same policy point, regardless of which library or tool the agent used; revocation is one row update in the proxy's database (next call returns 401 immediately); audit is automatic; the agent's existing SDK is unchanged. Proxy cons: one extra network hop (latency depends on proxy location); the proxy is the new single point of failure (cache the real key in your runtime as a fallback); you have to trust the proxy operator with key custody (or run it yourself).
For a single team running a handful of agents, the wrapper is faster to ship. For a team running multiple agents across vendors, or for any team where "what did the agent actually do" needs to be queryable, the proxy wins.
The same problem repeats per vendor
Once you've solved this for Stripe, the next agent your team writes touches Twilio. SMS sends are $0.0079 per US message. A stuck agent doing a verification flow can move $50 in five minutes. Then it touches Resend, where transactional emails are billed per send. Then Shopify Admin, where the cost is reputational, not just dollars.
Each of these vendors has its own version of the gap above. Restricted keys on Stripe; sub-account credentials on Twilio; per-domain API keys on Resend. None of them give you a per-day spend cap or a path-level allowlist. Each requires its own wrapper or its own proxy entry.
The honest position is: this is the work the platform should be doing, and it isn't yet. Until it is, the cheapest version of "safe" is a single proxy that knows about your top 3 vendors, with one policy schema and one audit table for all of them. That's the bet behind Keybrake.
Things to do today, before any tool
Even without standing up a wrapper or a proxy, you can close most of the blast radius this afternoon:
- Rotate to a Restricted Key per agent, with the narrowest resource toggles that pass your tests. Drop full secret keys from agent runtimes today.
- Set a Stripe Radar rule blocking refunds above a threshold per merchant per day. This isn't a key-scoped cap, but it catches the egregious failure mode.
- Wire a webhook on
refund.created+charge.dispute.createdto a Slack channel. You want to see the first three before the next thirty. - Tag every API call with an
idempotency_keythat includes the agent run id. This won't stop a runaway loop, but it makes the audit a query, not a forensic exercise. - Decide your kill protocol now. Who has dashboard access on a Saturday? How fast can they rotate the key? If the answer is "we'd Slack the founder," that's your real revocation latency.
None of these replace the missing controls. They lower the cost of the inevitable first incident.
Where this leads
The platforms will eventually ship governance — the Stripe issue is open, the demand is mapped, and the agent ecosystem is growing fast enough that the gap is felt. Until then, every team putting an agent on a real key is its own platform team for that vendor.
If you'd rather not be — if "scoped keys with policies, per-call audit, and a kill switch that works in 10 seconds" sounds like infrastructure you don't want to maintain — that is exactly the surface Keybrake is building. Three vendors at v1 (Stripe, Twilio, Resend), one policy schema, one audit table, one base-URL change in your agent.
Get Keybrake when v1 ships
Pre-launch waitlist. We'll email you a vault key when the proxy is live, and a working code sample for your stack.