Vercel Connect can now hand each agent (or each task) its own managed restricted Stripe key, with only the permissions you select, and during a rotation the old key can stay valid for up to 24 hours. That release fixes the worst habit in agent payments code: one sk_live_ secret key shared by every bot in the repo. It does not decide which agent gets which permissions, how you rotate without an outage, or how you prove a key really can't do what it shouldn't. Those three calls are still yours.

TL;DR

  • Split agents by blast radius: a reporting agent gets a read only restricted key, and anything that writes to customers gets its own key plus a human approval step.
  • Treat the 24 hour overlap as a migration window, not a grace period: ship the new key, verify it, watch the old one go quiet, then let it expire.
  • Write tests that expect a 403. A key you have only tested for success is a key you have not tested.

Managed restricted key release

Stripe has three kinds of live key: publishable (pk_live_), restricted (rk_live_) and secret (sk_live_). Stripe's own docs recommend granting a key only the permissions the integration needs. That's the restricted key's whole job, and for years most of us skipped it because minting and storing one key per integration was annoying.

The Vercel Connect change removes that excuse. The managed Stripe connector can create one restricted key per agent or per task, with the permissions you choose, so "one bot, one key, one scope" is now the cheap default instead of the disciplined exception.

Why this matters more for agents than for a normal backend: an agent picks its own next action. OWASP's LLM06:2025 entry on excessive agency names the fix directly: use read only credentials where you can, authorize in the user's context, and put an approval gate in front of high impact operations. If you want the wider threat model around this (prompt injection steering a tool call is the classic route), my production guide to AI agent security walks through it layer by layer.

The first line of defence costs five lines of code. Refuse to boot an agent runtime with anything other than a restricted key:

// lib/stripe-key.ts export function assertRestrictedKey(key: string | undefined, name: string): string { if (!key) { throw new Error(`${name} is not set`); } if (!/^rk_(live|test)_/.test(key)) { throw new Error(`${name} must be a restricted key (rk_), refusing to start`); } return key; } 

Footgun avoided: nobody can "temporarily" paste the account's secret key into an agent's env and forget about it.

Read only reporting versus customer writes

Most agent workloads I see split cleanly into two lanes, and they deserve different keys and different rules.

Two lanes side by side: the Reports agent holds a read only restricted key for balances, charges and payouts and runs unattended; the Customer agent holds a restricted key that writes customers and needs human approval first; a spotlight moves from one lane to the other while each key card grows

The reporting lane. A finance digest, a churn summary, a weekly revenue check. It reads and never writes. Give it a restricted key with read access to what it summarizes and nothing else. It can run on a cron with no human in the loop, because the worst thing a confused reporting agent can do is read data it was already allowed to read.

// agents/reports/weekly-revenue.ts import Stripe from "stripe"; import { assertRestrictedKey } from "../../lib/stripe-key"; const stripe = new Stripe( assertRestrictedKey(process.env.STRIPE_REPORTS_KEY, "STRIPE_REPORTS_KEY"), ); export async function weeklyNetRevenue(): Promise<number> { const since = Math.floor(Date.now() / 1000) - 7 * 24 * 60 * 60; let net = 0; for await (const txn of stripe.balanceTransactions.list({ created: { gte: since }, limit: 100, })) { net += txn.net; } return net; // smallest currency unit, e.g. cents } 

The write lane. Updating a customer's details, issuing a credit, anything a customer would notice. This agent gets a separate key whose permissions cover exactly those writes, and it never executes a write on its own say so. It proposes, a person approves, then the call runs:

// agents/support/propose-update.ts import Stripe from "stripe"; import { assertRestrictedKey } from "../../lib/stripe-key"; const stripe = new Stripe( assertRestrictedKey(process.env.STRIPE_SUPPORT_KEY, "STRIPE_SUPPORT_KEY"), ); type PendingWrite = { customerId: string; changes: Stripe.CustomerUpdateParams; requestedBy: string; approvedBy?: string; }; export async function applyApprovedWrite(write: PendingWrite) { if (!write.approvedBy || write.approvedBy === write.requestedBy) { throw new Error("Customer writes need approval from a second person"); } return stripe.customers.update(write.customerId, write.changes); } 

The split also keeps your agent graph honest. Once every agent has its own key, "which agent can touch money?" becomes a question you answer by reading key scopes, not by reading prompts. If you are drawing that graph for a bigger system, my notes on multi agent system architecture cover where each agent's boundaries should sit.

