# How Zanora works

> Who is involved, what happens during one paid call, and where the money goes afterwards. The flow is the same over HTTP and MCP.

## Who is involved

| | Who they are | What they hold |
|---|---|---|
| **Seller** (a *provider*) | anyone with an HTTP endpoint or MCP tool worth paying for | a provider API key and a **settlement wallet** where earnings land |
| **Buyer** (a *workspace*) | an organisation whose agents spend money | a workspace API key, one **agent wallet** per agent, and the **spending policies** those wallets follow |
| **Agent** | the software doing the buying: a model in an MCP client, or your own code | its wallet's **private signing key**, which signs each payment |
| **Zanora** | the gateway at `api.zanora.dev` | the registry, the ledger, the policy engine and the signing keys for challenges and receipts |

Zanora never calls a seller. The **agent calls the seller directly**, and the seller's middleware checks with Zanora before running the handler. That's why a seller's URL has to be reachable from wherever buying agents run, not just from Zanora.

## One paid call

The protocol is [x402](https://docs.zanora.dev/api/x402.md), named after HTTP's `402 Payment Required`:

```text
 Agent                         Seller (middleware)                  Zanora gateway
   │  1. call, no payment  ───►  │                                     │
   │                             │  ── asks for a signed quote ──────► │
   │  ◄── 2. 402 + challenge ─── │  ◄─ price, nonce, expiry, signed ── │
   │                             │                                     │
   │  3. verify the challenge, sign a payment proof with the wallet key
   │                             │                                     │
   │  4. call again + proof ───► │  ── authorize(proof) ─────────────► │  signature, replay,
   │                             │                                     │  policy, budget, balance
   │                             │  ◄─ ok: money held ──────────────── │
   │                             │  5. your handler runs               │
   │                             │  ── complete(response) ───────────► │  receipt signed over
   │  ◄── 6. answer + receipt ── │  ◄─ signed receipt ──────────────── │  sha256(response)
```

1. The agent calls the seller's endpoint or tool as usual.
2. The seller's middleware returns a **challenge**: the price, a single-use nonce and an expiry, **signed by Zanora**. Zanora sets the price the seller published, so the seller's server can't quote a different one on the fly.
3. The agent checks the challenge signature against Zanora's public key and refuses a price above its own ceiling. Then it signs a **payment proof** with its wallet's ed25519 key.
4. The agent retries with the proof attached. The middleware asks Zanora to **authorize** it. Zanora checks the signature against the wallet's registered public key, rejects replays, applies the buyer's **policy** and **daily budget**, and checks the balance. Then it takes the money from the wallet and holds it.
5. The handler runs. It only runs if the payment was authorized.
6. Zanora signs a **receipt** over a hash of the handler's response, and the agent gets the answer and the receipt together.

With an SDK or MCP package, all of that is one function call or one tool call. You only handle it yourself if you [write your own middleware](https://docs.zanora.dev/api/x402.md).

## After the call

- **Settlement is asynchronous.** The receipt comes back with the answer. The split into the seller's net and the platform fee happens moments later, as one balanced ledger movement. With the default fee of 3%, a $1.00 call pays the seller $0.97. The split is exact to the cent: fee plus net always equals the price.
- **Failures are refunded automatically.** If the handler throws, responds with an HTTP error status (400 or above), or returns an MCP result with `isError`, the call fails. The buyer gets a `failed` receipt and the held money comes back as a new refund entry. Sellers are never paid for calls they didn't serve.
- **Abandoned calls are refunded too.** If a seller authorizes a call and never completes it, Zanora refunds the buyer after 15 minutes.
- **Repeated seller failures pause a seller for that wallet.** After three charged failures in a row that are the seller's fault (a 5xx, a thrown handler, an MCP `isError`, or a timeout) between one wallet and one seller, calls between them are refused before any money moves. A 4xx is refunded but doesn't count, because it means the buyer's request was bad, not that the seller is broken. The pause lifts on its own after a while, or the buyer can [lift it](https://docs.zanora.dev/api/wallets.md#post-v1-wallets-id-resume-provider).

## Where the money lives

Every movement is a pair of entries in an **append-only, double-entry ledger**. A balance is always calculated from the entries and is never stored as a separate number. Nothing is edited or deleted. A refund or correction is a new entry that offsets the old one. Your wallet's ledger (`GET /v1/wallets/:id/ledger`) is the full history of every amount that moved.

Money enters through a **funding rail**: a card or bank payment, or USDC on Base. It leaves through a **payout rail**: a bank transfer or USDC. Payments between buyers and sellers never touch a blockchain or a card network. They are ledger entries, which is why a call can cost a cent. See [Money and amounts](https://docs.zanora.dev/concepts/money.md).

## REST and MCP are the same protocol

A capability is either an **HTTP endpoint** (`protocol: "rest"`) or a **tool on an MCP server** (`protocol: "mcp"`). The x402 handshake, the ledger, the receipts and the refunds are identical for both. Only the transport differs:

| | HTTP | MCP |
|---|---|---|
| Price quote | `402` status + `{ challenge }` body | error result + `_meta["zanora.dev/x402-challenge"]` |
| Payment | `x-payment` header | `params._meta["zanora.dev/x402-payment"]` |
| Receipt | `x-zanora-receipt` header | `result._meta["zanora.dev/x402-receipt"]` |

The live demo seller sells one OCR function both ways, at the same price. Buy both and you'll get the same answer and the same `responseHash`.
