ZanoraBetaDocs

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
  maxPriceMicros: 5_000_000n,                                     // 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
maxPriceMicrosnoa 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, the hosted platform. Leave it unset

Discover

TypeScript
const { results } = await agent.discover({
  query: "extract text from invoices",
  category: "OCR",           // optional filters
  protocol: "rest",
  maxPriceMicros: "2000000",      // a string, in micro-dollars: $2.00
  maxLatencyMs: 3000,
  limit: 5,
});

for (const r of results) {
  console.log(r.capability.name, r.capability.priceMicros, 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 listed as packages rather than URLs. resolveCapabilityAddress(version) returns the correct kind. Packages aren't payable yet: discovery marks them payable: false, and invokeMcp refuses one with NOT_SUPPORTED before running anything. See Seller packages.

Sellers outside Zanora (preview)

An x402 seller outside the Zanora registry answers with a standard x402 offer, not a Zanora challenge. By default invoke refuses it with OUTSIDE_SELLER. Create the agent with outsideSellers: true (and an apiKey) to pay from the wallet instead, if your workspace allows it. This isn't enabled on api.zanora.dev yet. See Paying x402 sellers outside Zanora.

TypeScript
const result = await agent.invoke("https://api.example.com/weather", { method: "GET" });
result.external?.receipt; // Zanora's signed record: nonRefundable: true, sellerAttested: false

These purchases can't be refunded, and the result carries external instead of a Zanora receipt.

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();                        // { balanceMicros, availableMicros, 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.