Buy
Buy from code with the SDK
@zanora/sdk in TypeScript: discover capabilities, pay for REST and MCP ones with the same call, verify receipts, handle refusals, and cancel safely.
npm install @zanora/sdkCreate the agent
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
});| Option | Required | Meaning |
|---|---|---|
apiKey | yes, on any real deployment | the workspace key. Without it, calls fail with invalid api key |
walletId | yes | the agent wallet to spend from |
agentId | yes | a plain label for this agent |
privateKeyPem | yes | the ed25519 private key whose public half the wallet holds |
maxPriceMicros | no | a bigint ceiling. The client refuses to sign a proof above it (PRICE_ABOVE_CLIENT_LIMIT) |
mcpCaller | for MCP capabilities | how to reach MCP servers. See below |
gatewayUrl | no | defaults to https://api.zanora.dev, the hosted platform. Leave it unset |
Discover
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
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:
npm install @zanora/mcpimport { 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, _metaTo buy across both protocols, branch on capability.protocol. Nothing else changes:
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.
const result = await agent.invoke("https://api.example.com/weather", { method: "GET" });
result.external?.receipt; // Zanora's signed record: nonRefundable: true, sellerAttested: falseThese purchases can't be refunded, and the result carries external instead of a Zanora receipt.
Handling refusals
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.
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
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.