Not every agent runs inside a framework SDK. Express and Fastify apps often expose /agent/run endpoints that orchestrate tool calls server-side. Paybond loads policy once at startup and binds per request so each session gets its own intent scope.
Deferred bind by default
paybond.instrument() does not guess production intentId. Pass bind credentials from your auth layer on each request — never from unauthenticated client tenant identifiers alone.
Try it
terminal
paybond login
paybond agent sandbox smoke \
--operation paid-tool \
--requested-spend-cents 100 \
--evidence-preset cost_and_completion \
--result-body '{"status":"ok","cost_cents":100}' \
--format jsonShared module
// paybond-runtime.ts
import { Paybond } from "@paybond/kit";
export const paybond = await Paybond.open({
apiKey: process.env.PAYBOND_API_KEY!,
});
export const instrumented = await paybond.instrument({
policy: process.env.PAYBOND_POLICY_FILE ?? "./paybond.policy.yaml",
tools: {
"travel.book_hotel": bookHotel,
searchWeb: searchWeb,
},
});
Python equivalent: await paybond.instrument(policy=..., tools=...) at import time.
Express
import express from "express";
import { instrumented } from "./paybond-runtime.js";
const app = express();
app.use(express.json());
app.post("/agent/tools/execute", async (req, res) => {
const { toolName, toolCallId, arguments: args, intentId, capabilityToken } = req.body;
// Resolve credentials from your session — not raw client tenant ids
const runtime = await instrumented.bind({ intentId, capabilityToken });
const tool = runtime.tools.find((t) => t.name === toolName);
if (!tool) {
res.status(404).json({ error: "tool_not_registered" });
return;
}
const result = await tool.execute({ toolName, toolCallId, arguments: args });
res.json({ result });
});
app.listen(3000);
Fastify
import Fastify from "fastify";
import { instrumented } from "./paybond-runtime.js";
const fastify = Fastify();
fastify.post("/agent/tools/execute", async (request, reply) => {
const { toolName, toolCallId, arguments: args, intentId, capabilityToken } = request.body as {
toolName: string;
toolCallId: string;
arguments: Record<string, unknown>;
intentId: string;
capabilityToken: string;
};
const runtime = await instrumented.bind({ intentId, capabilityToken });
const tool = runtime.tools.find((t) => t.name === toolName);
if (!tool) {
return reply.code(404).send({ error: "tool_not_registered" });
}
const result = await tool.execute({ toolName, toolCallId, arguments: args });
return { result };
});
await fastify.listen({ port: 3000 });
Lazy context alternative
When bind credentials live on request.session:
const instrumented = await paybond.instrument({
policy: "./paybond.policy.yaml",
tools: { /* ... */ },
context: () => requestStore.getStore()!.runtime,
});
Set requestStore in middleware before tool routes run — same pattern as Next.js agent checkout.
Sandbox shortcut
For local dev without manual bind:
const { tools, run } = await paybond.agent({
policy: "travel",
tools: { "travel.book_hotel": bookHotel },
});
// tools are pre-bound to sandbox
CI validation
terminal
paybond policy validate-tools --file paybond.policy.yaml --local-only
npm run smoke # paybond agent sandbox smoke --policy-file paybond.policy.yaml ...Related guides
- Agent-agnostic spend controls —
{ name, execute }tools - Agent middleware — interceptor and evidence
- Protect Stripe payments from agents — payment handler example
Developer reference: /docs/kit/agent-middleware.