paybondpaybond
Sign in

Claude Agents SDK agent spend controls

Claude Agent SDK agent spend controls — in-process MCP custom tools with spend verify and auto-evidence via @paybond/claude-agents. TypeScript and Python parity.

Paybond integrates with the Claude Agent SDK by wrapping paid custom tools registered via tool() and bundling them into an in-process MCP server.

SDK note: The Claude Agent SDK continues to evolve on npm and PyPI. Paybond tracks the tool() and createSdkMcpServer surface with regression tests; pin SDK versions in production and run paybond agent demo claude-agents smoke in CI after upgrades.

Built-in SDK tools (unguarded)

Claude Agent SDK built-in tools (Read, Write, Edit, Bash, Glob, Grep, WebFetch, WebSearch, …) execute inside Anthropic's runtime and do not pass through Paybond spend verify, approval holds, or auto-evidence. Only tools you register with tool() and expose via the Paybond MCP server are governed.

Recommended: restrict query() allowedTools to your Paybond MCP entries (mcp__paybond__*) and omit built-ins for spend-sensitive agents.

import {
  createPaybondClaudeAgentsConfig,
  findUnguardedClaudeBuiltinTools,
} from "@paybond/kit/claude-agents";

const { mcpServer, allowedTools } = createPaybondClaudeAgentsConfig(run, sdkTools, {
  queryAllowedTools: allowedToolsForQuery, // optional: emits a one-time console warning if built-ins remain
});

const unguarded = findUnguardedClaudeBuiltinTools(allowedToolsForQuery);
if (unguarded.length > 0) {
  // fail closed in production if needed
}

For local development, createPaybondClaudeAgentsConfig warns once when queryAllowedTools includes known built-in names (disable with warnOnUnguardedBuiltins: false).

Install

Install

npm install @paybond/claude-agents @anthropic-ai/claude-agent-sdk

Import from @paybond/claude-agents

import { createPaybondClaudeAgentsConfig } from "@paybond/claude-agents";
  • Equivalent subpath on the core package: `@paybond/kit/claude-agents` — use `@paybond/kit` when you need multiple adapters in one app.
  • The Python extra installs **`claude-agent-sdk`** on PyPI (not `@anthropic-ai/claude-agent-sdk`). Wheels bundle the Claude Code CLI binary (~65MB) — plan container image size accordingly.
  • Python: `paybond agent demo claude-agents smoke` requires the optional `claude-agents` extra. Use `pip install "paybond-kit[claude-agents]"`, `pipx install 'paybond-kit[claude-agents]'`, or `pipx inject paybond-kit claude-agent-sdk` (when base paybond-kit is already installed).

One-liner (sandbox): paybond.instrument({ policy, framework: "claude-agents", tools, sandbox: true }) or paybond.agent({ policy, framework: "claude-agents", tools }) returns claudeAgentsConfig with mcpServer and allowedTools for query().

TypeScript

import { tool } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";
import { Paybond } from "@paybond/kit";

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

const sdkTools = [
  tool(
    "travel.book_hotel",
    "Book a hotel room",
    { estimatedPriceCents: z.number() },
    async (args) => ({
      content: [{ type: "text", text: JSON.stringify(await bookHotel(args)) }],
      structuredContent: await bookHotel(args),
    }),
  ),
];

const { claudeAgentsConfig } = await paybond.instrument({
  policy: "./paybond.policy.yaml", // or preset id "travel"
  framework: "claude-agents",
  tools: sdkTools,
});

const { mcpServer, allowedTools } = claudeAgentsConfig!;

// query({ prompt, options: { mcpServers: { paybond: mcpServer }, allowedTools } })

Python

from claude_agent_sdk import tool
from paybond_kit import Paybond

paybond = await Paybond.open(api_key=os.environ["PAYBOND_API_KEY"])

sdk_tools = [
    tool("travel.book_hotel", "Book a hotel", {"estimated_price_cents": int}, book_hotel),
]

result = await paybond.instrument(
    policy="./paybond.policy.yaml",
    framework="claude-agents",
    tools=sdk_tools,
)

config = result.claude_agents_config
# query(..., options={"mcp_servers": {"paybond": config.mcp_server}, "allowed_tools": config.allowed_tools})

Already bound a run? paybond.wrapTools(run, tools, { framework: "claude-agents" }) / paybond.wrap_tools(run, tools, framework="claude-agents").

See Agent middleware for run binding and tenant isolation.

Advanced / manual wiring

When you need step-by-step control over registry and bind:

  1. Bind a PaybondAgentRun with a tool registry.
  2. Define side-effecting tools with the SDK tool() helper.
  3. Call createPaybondClaudeAgentsConfig(run, tools) and pass mcpServer and allowedTools to query().

TypeScript

import { tool } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";
import { Paybond, createPaybondToolRegistry } from "@paybond/kit";
import { createPaybondClaudeAgentsConfig } from "@paybond/kit/claude-agents";

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",
    },
  },
});

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

const sdkTools = [
  tool(
    "travel.book_hotel",
    "Book a hotel room",
    { estimatedPriceCents: z.number() },
    async (args) => ({
      content: [{ type: "text", text: JSON.stringify(await bookHotel(args)) }],
      structuredContent: await bookHotel(args),
    }),
  ),
];

const { mcpServer, allowedTools } = createPaybondClaudeAgentsConfig(run, sdkTools);

// query({ prompt, options: { mcpServers: { paybond: mcpServer }, allowedTools } })

Python

from claude_agent_sdk import tool
from paybond_kit import Paybond
from paybond_kit.agent import create_paybond_tool_registry
from paybond_kit.claude_agents import create_paybond_claude_agents_config

paybond = await Paybond.open(api_key=os.environ["PAYBOND_API_KEY"])

registry = create_paybond_tool_registry(
    {
        "default_deny": True,
        "side_effecting": {
            "travel.book_hotel": {
                "spend_cents": lambda args: args["estimated_price_cents"],
                "evidence_preset": "cost_and_completion",
            },
        },
    }
)

run = await paybond.agent_run.bind({...})

async def book_hotel(args: dict, _extra) -> dict:
    payload = await _book_hotel_impl(args)
    return {
        "content": [{"type": "text", "text": json.dumps(payload)}],
        "structuredContent": payload,
    }

sdk_tools = [
    tool("travel.book_hotel", "Book a hotel", {"estimated_price_cents": int}, book_hotel),
]

config = create_paybond_claude_agents_config(run, sdk_tools)
# query(..., options={"mcp_servers": {"paybond": config.mcp_server}, "allowed_tools": config.allowed_tools})

Approval holds

When Paybond returns an approval hold, the wrapped handler returns an MCP error block with isError: true. After operator approval in the tenant console:

run.storeApprovalToken(toolCallId, approvalTokenFromConsole);
// Retry the same tool call with the stored token.
run.store_approval_token(tool_call_id, approval_token_from_console)
# Retry the same tool call with the stored token.

Wrapped handlers read the token via run.getApprovalToken / run.get_approval_token for the same toolCallId.

Scaffold and smoke

paybond init agent-middleware --framework claude-agents --out ./paybond-claude-agents.ts

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

The smoke command invokes a wrapped SDK tool handler directly — no live LLM or Anthropic API key in CI.

Example apps: examples/paybond-kit-claude-agents-typescript, examples/paybond-kit-claude-agents-python.