# Sell an HTTP endpoint

> Publish a REST capability, then add @zanora/middleware-express to the route. Unpaid calls get a 402 with a signed price, and your handler only runs once the call is paid for.

Selling an HTTP endpoint takes two steps. You **publish** a capability that says what the endpoint is and what it costs, then you put the **middleware** in front of that URL.

### Step 1: Publish the capability

```bash title="API"
curl -s -X POST https://api.zanora.dev/v1/capabilities -H "x-api-key: $ZANORA_API_KEY" \
  -H 'content-type: application/json' -d '{
    "name": "Invoice OCR",
    "description": "Extract text and structured line items from scanned invoices",
    "protocol": "rest",
    "priceMinor": "100",
    "category": "OCR",
    "endpoint": "https://acme.example/ocr",
    "tags": ["invoices", "ocr"],
    "schema": { "type": "object", "properties": { "imageUrl": { "type": "string" } }, "required": ["imageUrl"] },
    "latencyP50Ms": 900
  }'
# → { "capability": { "id": "cap_d2383f37…", … }, "version": { … } }
```

```text title="Console"
Seller → Publish → Protocol: REST → name, description, category, price, endpoint URL → Publish
```

`priceMinor` is a **string of cents**. Leave out `providerId`, because it comes from your key. Keep the returned `cap_…` id: the middleware needs it.

### Step 2: Add the middleware

```bash
npm install @zanora/middleware-express express
```

```ts title="server.ts"
import express from "express";
import { HttpBackend, zanora } from "@zanora/middleware-express";

const app = express().use(express.json());
const backend = new HttpBackend({ apiKey: process.env.ZANORA_API_KEY! });   // gateway defaults to https://api.zanora.dev

app.post("/ocr", zanora({ backend, capabilityId: "cap_d2383f37…" }), (req, res) => {
  // Only reached once the call is paid for.
  // req.zanora = { transactionId, walletId, agentId }
  res.json(runOcr(req.body.imageUrl));
});

app.listen(4024);
```

### Step 3: Check the handshake

```bash
curl -si -X POST https://acme.example/ocr -H 'content-type: application/json' -d '{"imageUrl":"x"}'
# HTTP/1.1 402 Payment Required
# {"error":{"code":"PAYMENT_REQUIRED","message":"payment required"},
#  "challenge":{"capabilityId":"cap_…","providerId":"prv_…","amountMinor":"100","currency":"USD","nonce":"…","expiresAt":"…","payTo":"wal_…","signature":"…","signingKeyId":"…"}}
```

A `402` with a signed challenge means you're ready. Once you're [verified](https://docs.zanora.dev/sellers/verification.md), buyers can find and pay for it.

## What the middleware does

| Request | Middleware does |
|---|---|
| no `x-payment` header | asks Zanora for a signed challenge and returns **402** with it |
| with a payment proof | asks Zanora to **authorize** it (signature, replay, the buyer's policy, budget and balance), then calls your handler |
| handler responds `2xx`/`3xx` | sends the response bytes to Zanora, gets a **signed receipt**, returns it to the buyer in `x-zanora-receipt` |
| handler **responds `4xx`** | tells Zanora the call failed as a `client_error`: the buyer is **refunded**, and it **doesn't** count against you on the circuit breaker |
| handler **throws or responds `5xx`** | tells Zanora the call failed as a `provider_error`: the buyer is **refunded**, and it counts toward pausing that buyer's wallet from you (three in a row) |

The receipt's `responseHash` is the hash of the exact bytes you sent, and buyers check it. Don't add anything that changes the body per response (like a timestamp) after the middleware has hashed it.

If your process dies mid-call, Zanora keeps the in-flight transaction. A `complete` that arrives after a restart still counts. If nothing ever completes it, the buyer is refunded after 15 minutes.

## Other frameworks and languages

`zanora()` is short, and most of it is calls to four gateway routes with your provider key (`payments:write`). To support FastAPI, Go, Rails or anything else, implement the same sequence. See [x402 protocol](https://docs.zanora.dev/api/x402.md).

## Restarts and reuse

The capability and your server are separate. Publishing again creates a **second** capability. On restart, reuse the id you stored, or look it up in your own listings:

```bash
curl -s https://api.zanora.dev/v1/capabilities -H "x-api-key: $ZANORA_API_KEY"
```

To change the price or wording without a new id, [edit the listing](https://docs.zanora.dev/sellers/listings.md).

## Common mistakes

| Symptom | Cause |
|---|---|
| buyers see `INVOKE_FAILED — fetch failed` | the `endpoint` isn't reachable from where the buyer's agent runs (a `localhost` URL, or a firewall). Zanora never calls you: the agent does |
| `402` forever, or `PROVIDER_NOT_FOUND` on complete | the middleware's `HttpBackend` points at a different gateway than the one the capability was published on |
| the endpoint serves calls for free | the route isn't wrapped in `zanora(...)`. Publishing alone doesn't charge anything |
| `VALIDATION_FAILED: protocol must be one of: rest, mcp` | `protocol` and `category` are required; `priceMinor` must be a string |
