Vercel AI SDK · Stripe · Tool calls

Vercel AI SDK + Stripe: tool calls with spend caps and audit logs

Vercel's AI SDK makes Stripe tool calls feel native — define a tool(), describe it, and the model calls it when needed. The SDK handles the loop; your tool function calls Stripe. The missing piece: there's no built-in spending boundary, no per-invocation audit log, and no way to revoke access mid-stream. This page covers those three gaps and the two-line change that closes them.

TL;DR

Vercel AI SDK's tool() helper wires a function into the model's tool-use loop. If that function calls Stripe, it calls it with a raw API key that has no per-session dollar cap, no run-scoped revoke, and no structured audit trail. Swap the Stripe SDK's baseURL to https://proxy.keybrake.com/stripe/v1 and pass a vault key instead of the real secret. The two-line change is transparent to the model and to your tool function; the proxy enforces the policy and logs every call.

The typical Vercel AI SDK + Stripe pattern

A minimal Vercel AI SDK setup with a Stripe charge tool looks like this (TypeScript / Node.js route handler):

import { generateText, tool } from 'ai';
import { openai } from '@ai-sdk/openai';
import Stripe from 'stripe';
import { z } from 'zod';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

const result = await generateText({
  model: openai('gpt-4o'),
  tools: {
    chargeCustomer: tool({
      description: 'Charge a Stripe customer a given amount in cents.',
      parameters: z.object({
        customerId: z.string(),
        amountCents: z.number().int().positive(),
        currency: z.string().default('usd'),
      }),
      execute: async ({ customerId, amountCents, currency }) => {
        const intent = await stripe.paymentIntents.create({
          amount: amountCents,
          currency,
          customer: customerId,
        });
        return { id: intent.id, status: intent.status };
      },
    }),
  },
  prompt: 'Process the outstanding invoice for customer cus_abc123',
});

This works end-to-end. The model sees the tool description, decides when to call it, and the SDK handles the multi-turn loop. STRIPE_SECRET_KEY is the only credential in play — full access, no cap, no log beyond what Stripe's own dashboard shows.

Three gaps the SDK doesn't close

GapWhat breaks in productionSDK's answer
No per-session spend cap If the model reasons incorrectly and calls chargeCustomer 10 times on the same customer, you've created 10 PaymentIntents. A retried prompt re-enters the loop. None. You'd need to implement your own counter in the tool function and throw an error when it's exceeded.
No mid-session revoke A streaming session in progress can't have its Stripe access revoked without restarting the server or rotating the production key. None. The key is loaded once at server startup.
No per-call audit log with session context Stripe logs by IP and API key. You can't query "all Stripe calls made by session X" — Stripe doesn't know what session X is. None. The SDK's telemetry is at the model level, not the tool-call-to-vendor level.

The two-line fix

Keybrake sits between the Stripe SDK instance and api.stripe.com. Swap two things: the key and the base URL. Your tool function code is unchanged:

import Stripe from 'stripe';

// Before
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

// After: vault key + proxy base URL
const stripe = new Stripe(process.env.VAULT_KEY!, {
  baseURL: 'https://proxy.keybrake.com/stripe/v1',
});

The vault key is a vault_key_xxx you create in Keybrake with a policy attached. You can create it once and reuse it for all sessions of the same type, or issue a fresh vault key per user session from your backend before the generateText call:

// Issue a per-session vault key
const vaultKeyRes = await fetch('https://api.keybrake.com/vault/keys', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.KEYBRAKE_API_KEY}` },
  body: JSON.stringify({
    vendor: 'stripe',
    daily_usd_cap: 500,
    allowed_endpoints: ['POST /v1/payment_intents', 'GET /v1/customers/*'],
    expires_in: '1h',
    label: `session-${userId}`,
  }),
});
const { vault_key } = await vaultKeyRes.json();

// Pass it into the Stripe client for this request
const stripe = new Stripe(vault_key, {
  baseURL: 'https://proxy.keybrake.com/stripe/v1',
});

const result = await generateText({ /* ... */ });

Now each user session gets its own $500/day cap, can be revoked independently, and every Stripe call is logged with label: "session-{userId}" for post-session audit queries.

Why Vercel AI SDK users specifically benefit from this

Streaming + tool use creates non-deterministic call counts. The model decides how many tool calls to make in a session. You can guide it with the system prompt, but you can't guarantee it won't call chargeCustomer twice in an agentic loop. A per-session dollar cap is the only hard guarantee.

Edge runtime doesn't give you a natural place to enforce spend. If your AI route runs on Vercel Edge, you don't have a persistent server process to track cumulative spend across calls. The proxy is stateful by design — it tracks spend centrally regardless of where the Stripe SDK call originates.

Multi-tenant SaaS apps need per-user isolation. If you're building an app where multiple end-users each get their own AI assistant with Stripe access, per-vault-key caps give each user an independent budget. User A exhausting their budget doesn't affect User B.

How Keybrake fits

Keybrake is the proxy. Two lines change in your Stripe SDK instantiation; the rest of your Vercel AI SDK code is untouched. Vault keys are created via API, carry a daily_usd_cap, an endpoint allowlist, an expiry, and a human-readable label. The Free tier covers 1,000 proxied requests/month; Hobby ($29/month) adds all three vendors (Stripe, Twilio, Resend) and 30-day audit log retention.

Get early access

Related questions

Does this work with Next.js App Router API routes and the Vercel AI SDK's streamText?

Yes. The proxy change is at the Stripe SDK instance level — it doesn't interact with the Vercel AI SDK's streaming architecture. Whether you're using generateText, streamText, or the RSC streamUI pattern, the Stripe SDK calls go through the same proxy regardless. The vault key's policy is enforced on each Stripe API call as it happens, including mid-stream during a streamText session.

How do I handle the 429 cap-exceeded response in the tool's execute function?

The proxy returns a standard HTTP 429 with a JSON body: {"error": "daily_cap_exceeded", "cap": 500, "used": 502.50, "reset_at": "2026-06-02T00:00:00Z"}. The Stripe SDK will throw a StripeError with HTTP status 429. In your tool's execute function, catch StripeError and return a structured error string to the model: return { error: "Spend cap reached for today. No charge was created." }. The model will see this in its context and can inform the user gracefully instead of retrying.

Can I use this with the Vercel AI SDK's OpenAI-compatible providers (Together, Groq, etc.)?

The provider choice (OpenAI, Anthropic, Together, Groq) is orthogonal to the Stripe proxy. The proxy only intercepts calls to api.stripe.com — it has nothing to do with which model provider you're using. You can use any @ai-sdk/* provider with Keybrake.

Further reading