ZZanoraDocs

Buy

@zanora/sdk in TypeScript: discover capabilities, pay for REST and MCP ones with the same call, verify receipts, handle refusals, and cancel safely.

Terminal
npm install @zanora/sdk

Create the agent

TypeScript
import { readFileSync } from "node:fs";
import { ZanoraAgent } from "@zanora/sdk";

const agent = new ZanoraAgent({
  apiKey: process.env.ZANORA_API_KEY!,                     // workspace key: authenticates API calls
  walletId: "wal_f33b5618…",                               // the agent's wallet
  agentId: "invoice-bot",                                  // appears on payment proofs
  privateKeyPem: readFileSync("agent-key.pem", "utf8"),    // signs payment proofs
  maxPriceMinor: 500n,                                     // refuse to sign above $5.00
});
OptionRequiredMeaning
apiKeyyes, on any real deploymentthe workspace key. Without it, calls fail with invalid api key
walletIdyesthe agent wallet to spend from
agentIdyesa plain label for this agent
privateKeyPemyesthe ed25519 private key whose public half the wallet holds
maxPriceMinornoa bigint ceiling. The client refuses to sign a proof above it (PRICE_ABOVE_CLIENT_LIMIT)
mcpCallerfor MCP capabilitieshow to reach MCP servers. See below
gatewayUrlnodefaults to https://api.zanora.dev. Set it only for a sandbox or a self-hosted gateway

Discover

TypeScript
const { results } = await agent.discover({
  query: "extract text from invoices",
  category: "OCR",           // optional filters
  protocol: "rest",
  maxPriceMinor: "200",      // a string, in cents
  maxLatencyMs: 3000,
  limit: 5,
});

for (const r of results) {
  console.log(r.capability.name, r.capability.priceMinor, r.provider.name, r.provider.verified, r.score);
}

Results are ranked, best first. Each has capability (including schema, the request body's contract), version (the address), provider and score.

Buy a REST capability

TypeScript
const result = await agent.invoke<{ text: string }>(best.version.endpoint, {
  body: { imageUrl: "https://example.com/invoice.png" },   // sent as JSON
  // method: "POST" (default), headers: {...}, signal: AbortSignal
});

result.data;                   // the seller's response, typed
result.receipt;                // the signed receipt
result.receiptVerified;        // Zanora signed it
result.responseHashVerified;   // …and it's for this exact response
result.transactionId;

invoke handles the whole handshake: it calls the endpoint, receives the 402 challenge, checks the challenge's signature, signs a proof, retries, and verifies the receipt.

Buying MCP capabilities

MCP capabilities need an MCP transport. It's injected, so the SDK itself depends only on @zanora/core:

Terminal
npm install @zanora/mcp
TypeScript
import { resolveCapabilityAddress } from "@zanora/core";
import { StreamableHttpMcpToolCaller } from "@zanora/mcp";

const agent = new ZanoraAgent({ /* …as above… */ mcpCaller: new StreamableHttpMcpToolCaller() });

const { results } = await agent.discover({ query: "invoice OCR", protocol: "mcp" });
const best = results[0]!;

const result = await agent.invokeMcp(resolveCapabilityAddress(best.version), {
  tool: best.capability.toolName!,
  arguments: { imageUrl: "https://example.com/invoice.png" },
});
result.mcpResult;   // the seller's MCP result, unmodified: content blocks, structuredContent, _meta

To buy across both protocols, branch on capability.protocol. Nothing else changes:

TypeScript
const r = best.capability.protocol === "mcp"
  ? await agent.invokeMcp(resolveCapabilityAddress(best.version), { tool: best.capability.toolName!, arguments: args })
  : await agent.invoke(best.version.endpoint, { body: args });

Use resolveCapabilityAddress, not endpoint, for MCP

Some MCP capabilities are packages the buyer runs rather than URLs it calls. resolveCapabilityAddress(version) returns the correct kind. Running packages needs its own opt-in. See Running seller packages.

Handling refusals

TypeScript
import { ZanoraPaymentError, ZanoraPolicyRejection } from "@zanora/sdk";

try {
  const r = await agent.invoke(url, { body });
} catch (err) {
  if (err instanceof ZanoraPolicyRejection) {
    // The gateway refused the payment. Nothing was charged.
    // err.code: "POLICY_DENIED" | "APPROVAL_REQUIRED" | "INSUFFICIENT_FUNDS" | "WALLET_FROZEN" | …
    // err.decision?.matchedRule, err.approvalId
  } else if (err instanceof ZanoraPaymentError) {
    // Your client refused before paying. Nothing was charged.
    // err.code: "PRICE_ABOVE_CLIENT_LIMIT" | "CANCELLED" | "INVALID_CHALLENGE" | "MCP_TRANSPORT_UNAVAILABLE"
  } else {
    // e.g. TypeError("fetch failed"): the seller's URL wasn't reachable. The call never got as far as a price.
    throw err;
  }
}

INVALID_CHALLENGE means the price quote wasn't signed by Zanora's key. Don't pay it.

A seller failure is not an exception. You get a result whose receipt.status is "failed", and the money has already been refunded. See Errors and refusals for every code.

Cancelling

Pass an AbortSignal. It works until the payment proof is sent. After that, the call completes on purpose: the money has moved and the seller is working, and dropping the connection would only lose an answer you paid for.

TypeScript
const ac = new AbortController();
setTimeout(() => ac.abort(), 2000);
await agent.invoke(url, { body, signal: ac.signal });
// → ZanoraPaymentError("CANCELLED"): nothing was charged. The only code that guarantees that.

After the purchase

TypeScript
await agent.rateProvider(best.provider.id, result.receipt!.transactionId, 5);  // 1–5, once per purchase
await agent.verifyReceipt(result.receipt!);   // re-check any receipt, including an old one
await agent.balance();                        // { balanceMinor, availableMinor, currency }

Rate what you buy. Discovery ranks on reputation, and only a wallet holding the receipt can rate.

ZanoraClient

ZanoraClient (same package) wraps the other API routes with types: wallets, policies, approvals, receipts. Use it for setup scripts. The full route list is in the API reference.