ZZanoraDocs

Sell

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.

1

Publish the capability

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": { … } }

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

2

Add the middleware

Terminal
npm install @zanora/middleware-express express
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);
3

Check the handshake

Terminal
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, buyers can find and pay for it.

What the middleware does

RequestMiddleware does
no x-payment headerasks Zanora for a signed challenge and returns 402 with it
with a payment proofasks Zanora to authorize it (signature, replay, the buyer's policy, budget and balance), then calls your handler
handler responds 2xx/3xxsends the response bytes to Zanora, gets a signed receipt, returns it to the buyer in x-zanora-receipt
handler responds 4xxtells 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 5xxtells 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.

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:

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

Common mistakes

SymptomCause
buyers see INVOKE_FAILED — fetch failedthe 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 completethe middleware's HttpBackend points at a different gateway than the one the capability was published on
the endpoint serves calls for freethe route isn't wrapped in zanora(...). Publishing alone doesn't charge anything
VALIDATION_FAILED: protocol must be one of: rest, mcpprotocol and category are required; priceMinor must be a string