paybondpaybond
Sign in

TypeScript quickstart

Open a service-account Paybond Kit session in TypeScript, verify capabilities, and submit signed evidence against Harbor.

TypeScript quickstart

This quickstart walks through the canonical Paybond Kit flow in TypeScript:

  1. Open an authenticated session against the Gateway with a service-account API key.
  2. Verify a Biscuit capability token scoped to a funded intent.
  3. Submit signed evidence so Harbor can evaluate the predicate and transition the intent.

Every call below is tenant-scoped: the realm is derived from the Gateway exchange and bound to the returned client. You never pass a tenant id by hand, and the Kit throws if Harbor echoes a different tenant or intent than you requested.

allowedTools should contain your application's own tool or operation names. Paybond does not define a fixed catalog here; Harbor simply enforces that the later verifyCapability(..., operation) call matches one of the names you bound onto the intent.

For the full API surface, see the TypeScript SDK reference.

Prerequisites

  • Node.js 22+ (global fetch, crypto.randomUUID).
  • A paybond_sk_… service-account API key issued for your tenant realm.
  • The Gateway and Harbor base URLs reachable from your runtime (for example https://gateway.example.com, https://harbor.example.com).
  • For the principal-signed intent create flow: a 32-byte Ed25519 seed for the principal and a 32-byte seed for the payee bound to the intent. The Kit validates seed length in paybond.intents.create and paybond.intents.submitEvidence.

Install

Install the public package:

npm install @paybond/kit

For monorepo-local development against this checkout, the example app under examples/paybond-kit-typescript/ still uses a workspace file: dependency. External consumers should prefer the published npm package.

Authenticated session

Paybond.open exchanges your service-account API key for a short-lived Harbor JWT at the Gateway (POST /v1/auth/harbor-access), caches the tenant realm echoed by the response, and returns a tenant-bound Harbor client.

import { Paybond } from "@paybond/kit";

const paybond = await Paybond.open({
  gatewayBaseUrl: "https://gateway.example.com",
  apiKey: process.env.PAYBOND_API_KEY!,
  harborBaseUrl: "https://harbor.example.com",
});

try {
  console.log("tenant realm:", paybond.harbor.tenantId);
} finally {
  await paybond.aclose();
}

Key properties of the session:

  • Tenant is derived, not supplied. The tenant realm is whatever the Gateway echoes. Do not read the tenant from request bodies or environment variables at call time.
  • Token rotation is automatic. Each Harbor call awaits a cached JWT and refreshes before expiry. Force rotation with await paybond.rotateHarborToken() during a credential rotation drill.
  • Sessions are single-tenant. Construct one Paybond instance per (tenant, service account). Never share across tenants or reuse across concurrent runs bound to different intents.
  • Release resources. Always finish with await paybond.aclose() when the session is done. Paybond.open throws GatewayAuthError on any gateway or token failure.

Verify and submit evidence walkthrough

Once you have a funded intent id and a Biscuit capability token minted for the payee, the tenant-safe integration pattern is: verify the capability, run the tool work, then submit signed evidence.

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

function seed32FromHex(hex: string): Uint8Array {
  const buf = Buffer.from(hex.replace(/^0x/i, ""), "hex");
  if (buf.length !== 32) {
    throw new Error("seed must be 32 bytes (64 hex characters)");
  }
  return new Uint8Array(buf);
}

const paybond = await Paybond.open({
  gatewayBaseUrl: "https://gateway.example.com",
  apiKey: process.env.PAYBOND_API_KEY!,
  harborBaseUrl: "https://harbor.example.com",
});

try {
  const intentId = process.env.PAYBOND_INTENT_ID!; // UUID string of the funded intent
  const capToken = process.env.PAYBOND_CAPABILITY!; // base64 Biscuit token
  const payeeSeed = seed32FromHex(process.env.PAYBOND_PAYEE_SEED_HEX!);

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

  const verified = await binding.harbor.verifyCapability({
    intentId: binding.intentId,
    token: binding.capabilityToken,
    operation: "payments.capture",
    requestedSpendCents: 18_700,
  });
  if (!verified.allow) {
    throw new Error(`verify denied: ${verified.code ?? "deny"} ${verified.message ?? ""}`);
  }

  // …run the tool work guarded by the verified capability here…

  const submittedAt = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
  const result = await paybond.intents.submitEvidence({
    intentId,
    payeeDid: "did:web:example.com#booker-agent",
    payeeSigningSeed: payeeSeed,
    payload: { flight_booked: true, price: 187, confirmation: "PNR-4GH271" },
    artifactsBlake3Hex: [],
    submittedAtRfc3339: submittedAt,
    idempotencyKey: `evidence:${intentId}:${submittedAt}`,
  });
  console.log("evidence recorded:", result.state, result.predicatePassed);
} finally {
  await paybond.aclose();
}

Notes:

  • verifyCapability maps to Harbor POST /verify (operation id harborVerifyCapability). A 200 response with allow: false is not an HTTP error — treat code and message as the deny reason.
  • paybond.intents.submitEvidence builds a payee-signed body with the same canonical digest rules as the rest of the Kit and POSTs it to Harbor POST /intents/{id}/evidence (operation id harborSubmitEvidence). Reuse the same idempotencyKey to safely retry a transient 5xx; changing the body under the same key will fail with 409.
  • Harbor enforces tenant and intent echo on verify; the client throws on mismatches so confused-deputy bugs fail closed. See the TypeScript SDK reference for the exact Error messages.
  • Keep payeeSigningSeed in a secret manager. The value must be exactly 32 bytes and must correspond to the payee key bound on the intent.

Principal-signed intent create

paybond.intents.create builds a principal-signed POST /intents body and submits it (operation id harborCreateIntent). intent_id defaults to crypto.randomUUID() when omitted.

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

function seed32FromHex(hex: string): Uint8Array {
  const buf = Buffer.from(hex.replace(/^0x/i, ""), "hex");
  if (buf.length !== 32) {
    throw new Error("seed must be 32 bytes (64 hex characters)");
  }
  return new Uint8Array(buf);
}

const principalSeed = seed32FromHex(process.env.PAYBOND_PRINCIPAL_SEED_HEX!);
const payeeSeed = seed32FromHex(process.env.PAYBOND_PAYEE_SEED_HEX!);

const paybond = await Paybond.open({
  gatewayBaseUrl: "https://gateway.example.com",
  apiKey: process.env.PAYBOND_API_KEY!,
  harborBaseUrl: "https://harbor.example.com",
});

try {
  const created = await paybond.intents.create({
    principalDid: "did:web:example.com#principal",
    principalSigningSeed: principalSeed,
    payeeDid: "did:web:example.com#booker-agent",
    budget: { currency: "usd", max_spend_usd: 200 },
    predicate: { version: 1, root: { op: "true" } },
    currency: "usd",
    amountCents: 20_000,
    evidenceSchema: { type: "object" },
    deadlineRfc3339: "2030-12-31T23:59:59Z",
    allowedTools: ["travel.planner.plan", "travel.booker.purchase"],
    idempotencyKey: crypto.randomUUID(),
  });

  const intentId = String(created.intent_id);
  await paybond.intents.submitEvidence({
    intentId,
    payeeDid: "did:web:example.com#booker-agent",
    payeeSigningSeed: payeeSeed,
    payload: { flight_booked: true, price: 187, confirmation: "PNR-4GH271" },
    artifactsBlake3Hex: [],
  });
  console.log("created intent", intentId);
} finally {
  await paybond.aclose();
}

Managed-policy intent creation (signing_version 3 / policy_binding) is not wrapped by this helper; use paybond.harbor.createIntent with a pre-built body when you need registry-bound signing. See Harbor API — policy registry.

allowedTools is your namespace, not Paybond's. For example, if your runtime later verifies operation: "travel.booker.purchase", that exact string must appear in the intent allow-list.

Settlement confirmation

Settlement confirmation (release / refund) is an operator-tier action and is not wrapped by the Kit today. Confirm through the Operator Console or call Harbor directly: POST /intents/{id}/settlement/confirm (operation id harborConfirmSettlement). See the Harbor API reference.

Error handling

Kit symbolRaised whenNotes
GatewayAuthErrorharbor-access rejected the API key or omitted tenant_id / access_token.statusCode and redacted bodyText on the instance.
HarborHttpErrorAny Harbor 4xx/5xx after retries are exhausted.Log statusCode, url, and a redacted bodyText; never log the API key, JWT, or capability token.
Plain Error (tenant / intent mismatch)Harbor echoed a different tenant or intent than requested on verify or ledger reads.Treat as severity-zero; do not retry.

See the Error handling guide for retry/backoff guidance and logging hygiene.

Further reading