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
maxPriceMinor: 500n, // 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 |
maxPriceMinor | 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. Set it only for a sandbox or a self-hosted gateway |
Discover
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
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 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
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(); // { 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.