Sell
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.
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",
"priceMicros": "1000000",
"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": { … } }priceMicros is a string of micro-dollars ("1000000" = $1.00, "1000" = $0.001). Leave out providerId, because it comes from your key. Keep the returned cap_… id: the middleware needs it.
Or create it in the console. In the console↗, go to Seller → Publish and choose Protocol: REST endpoint:
Seller → Publish → Protocol: REST endpoint
Name Invoice OCR
Category OCR (pick a suggestion or type your own)
Endpoint URL https://acme.example/ocr
What it does Extract text and structured line items from scanned invoices
Price per call 1.00
Version 1.0.0
→ PublishThe new capability appears under Seller → Capabilities with its cap_… id. Copy that id into the middleware in the next step.
What the console form doesn't set
The form has no field for the request schema, tags or latency. Buyers' agents read the schema before paying, so add one through the API.
Add the middleware
npm install @zanora/middleware-express expressimport 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);Check the handshake
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_…","amountMicros":"1000000","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
| 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.
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:
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
| 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; priceMicros must be a string |