# 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.

```bash
npm install @zanora/sdk
```

## Create the agent

```ts
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](#buying-mcp-capabilities) |
| `gatewayUrl` | no | defaults to `https://api.zanora.dev`. Set it only for a sandbox or a self-hosted gateway |

## Discover

```ts
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

```ts
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`:

```bash
npm install @zanora/mcp
```

```ts
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:

```ts
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 });
```

> **Warning — 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](https://docs.zanora.dev/buyers/local-packages.md).

## Handling refusals

```ts
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](https://docs.zanora.dev/concepts/errors.md) 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.

```ts
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

```ts
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](https://docs.zanora.dev/api/overview.md).
