Demo: one paid call and its signed receipt
Last updated: 2026-10-13
One call to POST /v1/verify/quote, step by step: the request, the 402 payment offer (US$0.01 in USDC), the paid retry with x402, the answer with its signed receipt, and how to check the receipt's signature yourself with nothing but Node.js.
The example receipt is signed with an example key, published in step 5, so every check on this page works offline. The live POST /v1/receipt/verify route answers unknown_kid for this example, because the example key is not one of ours; real receipts verify there. Receipts from https://s2h6qt73jtzlgbgnnfacln2poq.srv.us are signed with the keys at https://s2h6qt73jtzlgbgnnfacln2poq.srv.us/.well-known/jwks.json and are checked the same way.
1. The request
An agent wants to quote a page and sends the URL and its exact words:
POST /v1/verify/quote HTTP/1.1
Host: attestpage.example
Content-Type: application/json
{
"url": "https://www.iana.org/help/example-domains",
"quote": "These domains may be used as illustrative examples in documents"
}
2. The 402 offer
No payment came with the request, so the answer is 402 Payment Required and nothing is charged. The PAYMENT-REQUIRED header is base64 JSON:
HTTP/1.1 402 Payment Required
PAYMENT-REQUIRED: eyJ4NDAyVmVyc2lvbiI6Miwi…
{
"error": {
"code": "payment_required",
"message": "This route costs US$0.01 via x402 (see the PAYMENT-REQUIRED header and /pricing.json; send the signed payment in the PAYMENT-SIGNATURE request header). Or send the header quoteproof-trial: 1 for a free trial (10 calls a day per IP address, resetting at midnight UTC).",
"details": {
"trial": {
"header": "quoteproof-trial",
"value": "1",
"per_day": 10,
"max_batch_items": 5,
"how": "Resend this request with the header quoteproof-trial: 1 and no payment header. Free, up to 10 calls a UTC day per caller (per IP address; on the srv.us direct port, per client header set), 50 a day across all direct-port callers with no payment id, and 200 across all callers; callers that reach the service through one shared address (a proxy or tunnel) share its 10 calls. Over that, 429 trial_exhausted."
}
}
}
}
Decoded, without the route description and the extensions:
{
"x402Version": 2,
"error": "Payment required",
"resource": {
"url": "https://attestpage.example/v1/verify/quote"
},
"accepts": [
{
"scheme": "exact",
"network": "eip155:84532",
"amount": "10000",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"payTo": "0x70C796f8671B1F9f13447872d357DE1144171302",
"maxTimeoutSeconds": 120,
"extra": {
"name": "USD Coin",
"version": "2"
}
}
]
}
amount is in USDC base units (6 decimals), so 10000 is US$0.01, paid in USDC with x402 v2 on Base Sepolia (a test network). Prices for every route: pricing.json.
3. The paid retry
The agent's x402 client signs a USDC transfer authorization for exactly that amount and sends the same request again with a PAYMENT-SIGNATURE header. Decoded, without the extensions and with the signature shortened:
{
"x402Version": 2,
"resource": {
"url": "https://attestpage.example/v1/verify/quote"
},
"accepted": {
"scheme": "exact",
"network": "eip155:84532",
"amount": "10000",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"payTo": "0x70C796f8671B1F9f13447872d357DE1144171302",
"maxTimeoutSeconds": 120,
"extra": {
"name": "USD Coin",
"version": "2"
}
},
"payload": {
"authorization": {
"from": "0xD61A51C2696dd5c9beFd8cA42F237eD5E9e9f337",
"to": "0x70C796f8671B1F9f13447872d357DE1144171302",
"value": "10000",
"validAfter": "0",
"validBefore": "1791786120",
"nonce": "0x5d415fb63d57b07b64cfe13feb5dd3319e68fed87adb0320c5ec415406f1cf24"
},
"signature": "0x5b696e2189…221b"
}
}
Any x402 v2 client does this step. One x402 payment step by step shows it with plain fetch, and the MCP server does it for you.
4. The answer and its receipt
The check runs and the payment is settled: 200 OK, with a PAYMENT-RESPONSE header. Decoded, without the transaction hash and the extensions:
{
"success": true,
"payer": "0xD61A51C2696dd5c9beFd8cA42F237eD5E9e9f337",
"network": "eip155:84532",
"amount": "10000"
}
The body:
{
"result": {
"match": "exact",
"score": 1,
"start": 171,
"end": 234,
"context": "...e.com and example.org are maintained for documentation purposes. These domains may be used as illustrative examples in documents without prior coordination with us. They are not available for re...",
"occurrences": 1,
"normalised_sha256": "75a93b6d9b17a11bb2277dde1de0e57ac357967052d4ae3e4d660856ff22e81d"
},
"page": {
"requested_url": "https://www.iana.org/help/example-domains",
"final_url": "https://www.iana.org/help/example-domains",
"http_status": 200,
"content_type": "text/html",
"content_kind": "real",
"signals": [
"status_200"
],
"title": "Example Domains",
"redirects": [],
"content_sha256": "6a7cca043c62d5d13f7d4c6f2eba2839c6446c06b5c7be6cb29d77ad6ab80e46",
"raw_sha256": "eeb94115fe65c771f01a0cc0701cded276a75cab3c0bb71cbf34daf802ce4a44",
"retrieved_at": "2026-10-12T06:20:02.607Z",
"server_ip": "104.18.24.232",
"tls": {
"cert_sha256": "cbf3a200a42e2b6b751fb6beb6d1b72a464a0ab7ffebfca0fff071af2c185b64",
"issuer": "C=US, O=Google Trust Services, CN=WE1",
"subject": "CN=www.iana.org",
"valid_from": "2026-08-21T20:51:32.000Z",
"valid_to": "2026-11-19T21:51:30.000Z"
},
"headers": {
"date": "Mon, 12 Oct 2026 06:20:02 GMT",
"last-modified": "Mon, 12 Oct 2026 06:20:02 GMT",
"content-type": "text/html; charset=utf-8",
"content-length": "1762"
},
"bytes": 1762,
"truncated": false
},
"injection_flags": [],
"advice": "The page loaded and looks like real content.",
"receipt": "eyJhbGciOiJFZERTQSIsImtpZCI6Imh0dHBzOi8vYXR0ZXN0cGFnZS5leGFtcGxlLy53ZWxsLWtub3duL2p3a3MuanNvbiNhcC1OeWl5c0NBSTR1Z3BEZlJPIiwidHlwIjoiYXR0ZXN0cGFnZS1ldmlkZW5jZStqd3MifQ.eyJjb250ZW50X2tpbmQiOiJyZWFsIiwiY29udGVudF9zaGEyNTYiOiI2YTdjY2EwNDNjNjJkNWQxM2Y3ZDRjNmYyZWJhMjgzOWM2NDQ2YzA2YjVjN2JlNmNiMjlkNzdhZDZhYjgwZTQ2IiwiZmluYWxfdXJsIjoiaHR0cHM6Ly93d3cuaWFuYS5vcmcvaGVscC9leGFtcGxlLWRvbWFpbnMiLCJoZWFkZXJzIjp7ImNvbnRlbnQtbGVuZ3RoIjoiMTc2MiIsImNvbnRlbnQtdHlwZSI6InRleHQvaHRtbDsgY2hhcnNldD11dGYtOCIsImRhdGUiOiJNb24sIDEyIE9jdCAyMDI2IDA2OjIwOjAyIEdNVCIsImxhc3QtbW9kaWZpZWQiOiJNb24sIDEyIE9jdCAyMDI2IDA2OjIwOjAyIEdNVCJ9LCJodHRwX3N0YXR1cyI6MjAwLCJpYXQiOjE3OTE3ODYwMDIsImlzcyI6Imh0dHBzOi8vYXR0ZXN0cGFnZS5leGFtcGxlIiwia2luZCI6InF1b3RlIiwicGF5bWVudCI6eyJhbW91bnQiOiIxMDAwMCIsImFzc2V0IjoiMHg4MzM1ODlmQ0Q2ZURiNkUwOGY0YzdDMzJENGY3MWI1NGJkQTAyOTEzIiwibmV0d29yayI6ImVpcDE1NTo4NDUzIiwicGF5ZXIiOiIweEQ2MUE1MUMyNjk2ZGQ1YzliZUZkOGNBNDJGMjM3ZUQ1RTllOWYzMzciLCJyYWlsIjoieDQwMiJ9LCJxdW90ZV9zaGEyNTYiOiJkZjM0YmUwZmNlYWI1NmYyNjk2ZGQwODE2OTRiNWM5MjRjYmRkYTUyMDY0N2ExOGNlYmMwMWY1ZDdlMjIzNjRmIiwicmF3X3NoYTI1NiI6ImVlYjk0MTE1ZmU2NWM3NzFmMDFhMGNjMDcwMWNkZWQyNzZhNzVjYWIzYzBiYjcxY2JmMzRkYWY4MDJjZTRhNDQiLCJyZXF1ZXN0X3NoYTI1NiI6IjkxMzU3YjcwMTRjMTVkNzJiMjE4ZWQyYTU1ZjdiMzhlODU2ZGYyNmYxOTg3ZWI2OGVhODkyNWE4OTE2YjkxZWEiLCJyZXN1bHQiOnsiY2FzZV9zZW5zaXRpdmUiOmZhbHNlLCJlbmQiOjIzNCwibWF0Y2giOiJleGFjdCIsIm1vZGUiOiJmdXp6eSIsIm5vcm1hbGlzZWRfc2hhMjU2IjoiNzVhOTNiNmQ5YjE3YTExYmIyMjc3ZGRlMWRlMGU1N2FjMzU3OTY3MDUyZDRhZTNlNGQ2NjA4NTZmZjIyZTgxZCIsIm9jY3VycmVuY2VzIjoxLCJzY29yZSI6MSwic3RhcnQiOjE3MX0sInJldHJpZXZlZF9hdCI6IjIwMjYtMTAtMTJUMDY6MjA6MDIuNjA3WiIsInNlcnZlcl9pcCI6IjEwNC4xOC4yNC4yMzIiLCJ0aWVyIjoicGFpZCIsInRscyI6eyJjZXJ0X3NoYTI1NiI6ImNiZjNhMjAwYTQyZTJiNmI3NTFmYjZiZWI2ZDFiNzJhNDY0YTBhYjdmZmViZmNhMGZmZjA3MWFmMmMxODViNjQiLCJpc3N1ZXIiOiJDPVVTLCBPPUdvb2dsZSBUcnVzdCBTZXJ2aWNlcywgQ049V0UxIiwic3ViamVjdCI6IkNOPXd3dy5pYW5hLm9yZyIsInZhbGlkX2Zyb20iOiIyMDI2LTA4LTIxVDIwOjUxOjMyLjAwMFoiLCJ2YWxpZF90byI6IjIwMjYtMTEtMTlUMjE6NTE6MzAuMDAwWiJ9LCJ1cmwiOiJodHRwczovL3d3dy5pYW5hLm9yZy9oZWxwL2V4YW1wbGUtZG9tYWlucyIsInYiOjF9.WBCdG3EoDUBKYbDbc94_ouDyHDzoynbeZL1h6nlYkO5c-ktgWXDvKQOMz-PUSsicwSn8ZSyo3XwpatZAhDLZCg",
"tier": "paid",
"rail": "x402",
"network": "eip155:84532"
}
result.match is exact: the quoted words are on the page, at characters 171 to 234 of its text. page.content_kind is real: the page loaded as content, not a bot wall or an error page. receipt is a compact JWS (header.payload.signature, each base64url) that signs the URL, the time, the page and quote hashes, the match and the payment. Field meanings: How it works.
5. Check the receipt signature
The receipt's header names the signing key:
{
"alg": "EdDSA",
"kid": "https://attestpage.example/.well-known/jwks.json#ap-NyiysCAI4ugpDfRO",
"typ": "attestpage-evidence+jws"
}
The key is published in the issuer's JWKS at /.well-known/jwks.json. For this example it is:
{
"keys": [
{
"kty": "OKP",
"crv": "Ed25519",
"x": "3vsTrl_8AE6RgHHi2nMAKvCDIXukag2fBhRZsM5mWQo",
"kid": "https://attestpage.example/.well-known/jwks.json#ap-NyiysCAI4ugpDfRO",
"alg": "EdDSA",
"use": "sig"
}
]
}
Save the receipt string as receipt.txt and the key set as jwks.json. This script, check.mjs, uses only Node.js built-ins. It finds the key whose kid matches the header and verifies the Ed25519 signature over header.payload:
import crypto from 'node:crypto';
import fs from 'node:fs';
const jws = fs.readFileSync(process.argv[2], 'utf8').trim();
const { keys } = JSON.parse(fs.readFileSync(process.argv[3], 'utf8'));
const [h, p, s] = jws.split('.');
const header = JSON.parse(Buffer.from(h, 'base64url'));
const jwk = keys.find((k) => k.kid === header.kid);
if (header.alg !== 'EdDSA' || !jwk) throw new Error('no matching key');
const key = crypto.createPublicKey({ key: { kty: jwk.kty, crv: jwk.crv, x: jwk.x }, format: 'jwk' });
const ok = crypto.verify(null, Buffer.from(`${h}.${p}`), key, Buffer.from(s, 'base64url'));
if (!ok) {
console.log('bad signature');
process.exit(1);
}
const claims = JSON.parse(Buffer.from(p, 'base64url'));
console.log('valid', claims.iss, claims.kind, claims.result.match, claims.tier);
node check.mjs receipt.txt jwks.json
valid https://attestpage.example quote exact paid
The last word is the receipt's tier: a paid receipt ends in paid, a receipt from a free-trial call in trial, and a free example call in sample.
Change one character in the middle of the receipt's payload (the part between the two dots) and it prints bad signature. Then check that iss is the issuer you expect: a valid signature from a key you did not choose means nothing.
Check your own receipts
For a receipt from https://s2h6qt73jtzlgbgnnfacln2poq.srv.us, use the issuer's keys:
curl -s https://s2h6qt73jtzlgbgnnfacln2poq.srv.us/.well-known/jwks.json > jwks.json
node check.mjs receipt.txt jwks.json
Or send it to the free route POST /v1/receipt/verify (Verify a receipt), or use our single-file checker, which also checks the issuer and the receipt fields: Verify receipts offline. To make the call yourself, step by step with curl: How to check a quote an LLM cites.
Paid routes at a glance
Every paid route answers 402 Payment Required to a request with no payment, and nothing is charged. The offer is in the PAYMENT-REQUIRED header. Payment settles only when the route answers below 400, so a request that fails validation is never charged. Each command below sends the unpaid request and prints the offer's network, amount and payee. The amount is in USDC base units: 10000 is US$0.01.
| Route | Price | Use it to |
|---|---|---|
POST /v1/verify/quote | US$0.01 | check one quote against its page |
POST /v1/verify/citations | US$0.10 | check up to 10 citations in one call |
POST /v1/verify/quotes | US$0.02 | check up to 20 quotes on one page |
POST /v1/verify/text-quote | US$0.005 | check a quote against text you already hold, nothing fetched |
POST /v1/verify/document | US$0.10 | check every quote in a document, per part of up to 10 pairs |
POST /v1/fetch | US$0.002 | read a page as clean text, with a signed receipt |
POST /v1/recheck | US$0.003 | see whether a page changed since an earlier receipt |
POST /v1/check/links | US$0.005 | check up to 10 links are live |
POST /v1/check/packages | US$0.005 | check up to 10 package names exist |
POST /v1/attest | US$0.002 | sign a SHA-256 hash with the time it was issued |
# verify/quote
curl -si -X POST https://s2h6qt73jtzlgbgnnfacln2poq.srv.us/v1/verify/quote -H 'content-type: application/json' -d '{}' \
| grep -i '^payment-required:' | cut -d' ' -f2 | tr -d '\r' | base64 -d | jq '.accepts[0] | {scheme, network, amount, payTo}'
# verify/citations
curl -si -X POST https://s2h6qt73jtzlgbgnnfacln2poq.srv.us/v1/verify/citations -H 'content-type: application/json' \
-d '{ "citations": [ { "url": "https://www.iana.org/help/example-domains", "quote": "These domains may be used as illustrative examples in documents" } ] }' \
| grep -i '^payment-required:' | cut -d' ' -f2 | tr -d '\r' | base64 -d | jq '.accepts[0] | {scheme, network, amount, payTo}'
# verify/quotes
curl -si -X POST https://s2h6qt73jtzlgbgnnfacln2poq.srv.us/v1/verify/quotes -H 'content-type: application/json' \
-d '{ "url": "https://www.iana.org/help/example-domains", "quotes": [ "These domains may be used as illustrative examples in documents", "without prior coordination with us" ] }' \
| grep -i '^payment-required:' | cut -d' ' -f2 | tr -d '\r' | base64 -d | jq '.accepts[0] | {scheme, network, amount, payTo}'
# verify/text-quote
curl -si -X POST https://s2h6qt73jtzlgbgnnfacln2poq.srv.us/v1/verify/text-quote -H 'content-type: application/json' \
-d '{ "text": "In its annual summary the agency said that emissions fell 12% in 2025.", "quote": "emissions fell 12% in 2025" }' \
| grep -i '^payment-required:' | cut -d' ' -f2 | tr -d '\r' | base64 -d | jq '.accepts[0] | {scheme, network, amount, payTo}'
# verify/document
curl -si -X POST https://s2h6qt73jtzlgbgnnfacln2poq.srv.us/v1/verify/document -H 'content-type: application/json' \
-d '{ "document": "WHO says \"Air pollution kills an estimated seven million people every year\" ([WHO](https://www.who.int/health-topics/air-pollution)).", "format": "markdown" }' \
| grep -i '^payment-required:' | cut -d' ' -f2 | tr -d '\r' | base64 -d | jq '.accepts[0] | {scheme, network, amount, payTo}'
# fetch
curl -si -X POST https://s2h6qt73jtzlgbgnnfacln2poq.srv.us/v1/fetch -H 'content-type: application/json' -d '{}' \
| grep -i '^payment-required:' | cut -d' ' -f2 | tr -d '\r' | base64 -d | jq '.accepts[0] | {scheme, network, amount, payTo}'
# recheck
curl -si -X POST https://s2h6qt73jtzlgbgnnfacln2poq.srv.us/v1/recheck -H 'content-type: application/json' \
-d '{ "url": "https://s2h6qt73jtzlgbgnnfacln2poq.srv.us/v1/sample/page", "content_sha256": "0e429607fdb16c005ef662414e758c53edb82e1cb24c3cbec239e4c1d6a7dfa0" }' \
| grep -i '^payment-required:' | cut -d' ' -f2 | tr -d '\r' | base64 -d | jq '.accepts[0] | {scheme, network, amount, payTo}'
# check/links
curl -si -X POST https://s2h6qt73jtzlgbgnnfacln2poq.srv.us/v1/check/links -H 'content-type: application/json' -d '{}' \
| grep -i '^payment-required:' | cut -d' ' -f2 | tr -d '\r' | base64 -d | jq '.accepts[0] | {scheme, network, amount, payTo}'
# check/packages
curl -si -X POST https://s2h6qt73jtzlgbgnnfacln2poq.srv.us/v1/check/packages -H 'content-type: application/json' \
-d '{ "packages": [{ "ecosystem": "npm", "name": "left-pad" }, { "ecosystem": "pypi", "name": "requests" }] }' \
| grep -i '^payment-required:' | cut -d' ' -f2 | tr -d '\r' | base64 -d | jq '.accepts[0] | {scheme, network, amount, payTo}'
# attest
curl -si -X POST https://s2h6qt73jtzlgbgnnfacln2poq.srv.us/v1/attest -H 'content-type: application/json' -d '{}' \
| grep -i '^payment-required:' | cut -d' ' -f2 | tr -d '\r' | base64 -d | jq '.accepts[0] | {scheme, network, amount, payTo}'
To pay on the call, see Fetch a page with an x402 client. Prices for every route: pricing.json.