The supported TypeScript path is the runtime-neutral wrapper around your tool handler: use paybond.spendGuard(...) for direct handlers or paybondRuntimeToolCallAdapter(...) when your runtime hands you a tool-call object plus an executor.
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
npm install @paybond/kit
For a complete example application, see the reference implementation linked at the end of this page.
Required environment
Only PAYBOND_API_KEY comes from Paybond. The APP_* variables are example-local placeholders for values your app or trusted signer owns.
npx -p @paybond/kit paybond login
export APP_PRINCIPAL_DID="did:web:example.com#principal"
export APP_PRINCIPAL_SEED_HEX="..."
export APP_PAYEE_DID="did:web:example.com#hotel-booker"
export APP_PAYEE_SEED_HEX="..."
export APP_INTENT_CREATE_RECOGNITION_PROOF_JSON='{"key_id":"..."}'
export APP_EVIDENCE_RECOGNITION_PROOF_JSON='{"key_id":"..."}'
export APP_SETTLEMENT_RAIL="x402_usdc_base" # optional
export APP_X402_PAYMENT_SIGNATURE="demo-signature" # optional
End-to-end flow
import { Buffer } from "node:buffer";
import { Paybond } 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);
}
function requiredJsonEnv(envName: string): Record<string, unknown> {
const raw = process.env[envName];
if (!raw) {
throw new Error(`missing env ${envName}`);
}
const parsed = JSON.parse(raw) as unknown;
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error(`${envName} must contain a JSON object`);
}
return parsed as Record<string, unknown>;
}
type Reservation = Readonly<{
hotel: string;
city: string;
status: "confirmed";
price_cents: number;
confirmation: string;
}>;
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({
apiKey: process.env.PAYBOND_API_KEY!,
expectedEnvironment: "sandbox",
});
try {
const intentId = crypto.randomUUID();
const created = await paybond.intents.create({
principalDid: process.env.APP_PRINCIPAL_DID!,
principalSigningSeed: seed32FromHex("APP_PRINCIPAL_SEED_HEX"),
payeeDid: process.env.APP_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"],
recognitionProof: requiredJsonEnv("APP_INTENT_CREATE_RECOGNITION_PROOF_JSON"),
settlementRail:
process.env.APP_SETTLEMENT_RAIL === "x402_usdc_base"
? "x402_usdc_base"
: "stripe_connect",
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 guard = paybond.spendGuard(intentId, capabilityToken);
const bookHotelTool = {
name: "travel.book_hotel",
description: "Reserve one hotel room inside the intent budget.",
execute: guard.guardTool(
{ operation: "travel.book_hotel", requestedSpendCents: 18_700 },
bookHotel,
),
};
const reservation = await bookHotelTool.execute("Lisbon", 18_700);
const submitted = await paybond.intents.submitEvidence({
intentId,
payeeDid: process.env.APP_PAYEE_DID!,
payeeSigningSeed: seed32FromHex("APP_PAYEE_SEED_HEX"),
payload: { reservation },
artifactsBlake3Hex: [],
recognitionProof: requiredJsonEnv("APP_EVIDENCE_RECOGNITION_PROOF_JSON"),
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 agent SDKs fit
In a real Vercel AI SDK, Anthropic, Gemini, OpenAI, or custom agent app, keep the same wrapper body and place it around the tool handler you register with the SDK. The important invariant is not the 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 guard must stay scoped to one
(tenant, intent, capability_token) - evidence submission should happen after the guarded tool returns its result
Stablecoin rail variant
Set APP_SETTLEMENT_RAIL="x402_usdc_base" to request the Base / USDC rail. The example keeps the commercial amount in USD (budget.currency: "usd", amountCents, max_spend_usd) while Harbor handles x402 funding and settlement in USDC on Base.
If create does not return capability_token, the example calls paybond.intents.fund, prints the paymentRequired challenge, and retries after you provide APP_X402_PAYMENT_SIGNATURE.
Expected Harbor outcome
createmay returncapability_tokenimmediately, or the example may obtain it throughpaybond.intents.fundonx402_usdc_base.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. - Missing
APP_X402_PAYMENT_SIGNATURE: Harbor returned an x402 challenge and is waiting for a signed retry. - Verify denial: the wrapper used the wrong
operation, requested spend exceeded the capability, or the capability token is stale. - Tenant mismatch: treat as a critical tenant-isolation error; 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-runtime-agent-typescript/src/demo.ts - Example README:
examples/paybond-kit-runtime-agent-typescript/README.md