paybondpaybond
Sign in

Claude Agents SDK agent spend controls

Claude Agent SDK agent spend controls — in-process MCP custom tools with Harbor 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. Built-in Claude tools (Read, Bash, etc.) pass through unguarded.

Beta: The Claude Agent SDK is alpha on PyPI and evolving on npm. Treat this adapter as beta until Anthropic stabilizes the SDK.

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.

One-liner (sandbox): paybond.instrument({ policy, framework: "claude-agents", tools, sandbox: true }) or paybond.agent({ policy, framework: "claude-agents", tools }) returns claudeAgentsConfig with mcpServer + 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 + 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 Harbor 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.

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.