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:
- Open a tenant-bound Paybond session.
- Create an intent for
travel.book_hotel. - Read the returned
capability_token. - Wrap the tool handler with a capability check.
- Execute the tool only if Harbor allows it.
- Submit signed evidence and inspect the resulting Harbor state.
Install
For published packages:
npm install @paybond/kitFor the repo example:
cd examples/paybond-kit-openai-agents-typescript
npm install
npm run build --prefix ../../kit/ts
npm run buildRequired 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
allowedToolsexactly - 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
createreturnscapability_tokenonly after the intent is funded.submitEvidencereturns a state plus optionalpredicatePassed.- 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
- Example app:
examples/paybond-kit-openai-agents-typescript/src/demo.ts - Example README:
examples/paybond-kit-openai-agents-typescript/README.md