paybondpaybond
Sign in

Express and Fastify agent routes

Wrap paybond.instrument() in Express or Fastify route handlers — request-scoped bind, lazy context, and guarded tool execution over HTTP.

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
Terminal commandSwipe to inspect long lines
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 json

Shared 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
Terminal commandSwipe to inspect long lines
paybond policy validate-tools --file paybond.policy.yaml --local-only
npm run smoke   # paybond agent sandbox smoke --policy-file paybond.policy.yaml ...

Developer reference: /docs/kit/agent-middleware.