paybondpaybond
Sign in

Recipe · Framework

Spend controls for Express and Fastify agent routes

Wrap paybond.instrument() in Express or Fastify route handlers — request-scoped bind, spend verify before execute, and auto-evidence over HTTP.

  1. 1Wire
  2. 2Authorize
  3. 3Smoke

You'll build

Express/Fastify route handlers that refuse unpaid agent spend

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.

TypeScript only. The route samples below use Express/Fastify Node APIs. For Python HTTP hosts, use the same paybond.instrument() / bind() contract from agent-agnostic spend controls.

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

  • Load once, bind per request

    instrument() at startup; bind intentId + capabilityToken on each /agent route.

  • spend verify before execute

    Guarded tools authorize operation and amount before HTTP handlers run side effects.

  • Auto-evidence

    Wrapped execute finalizes spend and submits signed evidence after success.

  • TypeScript HTTP samples

    Express/Fastify route examples are TypeScript; Python hosts use the same instrument()+bind() contract via agent-agnostic.

Why Paybond (not just HTTP auth)?

Route-level auth does not enforce a spend limit, a per-operation permission check (capability token), or a signed completion receipt tied to a spend agreement (intent).

Express/Fastify agent routes alone versus Paybond Harbor spend controls
  • Model / input guardrails

    HTTP routes alone
    Yes — SDK or host checks and approvals
    With Paybond
    Yes — plus Harbor authorize at the tool boundary
  • Spend boundary

    HTTP routes alone
    No per-tool Harbor budget or capability token
    With Paybond
    Per-call and intent budgets enforced before invoke
  • Signed evidence

    HTTP routes alone
    SDK traces / logs only
    With Paybond
    Signed completion digests bound to the intent
  • Intent binding

    HTTP routes alone
    No Harbor intent or settlement receipt
    With Paybond
    Capability token + intentId from authenticated bind
  • Paid tool deny / HITL

    HTTP routes alone
    Host or SDK approvals only
    With Paybond
    spend verify, deny, or HITL hold before side effects

How it works

Express and Fastify load policy once, bind per request, then execute Paybond-wrapped tools over HTTP.

HTTP route flow

  1. POST /agent/tools/execute

    Client sends tool call + session bind credentials

  2. instrumented.bind

    Harbor session scoped to this request

    • intentId + capabilityToken
    • From auth/session layer
    • Never raw client tenant ids
  3. tool.execute

    Guarded handler performs the paid work

  4. Evidence

    Wrapped execute finalizes spend + auto-evidence

Express and Fastify load policy once, bind per request, then execute Paybond-wrapped tools over HTTP.

3-minute quickstart

Smoke the sandbox contract your HTTP routes will enforce:

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

When the smoke succeeds you should see:

  • ✓ Spend approved
  • ✓ Tool completed
  • ✓ Evidence verified (cost_and_completion)

What success looks like

Example status after a Paybond-guarded Express / Fastify agent routes tool call: approved spend, requested amount, and verified cost_and_completion evidence.

What success looks like

Authorized tool call · illustrative

Sandbox path
Operation
paid-tool
Status
Approved
Requested
$1.00
Evidence
Verified
Preset
cost_and_completion

Wire middleware

Shared module

Load policy once at process startup; bind per request in Express or Fastify handlers.

paybond-session.ts

TS
TypeScript code sampleSwipe to inspect long lines
// 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,
  },
});

Express

paybond-session.ts

TS
TypeScript code sampleSwipe to inspect long lines
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

paybond-session.ts

TS
TypeScript code sampleSwipe to inspect long lines
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:

paybond-session.ts

TS
TypeScript code sampleSwipe to inspect long lines
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:

paybond-session.ts

TS
TypeScript code sampleSwipe to inspect long lines
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 ...

Production checklist

Production checklist for Paybond-guarded Express and Fastify agent routes.

Production checklist

Works with

Works with

  • Agent-agnostic
  • Vercel
  • OpenAI
  • MCP

Ready to test?

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