paybondpaybond
Sign in

Vercel AI SDK agent spend controls

Vercel AI SDK agent spend controls — Harbor verify, toolApproval holds, spend finalize, and auto-evidence on paid tool calls via @paybond/vercel-ai.

Paybond integrates with the Vercel AI SDK at the tool approval and execute boundary — not as model middleware. Keep your provider client for inference; use Paybond for Harbor verify, approval holds, spend finalize, and auto-evidence on side-effecting tools.

TypeScript only. Python Kit does not ship a Vercel AI adapter; use this page with @paybond/kit or see Agent-agnostic adapter for Python parity.

Install

Install

npm install @paybond/vercel-ai ai

Import from @paybond/vercel-ai

import {
  paybondVercelToolApproval,
  paybondVercelWrapTools,
} from "@paybond/vercel-ai";
  • Equivalent subpath on the core package: `@paybond/kit/vercel-ai` — use `@paybond/kit` when you need multiple adapters in one app.
  • TypeScript only. Python Kit does not ship a Vercel AI adapter — use the agent-agnostic adapter for Python parity.
  • Optional peer: ai >= 4.0.0 (tested with v7).

One-liner (sandbox): paybond.instrument({ policy, framework: "vercel-ai", tools, sandbox: true }) returns guarded tools plus toolApproval for generateText / streamText.

import { generateText, tool } from "ai";
import { z } from "zod";
import { Paybond } from "@paybond/kit";

const paybond = await Paybond.open({ apiKey: process.env.PAYBOND_API_KEY! });

const { agentTools: tools, toolApproval } = await paybond.instrument({
  policy: "./paybond.policy.yaml", // or preset id "travel"
  framework: "vercel-ai",
  tools: {
    bookHotel: tool({
      description: "Book a hotel room",
      inputSchema: z.object({
        city: z.string(),
        estimatedPriceCents: z.number().int().nonnegative(),
      }),
      execute: async (args) => bookHotel(args),
    }),
    searchWeb: tool({
      description: "Search the web",
      inputSchema: z.object({ query: z.string() }),
      execute: async (args) => searchWeb(args),
    }),
  },
});

const result = await generateText({
  model: openai("gpt-4.1"),
  tools,
  toolApproval,
  prompt: "Find hotels in Lisbon and book one under budget.",
});

Already bound a run? paybond.wrapTools(run, tools, { framework: "vercel-ai" }) returns the same { tools, toolApproval } shape without reloading policy.

See Agent middleware for policy files, registry rules, and tenant isolation.

Advanced / manual wiring

When you need step-by-step control over registry and bind, use both primitives:

PrimitivePaybond helperPurpose
toolApproval on generateText / streamTextpaybondVercelToolApproval(run)Pre-execution Harbor verify, deny, HITL hold
Wrapped execute on side-effecting toolspaybondVercelWrapTools(run, tools)Post-success spend finalize + auto-evidence
import { generateText, tool } from "ai";
import { z } from "zod";
import { Paybond, createPaybondToolRegistry } from "@paybond/kit";
import {
  paybondVercelToolApproval,
  paybondVercelWrapTools,
} from "@paybond/kit/vercel-ai";

const paybond = await Paybond.open({ apiKey: process.env.PAYBOND_API_KEY! });

const registry = createPaybondToolRegistry({
  defaultDeny: true,
  sideEffecting: {
    "travel.book_hotel": {
      spendCents: (args: { estimatedPriceCents: number }) => args.estimatedPriceCents,
      evidencePreset: "cost_and_completion",
      evidenceMapper: (result) => ({
        status: result.reservation.status === "confirmed" ? "completed" : result.reservation.status,
        cost_cents: result.reservation.price_cents,
      }),
    },
  },
});

const run = await paybond.agentRun.bind({
  bootstrap: {
    kind: "sandbox",
    operation: "travel.book_hotel",
    requestedSpendCents: 20_000,
    completionPreset: "cost_and_completion",
  },
  registry,
});

const tools = paybondVercelWrapTools(run, {
  bookHotel: tool({
    description: "Book a hotel room",
    inputSchema: z.object({
      city: z.string(),
      estimatedPriceCents: z.number().int().nonnegative(),
    }),
    execute: async (args) => bookHotel(args),
  }),
  searchWeb: tool({
    description: "Search the web",
    inputSchema: z.object({ query: z.string() }),
    execute: async (args) => searchWeb(args),
  }),
});

const result = await generateText({
  model: openai("gpt-4.1"),
  tools,
  toolApproval: paybondVercelToolApproval(run),
  prompt: "Find hotels in Lisbon and book one under budget.",
});

paybondVercelToolApproval(run)

  1. Looks up toolCall.toolName in the registry.
  2. Read-only tools → 'approved' (no Harbor verify).
  3. Side-effecting tools → middleware interceptor pre-check (authorizeToolCall).
  4. Approval hold → 'user-approval'.
  5. Hard deny → { type: 'denied', reason } (surfaced to the model as a tool error).

paybondVercelWrapTools(run, tools)

Wraps only client-executed, registry side-effecting tools. Each successful execution:

  • Finalizes the spend decision (consumed / released on failure).
  • Builds evidence from the registry evidencePreset and optional evidenceMapper.
  • Submits with idempotency key evidence:{intentId}:{toolCallId} (safe for streamText multi-step loops).

Provider-executed tools (isProviderExecuted: true) are never wrapped — they bypass local approval and evidence.

Approval hold + retry

When Harbor returns approvalRequired, toolApproval yields 'user-approval'. After operator approval:

run.storeApprovalToken(toolCallId, approvalTokenFromConsole);
// Retry generateText / streamText with the same run binding.

Both paybondVercelToolApproval and wrapped execute read stored tokens via run.getApprovalToken(toolCallId).

Scaffold and smoke

paybond init agent-middleware --framework vercel-ai --out ./paybond-vercel-ai.ts

paybond agent demo vercel-ai smoke \
  --operation paid-tool \
  --requested-spend-cents 100 \
  --evidence-preset cost_and_completion \
  --format json

The smoke command uses MockLanguageModelV4 from ai/test — no OpenAI API key in CI.

Example app: examples/paybond-kit-vercel-ai-typescript.

Known limitations

  • Provider-executed tools (web search, code execution on-provider) bypass local toolApproval and wrapTools. Register explicit registry entries only for tools you execute locally.
  • LanguageModelV3Middleware / wrapLanguageModel operate on model calls only; they cannot block tool execution. Do not document Paybond as model middleware.
  • experimental_sandbox tools need explicit registry entries like any other side-effecting tool.