paybondpaybond
Sign in

Agents SDK tutorial (TypeScript)

Create an intent, wrap an OpenAI Agents SDK-style tool handler with Paybond verification, and submit signed evidence from TypeScript.

Agents SDK tutorial (TypeScript)

Paybond does not ship a first-party TypeScript OpenAI Agents helper in this repo today. The supported TypeScript path is a thin app-side wrapper around your tool handler using PaybondCapabilityBinding and harbor.verifyCapability(...).

That wrapper is deliberately small: the agent runtime stays yours, while Paybond still controls tenant binding, capability checks, and evidence submission.

What you will build

One guarded tool flow:

  1. Open a tenant-bound Paybond session.
  2. Create an intent for travel.book_hotel.
  3. Read the returned capability_token.
  4. Wrap the tool handler with a capability check.
  5. Execute the tool only if Harbor allows it.
  6. Submit signed evidence and inspect the resulting Harbor state.

Install

For published packages:

npm install @paybond/kit

For the repo example:

cd examples/paybond-kit-openai-agents-typescript
npm install
npm run build --prefix ../../kit/ts
npm run build

Required environment

export PAYBOND_GATEWAY_URL="https://gateway.example.com"
export PAYBOND_HARBOR_URL="https://harbor.example.com"
export PAYBOND_API_KEY="paybond_sk_..."
export PAYBOND_PRINCIPAL_DID="did:web:example.com#principal"
export PAYBOND_PRINCIPAL_SEED_HEX="..."
export PAYBOND_PAYEE_DID="did:web:example.com#hotel-booker"
export PAYBOND_PAYEE_SEED_HEX="..."

End-to-end flow

import { Buffer } from "node:buffer";
import { Paybond, PaybondCapabilityBinding } from "@paybond/kit";

function seed32FromHex(envName: string): Uint8Array {
  const value = process.env[envName];
  if (!value) {
    throw new Error(`missing env ${envName}`);
  }
  const raw = Buffer.from(value.replace(/^0x/i, ""), "hex");
  if (raw.length !== 32) {
    throw new Error(`${envName} must decode to exactly 32 bytes`);
  }
  return new Uint8Array(raw);
}

type Reservation = Readonly<{
  hotel: string;
  city: string;
  status: "confirmed";
  price_cents: number;
  confirmation: string;
}>;

function guardToolWithPaybond<TArgs extends unknown[], TResult>(init: {
  binding: PaybondCapabilityBinding;
  operation: string;
  requestedSpendCents: number;
  execute: (...args: TArgs) => Promise<TResult>;
}): (...args: TArgs) => Promise<TResult> {
  return async (...args: TArgs): Promise<TResult> => {
    const verified = await init.binding.harbor.verifyCapability({
      intentId: init.binding.intentId,
      token: init.binding.capabilityToken,
      operation: init.operation,
      requestedSpendCents: init.requestedSpendCents,
    });
    if (!verified.allow) {
      throw new Error(
        `verify denied: ${verified.code ?? "deny"} ${verified.message ?? ""}`.trim(),
      );
    }
    return init.execute(...args);
  };
}

async function bookHotel(city: string, nightlyBudgetCents: number): Promise<Reservation> {
  return {
    hotel: "Harbor House",
    city,
    status: "confirmed",
    price_cents: nightlyBudgetCents,
    confirmation: "HB-2049",
  };
}

const paybond = await Paybond.open({
  gatewayBaseUrl: process.env.PAYBOND_GATEWAY_URL!,
  apiKey: process.env.PAYBOND_API_KEY!,
  harborBaseUrl: process.env.PAYBOND_HARBOR_URL!,
});

try {
  const intentId = crypto.randomUUID();
  const created = await paybond.intents.create({
    principalDid: process.env.PAYBOND_PRINCIPAL_DID!,
    principalSigningSeed: seed32FromHex("PAYBOND_PRINCIPAL_SEED_HEX"),
    payeeDid: process.env.PAYBOND_PAYEE_DID!,
    budget: { currency: "usd", max_spend_usd: 200 },
    predicate: {
      version: 1,
      root: {
        op: "and",
        clauses: [
          {
            op: "completion",
            path: ["reservation", "status"],
            value: "confirmed",
          },
          {
            op: "budget_cap",
            path: ["reservation", "price_cents"],
          },
        ],
      },
    },
    currency: "usd",
    amountCents: 20_000,
    evidenceSchema: { type: "object", properties: { reservation: { type: "object" } } },
    deadlineRfc3339: "2030-12-31T23:59:59Z",
    allowedTools: ["travel.book_hotel"],
    intentId,
    idempotencyKey: `intent:${intentId}`,
  });

  const capabilityToken = String(created.capability_token ?? "");
  if (!capabilityToken) {
    throw new Error("intent created without capability_token; ensure the intent is funded");
  }

  const binding = new PaybondCapabilityBinding(
    paybond.harbor,
    intentId,
    capabilityToken,
  );

  const bookHotelTool = {
    name: "travel.book_hotel",
    description: "Reserve one hotel room inside the intent budget.",
    execute: guardToolWithPaybond({
      binding,
      operation: "travel.book_hotel",
      requestedSpendCents: 18_700,
      execute: bookHotel,
    }),
  };

  const reservation = await bookHotelTool.execute("Lisbon", 18_700);
  const submitted = await paybond.intents.submitEvidence({
    intentId,
    payeeDid: process.env.PAYBOND_PAYEE_DID!,
    payeeSigningSeed: seed32FromHex("PAYBOND_PAYEE_SEED_HEX"),
    payload: { reservation },
    artifactsBlake3Hex: [],
    idempotencyKey: `evidence:${intentId}`,
  });

  console.log(
    JSON.stringify(
      {
        intentState: created.state,
        tool: bookHotelTool.name,
        settlementState: submitted.state,
        predicatePassed: submitted.predicatePassed,
      },
      null,
      2,
    ),
  );
} finally {
  await paybond.aclose();
}

Where the OpenAI Agents SDK fits

In a real OpenAI Agents app, keep the same wrapper body and place it around the tool handler you register with the SDK. The important invariant is not the runtime hook name; it is that travel.book_hotel is verified against Harbor before the tool executes.

Because the wrapper is app-owned:

  • your tool name must match allowedTools exactly
  • the binding must stay scoped to one (tenant, intent, capability_token)
  • evidence submission should happen after the guarded tool returns its result

Expected Harbor outcome

  • create returns capability_token only after the intent is funded.
  • submitEvidence returns a state plus optional predicatePassed.
  • Settlement confirmation remains an operator-tier action in the current Kit surface. See Harbor API.

Common failure cases

  • Missing capability_token: the intent is not funded yet.
  • Verify denial: the wrapper used the wrong operation, requested spend exceeded the capability, or the capability token is stale.
  • Tenant mismatch: treat as severity-zero; do not retry with a different tenant.
  • Evidence rejection: the payee seed, DID, or payload does not match what the intent expects.

Reference implementation