TypeScript quickstart
This quickstart walks through the canonical Paybond Kit flow in TypeScript:
- Open an authenticated session against the Gateway with a service-account API key.
- Verify a Biscuit capability token scoped to a funded intent.
- 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.createandpaybond.intents.submitEvidence.
Install
Install the public package:
npm install @paybond/kitFor 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
Paybondinstance 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.openthrowsGatewayAuthErroron 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:
verifyCapabilitymaps to HarborPOST /verify(operation idharborVerifyCapability). A200response withallow: falseis not an HTTP error — treatcodeandmessageas the deny reason.paybond.intents.submitEvidencebuilds a payee-signed body with the same canonical digest rules as the rest of the Kit and POSTs it to HarborPOST /intents/{id}/evidence(operation idharborSubmitEvidence). Reuse the sameidempotencyKeyto safely retry a transient 5xx; changing the body under the same key will fail with409.- 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
Errormessages. - Keep
payeeSigningSeedin 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 symbol | Raised when | Notes |
|---|---|---|
GatewayAuthError | harbor-access rejected the API key or omitted tenant_id / access_token. | statusCode and redacted bodyText on the instance. |
HarborHttpError | Any 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
- TypeScript SDK reference — full surface, including
Paybond.open,paybond.intents.{create, submitEvidence},paybond.harbor.verifyCapability,paybond.aclose, and typed error classes. - Authentication & tenant binding.
- Evidence & artifacts and Capabilities.
- Harbor API — the OpenAPI contract the Kit calls.
- TypeScript example project — a runnable sketch you can execute locally from
examples/paybond-kit-typescript/.