Paybond agent middleware replaces per-tool guardTool ceremony with a run-scoped layer that binds one intent and capability per agent task, auto-guards registered side-effecting tools, and automatically submits evidence after every successful side-effecting call.
The core lives in @paybond/kit/agent (TypeScript) and paybond_kit.agent (Python). It has zero agent-framework dependencies — OpenAI, Claude, Gemini, LangGraph, MCP, and custom runtimes plug in through optional framework adapters.
When to use middleware vs manual guard
| Approach | Best for |
|---|---|
Agent middleware (PaybondAgentRun and registry) | Multi-tool agents, coding-agent automation, any integration that must not forget evidence on side-effecting tools |
Per-tool guardTool | Single paid handler, legacy code, or advanced cases where you control evidence submission explicitly |
Prefer middleware when building agents with several side-effecting tools. Use defaultDeny: true so unregistered tools that match intent allowedTools fail closed at intercept time.
For GitOps-friendly configuration, use a versioned paybond.policy.yaml instead of scattering registry and intent fields across code.
One-liner: instrument()
Add one line and every side-effecting tool becomes spend-controlled. paybond.instrument() loads policy and returns deferred tool shells by default — safe to register with any agent framework at startup. Bind a funded intent per session with .bind() (or pass context / sandbox: true when you already know the binding mode).
No framework argument is required for agent-agnostic tool maps. Use framework-specific helpers only when your SDK needs native hooks.
import { Paybond } from "@paybond/kit"; const paybond = await Paybond.open({ apiKey: process.env.PAYBOND_API_KEY! }); // Static instrumentation — no intent yet (deferred by default) const instrumented = await paybond.instrument({ policy: "./paybond.policy.yaml", // or a preset id such as "travel" tools: { "travel.book_hotel": async (args) => bookHotel(args), searchWeb: async (args) => searchWeb(args), }, }); // Safe to pass to your agent constructor — execution throws until bound const agent = new Agent({ tools: instrumented.tools }); // Per session / request: const runtime = await instrumented.bind({ intentId, capabilityToken }); const tools = runtime.tools; // guarded tools for this session // runtime.run — bound PaybondAgentRun for additional wraps or hooks // runtime.binding — { phase: "bound", mode: "attach", intentId, tenantId, ... } // instrumented.policy — loaded policy document
Local sandbox (opt-in):
const runtime = await paybond.instrument({ policy: "./paybond.policy.yaml", tools, sandbox: true, }); // runtime.binding === { phase: "bound", mode: "sandbox", intentId: "intent-sandbox", ... }
Immediate bind at instrument time:
const runtime = await paybond.instrument({ policy: "./paybond.policy.yaml", tools, context: { intentId, capabilityToken }, });
from paybond_kit import Paybond paybond = await Paybond.open(api_key=os.environ["PAYBOND_API_KEY"]) instrumented = await paybond.instrument( policy="./paybond.policy.yaml", # or a preset id such as "travel" tools={ "travel.book_hotel": book_hotel, "search.web": search_web, }, ) # Safe at startup — execution throws until bound agent = Agent(tools=instrumented.tools) # Per session: runtime = await instrumented.bind(intent_id=intent_id, capability_token=capability_token) tools = runtime.tools # runtime.run — bound PaybondAgentRun for additional wraps or hooks # runtime.binding — {"phase": "bound", "mode": "attach", "intent_id", "tenant_id", ...}
Inline policies (tutorials)
const instrumented = await paybond.instrument({ policy: { budget: "$500/day", approve: ["travel.*"], deny: ["crypto.*"], }, tools: myTools, });
Policy presets
policy: "travel" | "shopping" | "saas" | "aws" resolve bundled vertical presets.
Fluent builder
const instrumented = await paybond .policy("./paybond.policy.yaml") .instrument(tools); // or const instrumented = await paybond.usePolicy("travel").instrument(tools);
Framework-specific adapters (only when needed)
| Helper | When |
|---|---|
paybond.instrument({ policy, tools }) | Default — any { name, execute } or tool map |
paybond.instrumentLangGraph({ policy, tools }) | LangGraph awrapToolCall / createToolNode hooks on .hooks |
paybond.instrumentOpenAI({ policy, tools }) | OpenAI Agents SDK and .hooks.runConfig |
paybond.instrumentVercel({ policy, tools }) | Vercel AI SDK and .hooks.toolApproval |
paybond.instrumentClaudeAgents({ policy, tools }) | Claude Agents SDK MCP surface |
paybond.instrumentMCP({ policy, tools }) | MCP-style tool hosts (alias for agent-agnostic) |
Or pass framework: "langgraph" only when you prefer a single entry point.
Static instrumentation vs per-request context
instrument() splits policy and tool registration (static, reusable) from intent binding (per request).
| Phase | What happens | Where intentId comes from |
|---|---|---|
instrument({ policy, tools }) | Loads policy, registers deferred tool shells | Deferred default — instrumented.binding.phase === "deferred". Tools are safe to register; side-effecting execution throws until bound. |
instrument({ ..., sandbox: true }) | Bootstraps a sandbox intent immediately | Kit sandbox bootstrap — inspect runtime.binding.intentId. |
instrument({ ..., context: () => ... }) | Lazy per-execution binding via request-local provider | You declare a context function up front; each tool call resolves the active request. instrumented.binding.phase === "lazy". |
instrument({ ..., context: { intentId, ... } }) or instrumented.bind(...) | Eager bind for one session | You supply intent and capability. Returns a PaybondInstrumentRuntime. |
Deferred (production default):
// Static: load policy once at process startup — no implicit intent const instrumented = await paybond.instrument({ policy: "./paybond.policy.yaml", tools, }); // instrumented.binding === { phase: "deferred" } // instrumented.tools — deferred shells; register with your agent framework // Per request / session / agent task (immutable — safe for concurrency): const runtime = await instrumented.bind({ intentId, // from your session — never guessed by Kit capabilityToken, userId, // optional audit attribution }); const tools = runtime.tools; // runtime.binding === { phase: "bound", mode: "attach", intentId, tenantId, ... }
Lazy context (request-scoped binding):
const purchasing = await paybond.instrument({ policy: "./paybond.policy.yaml", tools, context: () => activeRequest.paybond, // { intentId, capabilityToken, userId? } }); // purchasing.binding === { phase: "lazy" } const agent = new Agent({ model: "gpt-5", tools: purchasing.tools }); // Each tool execution resolves context from the provider and binds automatically.
instrumented = await paybond.instrument( policy="./paybond.policy.yaml", tools=tools, context=lambda: active_request.paybond, ) # instrumented.binding == {"phase": "lazy"}
The provider is declared at instrumentation time (explicit), but binding happens at execution time. Runtimes are cached per (intentId, capabilityToken) for the lifetime of the instrumented instance so multi-tool agent turns do not re-bind on every call.
Sandbox (local dev — opt-in):
const runtime = await paybond.instrument({ policy: "./paybond.policy.yaml", tools, sandbox: true }); // runtime.binding === { phase: "bound", mode: "sandbox", intentId, capabilityToken, tenantId } const tools = runtime.tools;
Do not share one bound runtime across concurrent requests. Call instrumented.bind() (or pass context to instrument()) to obtain a fresh immutable runtime per task.
Tenant id always comes from the authenticated Paybond.open() session (paybond.harbor.tenantId), not from client input.
Per-request context (API reference)
const instrumented = await paybond.instrument({ policy: "./paybond.policy.yaml", tools }); const runtime = await instrumented.bind({ intentId, capabilityToken, userId, }); const requestTools = runtime.tools;
withContext() remains as a deprecated alias for bind().
Inspect binding state at any time via .binding or .status on instrumented or runtime.
Already bound a run? Wrap additional tools without reloading policy:
- TypeScript:
paybond.wrapTools(run, tools) - Python:
paybond.wrap_tools(run, tools)
LangGraph does not wrap tools in place — use instrumentLangGraph() or createPaybondLangGraphHooks(run) / create_paybond_langgraph_hooks(run) for awrapToolCall hooks.
Preset quickstart: paybond.agent({ policy: "travel", tools }) resolves bundled presets before calling instrument().
Automatic tool discovery: pass a framework agent instance — tools are read from .tools (or .functionTools), policy from agent.policy / PAYBOND_POLICY / ./paybond.policy.yaml:
class Agent { constructor(public tools: Record<string, ToolFn>) {} } const agent = new Agent({ "travel.book_hotel": bookHotel }); agent.policy = "travel"; await paybond.instrument(agent); // or: const agent = await paybond.instrument(agent); // agent.tools — guarded after bind; agent.paybond — { policy, binding, bind, ... }
Lower-level escape hatch: createGuardedAgent / create_guarded_agent when you need full control over bind inputs. Per-tool guardTool / guard_tool remains available for single-handler integrations.
Architecture
Agent app → PaybondAgentRun.bind() # sandbox bootstrap or attach existing intent → PaybondToolRegistry # side-effecting map and evidence presets → PaybondToolInterceptor # authorize → execute → complete spend → auto-evidence → Harbor / Gateway # POST /verify, evidence submit
Framework adapters (for example createGenericToolExecutor()) translate framework tool definitions into run.interceptor.wrapExecute calls. Adapters must not embed Harbor logic.
Core API
Tool registry
Every side-effecting tool must declare an evidencePreset (required). Read-only tools pass through without spend verify or evidence.
import { Paybond } from "@paybond/kit"; const paybond = await Paybond.open({ apiKey: process.env.PAYBOND_API_KEY! }); const registry = paybond.toolRegistry({ sideEffecting: { "travel.book_hotel": { spendCents: (args: { estimatedPriceCents: number }) => args.estimatedPriceCents, evidencePreset: "cost_and_completion", }, searchWeb: { operation: "search.web", evidencePreset: "api_response_ok", }, }, defaultDeny: true, });
Python parity:
registry = paybond.tool_registry( { "side_effecting": { "travel.book_hotel": { "spend_cents": lambda args: args["estimated_price_cents"], "evidence_preset": "cost_and_completion", }, }, "default_deny": True, } )
Registry rules:
operationdefaults to the tool name; override when the Harbor operation differs from the handler name.defaultDeny: truerejects tool calls whose operation is in intentallowedToolsbut missing from the registry.- Bind-time validation fails if
defaultDenyis enabled and an allowed operation has no registered side-effecting entry. - Unknown or missing
evidencePresetvalues raise at registry construction.
Import the core directly when tree-shaking matters:
import { createPaybondToolRegistry } from "@paybond/kit/agent";
from paybond_kit.agent import create_paybond_tool_registry
Run binding
Bind once per agent task. Do not share a PaybondAgentRun across concurrent runs.
Sandbox bootstrap — funds a sandbox intent for one operation:
const run = await paybond.agentRun.bind({ bootstrap: { kind: "sandbox", operation: "travel.book_hotel", requestedSpendCents: 20_000, completionPreset: "cost_and_completion", }, registry, });
Production attach — reuse an existing funded intent with intentId and capabilityToken. Not required for sandbox smoke — see Going to production.
const run = await paybond.agentRun.bind({ attach: { intentId: process.env.PAYBOND_INTENT_ID!, capabilityToken: process.env.PAYBOND_CAPABILITY_TOKEN!, }, registry, });
When attach.allowedTools is omitted, allowed tools are loaded from Harbor for the bound intent.
Python sandbox bootstrap:
run = await paybond.agent_run.bind( { "bootstrap": { "kind": "sandbox", "operation": "travel.book_hotel", "requested_spend_cents": 20_000, "completion_preset": "cost_and_completion", }, "registry": registry, } )
For Python production attach, see Going to production.
Interceptor
The interceptor is the heart of middleware. For side-effecting tools it:
- Resolves operation and requested spend from the registry and call arguments.
- Calls
guard.assertSpendAuthorized(...)— approval holds and hard denials propagate to the caller. - Runs your handler on allow.
- Calls
completeSpendAuthorization(decisionId, "consumed")on success or"released"on handler failure. - Automatically submits evidence on success using the registry
evidencePresetand completion catalog.
const result = await run.interceptor.wrapExecute({ toolName: "travel.book_hotel", toolCallId: "call-1", arguments: { city: "Lisbon", estimatedPriceCents: 18_700 }, execute: () => hotelVendor.book(input.arguments), }); // result.toolResult — handler output // result.authorization — allow and decision metadata (side-effecting only) // result.evidence — auto-evidence submit result (side-effecting only)
Read-only tools skip steps 2–5 and return { toolResult } only.
Evidence idempotency key: evidence:{intentId}:{toolCallId}. Evidence submit failures throw PaybondAutoEvidenceSubmitError / PaybondEvidenceSubmitError with the successful toolResult attached — they are not logged and swallowed.
Optional per-tool customization:
evidenceMapper: (result, ctx) => ({ status: result.reservation.status === "confirmed" ? "completed" : result.reservation.status, cost_cents: result.reservation.price_cents, })
Trace events and observability
For the visual trace dashboard, timeline UI, data flow, and when to use local vs CLI vs hosted replay, see Middleware trace.
The interceptor emits structured PaybondTraceEvent records as each side-effecting tool moves through authorize → execute → finalize → evidence. Opt in with traceSink on PaybondAgentRun.bind() (or pass traceSink through paybond.instrument() / createGuardedAgent).
| Event | When emitted | Key fields |
|---|---|---|
tool_selected | Side-effecting tool resolved | toolName, toolCallId, operation |
spend_authorized | Paybond allows the spend | decisionId, amountCents, auditId |
spend_denied | Hard denial | message, code, auditId |
approval_required | Approval hold | message, code, auditId |
tool_executed | Handler finished | durationMs |
spend_finalized | Decision consumed or released | status: consumed | released |
evidence_submitted | Auto-evidence succeeded | evidenceId, presetId |
Each event also includes runId, operation, and recordedAt for correlation. Evidence idempotency key (evidenceId) matches the interceptor: evidence:{intentId}:{toolCallId}.
TypeScript — custom sink on bind:
import type { PaybondTraceEvent } from "@paybond/kit/agent"; const events: PaybondTraceEvent[] = []; const run = await paybond.agentRun.bind({ bootstrap: { kind: "sandbox", operation: "travel.book_hotel", requestedSpendCents: 20_000, }, registry, traceSink: (event) => events.push(event), }); await run.interceptor.wrapExecute({ /* ... */ }); // events: tool_selected → spend_authorized → tool_executed → spend_finalized → evidence_submitted
Python — same shape with snake_case event fields:
trace_events: list[dict[str, object]] = [] run = await paybond.agent_run.bind( { "bootstrap": { "kind": "sandbox", "operation": "travel.book_hotel", "requested_spend_cents": 20_000, }, "registry": registry, "trace_sink": trace_events.append, } )
Local dev dashboard — paybond dev smoke and paybond dev loop activate an in-process collector during the smoke run. Events are written to .paybond/dev-trace.jsonl so paybond dev trace can load them from another terminal in the same project directory (no traceSink required for CLI workflows):
paybond dev loop --offline # terminal 1 — or paybond dev loop after login paybond dev trace # terminal 2 — http://127.0.0.1:9477
The dashboard renders a vertical timeline with phase-colored steps, per-run detail JSON, and a live refresh every two seconds. See Middleware trace for the full UI and event model.
Per-run CLI trace — after a multi-step bind and execute, inspect the same event stream without the dashboard or upstream service logs:
paybond agent run bind --sandbox \ --operation travel.book_hotel \ --requested-spend-cents 20000 \ --completion-preset cost_and_completion \ --format json paybond agent tool execute --run-id <id> \ --operation travel.book_hotel \ --tool-call-id call-1 \ --result-body '{"status":"completed","cost_cents":18700}' \ --format json paybond agent run trace --run-id <id> --format table
CLI --result-body must match the bind completion_preset. With cost_and_completion, Paybond expects top-level status and cost_cents in the auto-evidence payload — the same shape paybond agent sandbox smoke uses for paid-tool. Nested vendor shapes (for example reservation.status / reservation.price_cents) are not mapped automatically on the CLI path; use an SDK evidenceMapper / evidence_mapper (see above) or pick a vendor-pack preset such as ach_travel_booking or x402_travel_booking with its canonical fields. A predicate mismatch surfaces as cli.agent.evidence_failed (Gateway may return HTTP 502 even though Harbor rejected the payload with a 422 predicate error).
For the travel vertical vendor pack (ACH or x402):
paybond agent run bind --sandbox \ --operation travel.book_hotel \ --requested-spend-cents 20000 \ --completion-preset ach_travel_booking \ --format json paybond agent tool execute --run-id <id> \ --operation travel.book_hotel \ --tool-call-id call-1 \ --result-body '{"status":"completed","confirmation_number":"AA-8JZ3QK","total_cents":18700,"fare_class":"economy"}' \ --format json
See Completion presets for preset ↔ evidence field mapping.
paybond agent run trace reads .paybond/runs/<run_id>.trace.json (written during tool execute) and prints a vertical checklist in table mode or full trace_events[] / steps[] in JSON mode. Denied or approval-held calls include spend_denied / approval_required events so you can debug policy failures from the trace output alone.
onTrace / on_trace remain supported as deprecated aliases for traceSink / trace_sink.
Generic tool executor adapter
Drop-in replacement for manual paybondRuntimeToolCallAdapter setup when your runtime exposes { name, execute } tools:
import { createGenericToolExecutor } from "@paybond/kit/agent"; const adapter = createGenericToolExecutor(); const guardedTools = adapter.wrapTools(run, [ { name: "travel.book_hotel", execute: async (args) => hotelVendor.book(args), }, { name: "lookup.weather", execute: async (args) => weather.lookup(args), }, ]); // In your orchestrator: const wrapped = guardedTools[0]; await wrapped.execute({ toolName: "travel.book_hotel", toolCallId: "call-1", arguments: { city: "Lisbon", estimatedPriceCents: 18_700 }, });
Python:
from paybond_kit.agent import create_generic_tool_executor adapter = create_generic_tool_executor() guarded = adapter.wrap_tools(run, [{"name": "travel.book_hotel", "execute": book_hotel}])
Framework adapter contract
export interface PaybondFrameworkAdapter { readonly name: string; wrapTools(run: PaybondAgentRun, tools: unknown): unknown; }
Optional framework packs (Vercel AI, LangGraph TS, OpenAI Agents) implement this contract without changing Paybond semantics. The generic adapter is always available.
Vercel AI SDK
TypeScript-only adapter at @paybond/kit/vercel-ai:
paybondVercelToolApproval(run)— centralizedtoolApprovalfor spend pre-checks.paybondVercelWrapTools(run, tools)— wraps side-effectingexecutefor auto-evidence.
Full guide: Vercel AI adapter.
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
LangGraph
TypeScript and Python adapters at @paybond/kit/langgraph and paybond_kit.langgraph_hooks:
paybondAwrapToolCall(run)/paybond_awrap_tool_call(run)—ToolNodehook for spend pre-checks and auto-evidence.paybondToolNode(tools, run)— convenience factory (TypeScript).
Full guide: LangGraph adapter.
Python: install paybond-kit[langgraph] before the smoke command — see LangGraph adapter — Scaffold and smoke.
paybond init agent-middleware --framework langgraph --out paybond-langgraph.ts paybond agent demo langgraph smoke \ --operation paid-tool \ --requested-spend-cents 100 \ --evidence-preset cost_and_completion \ --format json
Agent-agnostic (default)
TypeScript and Python — createPaybondGenericAgentConfig / create_paybond_generic_agent_config:
Full guide: Agent-agnostic adapter.
paybond init agent-middleware --framework generic --out paybond-generic.ts paybond agent demo generic smoke \ --operation paid-tool \ --requested-spend-cents 100 \ --evidence-preset cost_and_completion \ --format json
Claude Agents SDK
TypeScript and Python — in-process MCP custom tools via createPaybondClaudeAgentsConfig:
Full guide: Claude Agents adapter (beta).
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
OpenAI Agents SDK
TypeScript and Python — input guardrails with wrapped invoke / on_invoke_tool:
Full guide: OpenAI Agents adapter.
paybond init agent-middleware --framework openai --out paybond-openai-agents.ts # Python: paybond init agent-middleware --framework openai --out paybond_openai_agents.py pip install "paybond-kit[openai-agents]" # Python smoke only paybond agent demo openai-agents smoke \ --operation paid-tool \ --requested-spend-cents 100 \ --evidence-preset cost_and_completion \ --format json
Mastra
TypeScript only — wrap createTool({ execute }) handlers:
Full guide: Mastra adapter.
npm install @paybond/mastra @mastra/core paybond init agent-middleware --framework mastra --out paybond-mastra.ts paybond agent demo mastra smoke \ --operation paid-tool \ --requested-spend-cents 100 \ --evidence-preset cost_and_completion \ --format json
Cloudflare Agents
TypeScript only — wrap getTools() AI SDK tool() execute handlers:
Full guide: Cloudflare Agents adapter.
npm install @paybond/cloudflare-agents agents ai paybond init agent-middleware --framework cloudflare-agents --out paybond-cloudflare-agents.ts paybond agent demo cloudflare-agents smoke \ --operation paid-tool \ --requested-spend-cents 100 \ --evidence-preset cost_and_completion \ --format json
MCP hosts
Stdio paybond-mcp-server for Claude Desktop, Codex, and generic MCP orchestrators (typically npx -y -p @paybond/kit paybond-mcp-server after login). Hosts that need a network URL use the same tool surface over Streamable HTTP (https://mcp.paybond.ai/mcp with Bearer paybond_sk_..., or paybond mcp serve --transport http).
Full guide: MCP server. Setup walkthrough: Coding-agent setup.
paybond login npx -y -p @paybond/kit paybond-mcp-server
Additional framework adapters
Native runner helpers for Mastra, CrewAI, Pydantic AI, Google ADK, Microsoft Agent Framework, and Cloudflare Agents ship on the same middleware contract as the core adapters:
- Mastra adapter
- CrewAI adapter
- Pydantic AI adapter
- Google ADK adapter
- Microsoft Agent Framework adapter
- Cloudflare Agents adapter
Tenant isolation
- Run binding always derives
tenantIdfromPaybond.open()/ session credentials — never from tool arguments. - The interceptor rejects operations not in the bound intent
allowedTools. - Evidence submit uses the same
(tenantId, intentId)as authorization. - Do not share one
PaybondAgentRunacross concurrent agent tasks.
Approval holds vs hard denials
assertSpendAuthorized may return allow: false for an approval hold or a hard denial. Middleware propagates:
PaybondSpendApprovalRequiredError— surface to operators, approve, retry withapprovalToken.PaybondSpendDeniedError— do not execute the tool.
See Agent integrations — approval holds.
Migration from per-tool guardTool
Before (manual, one handler at a time):
const guard = paybond.spendGuard(intentId, capabilityToken); const bookHotel = guard.guardTool({ operation: "travel.book_hotel", requestedSpendCents: 20_000 }, handler); // You must also call submitEvidence yourself after success.
After (instrument() — policy file or preset, one call):
const { agentTools } = await paybond.instrument({ policy: "./paybond.policy.yaml", tools: { "travel.book_hotel": handler }, }); // Evidence submits automatically on successful side-effecting calls.
Already using registry and bind manually? paybond.wrapTools(run, tools) or createGenericToolExecutor().wrapTools(run, tools) still work for an existing bound run.
Advanced: per-tool guardTool / guard_tool
Reserve per-tool guardTool / guard_tool for single-handler integrations or when you need full control over evidence timing. Multi-tool agents should use instrument() or wrapTools() / wrap_tools() instead.
See Agent integrations — Advanced: per-tool guardTool for TypeScript and Python single-handler examples.
CLI commands
The Kit CLI exposes the same middleware surface for automation and CI. Commands default to sandbox unless --production is passed.
Local dev — guided smoke with checklist output and a trace dashboard (no app code):
paybond dev loop --offline paybond dev trace
With sandbox credentials, use paybond dev loop (login when needed) instead of --offline. See Agent quickstart.
paybond login paybond agent sandbox smoke \ --policy-file paybond.policy.yaml \ --result-body '{"status":"completed","cost_cents":18700}' \ --format json
Or with explicit flags (no policy file):
paybond agent sandbox smoke \ --operation paid-tool \ --requested-spend-cents 100 \ --evidence-preset cost_and_completion \ --result-body '{"status":"ok","cost_cents":100}' \ --format json
Production attach (--production) is documented under Going to production — not required for sandbox smoke.
Policy-file smoke bootstrap: --policy-file maps tool evidence_preset to Gateway completion_preset. Do not send evidence_schema or template_id in the same bootstrap request. Kit 0.11.6+ omits those fields automatically; upgrade if policy-file CI smoke fails after login with validation_error. See Agent policy — Sandbox bootstrap.
| Command | Purpose |
|---|---|
paybond init agent-middleware | Scaffold a registry and PaybondAgentRun integration file. |
paybond agent run bind | Sandbox bootstrap or production attach; persist .paybond/runs/<run_id>.json. Accepts --policy-file. |
paybond agent run status | Inspect a persisted run. |
paybond agent run trace | Show middleware trace events for a run (--format json|table). Reads .paybond/runs/<run_id>.trace.json from tool execute. |
paybond agent tool execute | Full intercept path with --result-body (v1 does not spawn external processes). |
paybond agent tool validate | Authorization-only check. |
paybond agent registry validate --file <path> | Validate registry YAML/JSON before bind. |
paybond policy validate-tools --file <path> | Validate policy YAML locally (--local-only) or against Gateway (--remote). |
paybond agent sandbox smoke | End-to-end bind and execute in one invocation. Accepts --policy-file. |
paybond dev smoke | Travel-preset smoke with checklist table and local trace event. --offline for no API key (includes in-process x402 /fund state machine). |
paybond dev trace | Local HTTP trace dashboard on port 9477; vertical timeline from .paybond/dev-trace.jsonl. See Middleware trace. |
paybond dev loop | Login (optional) → policy scaffold → validate → smoke; prints trace and audit log paths. |
paybond dev up | Start/stop local WireMock Gateway (Docker). Bundled mappings include x402 fund 402 → 202 → 200 sequence for contract tests. |
paybond agent demo vercel-ai smoke | No-LLM Vercel AI SDK demo: toolApproval with wrapped execute and auto-evidence. |
Registry file format (v1):
version: 1 default_deny: true tools: paid-tool: side_effecting: true evidence_preset: cost_and_completion
See CLI contract for JSON field shapes and exit codes.
paybond doctor --agent runs the same sandbox smoke path as a doctor check when sandbox credentials are present.
Package paths
| Language | Import |
|---|---|
| TypeScript (facade) | Paybond, paybond.toolRegistry(), paybond.agentRun.bind() from @paybond/kit |
| TypeScript (core) | @paybond/kit/agent |
| Python (facade) | paybond.tool_registry(), paybond.agent_run.bind() |
| Python (core) | paybond_kit.agent |
Going to production
Sandbox paths (paybond.agent(), paybond.instrument({ sandbox: true }), CLI smoke, and paybond dev loop) never require payee identities or signing credentials — Gateway issues sandbox capabilities.
When you have a funded production intent, attach middleware so every successful side-effecting call auto-submits signed evidence. See Production attach for CLI flags, SDK productionEvidence, console-managed attach bundles, and env fallbacks.
Related
- Agent integrations — runtime patterns and framework scaffolds
- Agent-agnostic adapter
- Claude Agents adapter
- OpenAI Agents adapter
- Vercel AI adapter
- LangGraph adapter
- MCP server
- Mastra adapter
- CrewAI adapter
- Pydantic AI adapter
- Google ADK adapter
- Microsoft Agent Framework adapter
- Cloudflare Agents adapter
- Completion presets — catalog preset IDs for
evidencePreset - Capabilities — obtaining
intent_idandcapability_token - Agent middleware guide — selection rule for buyers and coding agents
- Agent policy-as-code —
paybond.policy.yamlformat and CI validation - Policy hot-reload — runtime policy updates for long-lived runs
- Agent policy validate — local and server-authoritative policy checks