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
403. A key you have only tested for success is a key you have not tested.
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.
Most agent workloads I see split cleanly into two lanes, and they deserve different keys and different rules.
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.
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.
Here's the order I'd run it in:
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 ?? ""),
});
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.
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:
| 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.
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.