Scoped key lifecycle and rotation

Per Vercel's changelog, when a managed key rotates, the old key can remain valid for up to 24 hours. Read that as a window you control, not a safety net you lean on.

Rotation timeline in four cards from left to right: New key issued, Deploy and verify, Old key still valid for 24 hours at most, Old key expires; each card's object grows in turn and 24 hours counts up while the hourglass grows

Here's the order I'd run it in:

  1. New key issued. The connector mints it with the same scope as the old one. Don't change scope and rotate in the same step; if something breaks you won't know which change did it.
  2. Deploy and verify. Ship the new key and run your denial tests (next section) against it before anything else.
  3. Watch the old key go quiet. During the overlap both keys work. Log which key each request used (the last few characters are enough) and confirm traffic on the old one drops to zero.
  4. Let it expire. If the old key is still busy near the end of the window, you have a forgotten caller (a cron, a preview deploy, a teammate's script). Find it now, while it's a log line and not an outage.

A tiny helper makes step 3 boring:

// lib/key-fingerprint.ts export function keyFingerprint(key: string): string { return `${key.slice(0, 8)}…${key.slice(-4)}`; } // usage inside an agent run console.info("stripe_call", { agent: "weekly-revenue", key: keyFingerprint(process.env.STRIPE_REPORTS_KEY ?? ""), }); 

How to test authorization denials

This is the part almost everybody skips. A restricted key that passes your "can it read balances?" test has proven half of its contract. The other half is what it must refuse.

Three test cards: Allowed call returns 200 OK; Forbidden call returns 403 permission error; Wrong key type fails at boot; each result counts in and its card grows in turn

Run these against a restricted test mode key (rk_test_) with the same permissions as production:

// agents/reports/key-contract.test.ts import Stripe from "stripe"; import { describe, expect, it } from "vitest"; import { assertRestrictedKey } from "../../lib/stripe-key"; const stripe = new Stripe( assertRestrictedKey(process.env.STRIPE_REPORTS_TEST_KEY, "STRIPE_REPORTS_TEST_KEY"), ); describe("reports agent key contract", () => { it("can read balance transactions", async () => { const page = await stripe.balanceTransactions.list({ limit: 1 }); expect(Array.isArray(page.data)).toBe(true); }); it("cannot create customers", async () => { await expect( stripe.customers.create({ email: "contract-test@example.com" }), ).rejects.toBeInstanceOf(Stripe.errors.StripePermissionError); }); it("refuses a secret key at boot", () => { expect(() => assertRestrictedKey("sk_test_123", "TEST")).toThrow(/restricted key/); }); }); 

Three things to know from running tests like this:

  • Assert on the error class, not the message text. Messages change; the permission error type is the contract.
  • Put this suite in CI and rerun it after every rotation. A rotated key with a wider scope than its predecessor is exactly the mistake nobody notices.
  • Add one test per write the agent must never do. When someone widens a scope "just for this feature", a red test forces the conversation.

Production checklist

Production checklist with six items: one restricted key per agent; read only keys for reporting; approval before customer writes; boot fails on a secret key; rotate scope and key separately; denial tests run in CI

Check Reporting agent Customer write agent
Key type Restricted, read only Restricted, narrow writes
Human in the loop No Yes, a second person approves
Runs unattended Yes Proposes only
Denial tests Must fail on any write Must fail outside its writes
Rotation Verify, watch old key, expire Same, plus a test write in test mode

My opinion after wiring a few of these: the managed key is the easy part now. The real work is the boring contract around it (a boot check, a log line, a test that expects 403), and that contract is what keeps a clever agent from becoming an expensive one.

Related reading: OpenAI Sandbox Findings Change How Leaders Should Design Agent Controls and Multi Agent LLM Systems: Testing Strategies That Actually Catch Real Failures.

FAQ

How do I implement vercel connect stripe restricted api key safely?
Give each agent its own restricted key with only the permissions it needs, refuse to boot on anything that isn't an rk_ key, put a human approval in front of customer writes, and keep a test suite that proves the forbidden calls fail.

What changed with vercel connect stripe restricted api key?
Vercel Connect's managed Stripe connector can now create one restricted key per agent or per task with selected permissions, and an old key can stay valid for up to 24 hours during a rotation.

If you're writing the instructions these agents follow, the agent system prompt builder helps you spell out tool limits so the prompt and the key scope agree.

Drop a comment if your setup looks different. Curious how other teams are splitting agent keys in production.