AttestPage · Docs · Pricing · OpenAPI · llms.txt

How to check every quote in a Markdown document from a CI job before merge, step by step with curl

Last updated: 2026-10-13

To check every quote in a Markdown file from a CI job before it merges, send the file to POST /v1/verify/document, one part of up to 10 quote and link pairs per call, at US$0.10 a part. The job fails when a quote is not on the page it cites. A page that cannot be read (a bot wall, a JavaScript shell, a paywall or an error) is reported as a warning, because it is unchecked, not wrong.

This guide runs the check with curl and jq against https://vehcdj664efetfrsolne5umanq.srv.us. For checking a draft by hand, see How to check every quote in a draft before publishing.

What the job does

Price

US$0.10 per part of up to 10 pairs, paid in USDC with x402 v2 on Base mainnet (real USDC). A file with 10 quotes or fewer is one part, so one run costs US$0.10. Every run that checks a part is charged, so run the job only when Markdown changes, for example with a path filter on your docs folder. A request that fails validation gets 400 before payment and is never charged. All prices: pricing.json.

1. Preview the pairs for free

The preview is free and changes nothing. It gives the number of parts and the exact price for the file. Put the draft in a file:

cat > body.json <<'EOF'
{ "document": "\"These domains may be used as illustrative examples in documents\" ([IANA](https://www.iana.org/help/example-domains)).", "format": "markdown" }
EOF
curl -s -X POST https://vehcdj664efetfrsolne5umanq.srv.us/v1/verify/document/preview -H 'content-type: application/json' -d @body.json \
  | jq '{pairs_total, parts, price: .price.usd_total, skipped_counts, pairs: [.pairs[] | {quote, url}]}'
{
  "pairs_total": 1,
  "parts": 1,
  "price": "0.10",
  "skipped_counts": {},
  "pairs": [
    {
      "quote": "These domains may be used as illustrative examples in documents",
      "url": "https://www.iana.org/help/example-domains"
    }
  ]
}

skipped_counts lists what was not checked and why: no_url, bad_url, too_short, too_long or beyond_limit. Read it before you trust a green job.

2. Read the price from the 402 offer

A request with a valid body gets the payment offer without being charged. The PAYMENT-REQUIRED header is base64 JSON:

curl -si -X POST https://vehcdj664efetfrsolne5umanq.srv.us/v1/verify/document -H 'content-type: application/json' -d @body.json \
  | grep -i '^payment-required:' | cut -d' ' -f2 | tr -d '\r' | base64 -d \
  | jq '.accepts[0] | {scheme, network, amount, payTo}'

amount is in USDC base units (6 decimals), so 100000 is US$0.10.

3. Check the part

Write the part number into the body. Part 0 is the first 10 pairs:

cat > body.json <<'EOF'
{ "document": "\"These domains may be used as illustrative examples in documents\" ([IANA](https://www.iana.org/help/example-domains)).", "format": "markdown", "part": 0 }
EOF

Pay with any x402 v2 client: it reads the offer from step 2, signs it and sends the same request again with a PAYMENT-SIGNATURE header. One x402 payment step by step shows this with plain fetch. Save the paid answer as answer.json.

A free trial is on: send the header quoteproof-trial: 1 with a paid route and no payment, and the call runs without charge, 5 calls per IP address per UTC day and 200 a day across all callers. Trial receipts have tier "trial" and payment null. Over the limit you get 429 trial_exhausted with Retry-After. A trial call to verify/citations, verify/document, verify/quotes, check/links or fetch with urls covers at most 3 citations, document pairs, quotes or URLs; a larger batch is not run as a trial but answered with the normal 402 offer, details.reason trial_too_large.

4. Read the answer

Start with summary, then the results that count against the build:

jq '.summary' answer.json
jq '.results[] | {quote, url, content_kind, match, score}' answer.json
content_kindmatchWhat the job does
realexactPasses. The quoted words are on the page.
realfuzzyPasses. A close match: compare context with the quote, since a word may have changed.
realnoneFails the job. The quote is not on the page as fetched.
any value other than realanyWarns. The quote is unchecked, not false.
realnull with an errorWarns. Unchecked: the quote was too costly to match; retry with match exact.

Each result gives doc_start and doc_end, the position of the quote in the file, so the job can print where to look.

5. Keep the receipt

The receipt field is a compact JWS. Check it with the free verify route:

jq '{receipt}' answer.json \
  | curl -s -X POST https://vehcdj664efetfrsolne5umanq.srv.us/v1/receipt/verify -H 'content-type: application/json' -d @- | jq .valid

Or check it offline with our single-file checker: Verify receipts offline.

6. Run the check in CI

The job runs one script after checkout. The script does the free preview with curl, then one paid call for each part. A paid call needs an x402 v2 client, because curl cannot sign the payment. Keep the wallet key in your CI secrets, and fund the wallet with USDC on Base mainnet (real USDC).

PAY_CMD is that client: a command that reads the JSON body on stdin, posts it to https://vehcdj664efetfrsolne5umanq.srv.us/v1/verify/document, pays the 402 offer and prints the 200 answer. It exits non-zero on any other status. One x402 payment step by step shows the payment with plain fetch.

Save this as check-quotes.sh in your repository:

#!/bin/sh
# Check every quote in one Markdown file. Usage: sh check-quotes.sh docs/report.md
set -eu
file=$1
rm -f preview.json
status=$(jq -n --rawfile d "$file" '{document: $d, format: "markdown"}' \
  | curl -s -o preview.json -w '%{http_code}' -X POST https://vehcdj664efetfrsolne5umanq.srv.us/v1/verify/document/preview -H 'content-type: application/json' -d @- || true)
if [ "$status" != 200 ]; then
  echo "preview failed for $file: HTTP $status $(jq -r '.error.message // empty' preview.json 2>/dev/null)" >&2
  exit 1
fi
parts=$(jq .parts preview.json)
case "$parts" in ''|*[!0-9]*) echo "preview failed for $file" >&2; exit 1;; esac
i=0
while [ "$i" -lt "$parts" ]; do
  jq -n --rawfile d "$file" --argjson part "$i" '{document: $d, format: "markdown", part: $part}' \
    | $PAY_CMD > "answer-$i.json"
  if ! jq -e '[.results[] | select(.content_kind == "real" and .match == "none")] | length == 0' "answer-$i.json" > /dev/null; then
    echo "part $i: quote not found on its page:"
    jq -r '.results[] | select(.content_kind == "real" and .match == "none") | "  \(.url)  \(.quote)"' "answer-$i.json"
    exit 1
  fi
  jq -r '.results[] | select(.content_kind != "real") | "warning: \(.content_kind) \(.url)"' "answer-$i.json"
  jq -r '.results[] | select(.error != null) | "warning: unchecked \(.url) \(.error.code)"' "answer-$i.json"
  i=$((i + 1))
done
echo "all quotes in $file are on their pages"

The job sets PAY_CMD to your client, for example PAY_CMD="node pay-post.mjs", and runs sh check-quotes.sh docs/report.md. It fails with the quote and URL of each miss, and prints a warning for each page that could not be read. The answers stay in the working folder as answer-0.json, answer-1.json and so on, so save them as build artifacts. The preview is kept as preview.json beside them. If the preview fails, the script prints the error message from that response and stops.

With MCP

The same check is the MCP tool attestpage_verify_document, with the body from step 3 as its arguments. To use it from an MCP client with nothing to install, add the remote endpoint:

{
  "mcpServers": {
    "attestpage": { "type": "http", "url": "https://vehcdj664efetfrsolne5umanq.srv.us/mcp" }
  }
}

Or run the local server, which pays from your own wallet key within limits you set: MCP server.

One call to the remote endpoint with curl. _meta["attestpage/trial"] set to false asks for the payment offer rather than a trial call, so nothing is charged:

curl -s -X POST https://vehcdj664efetfrsolne5umanq.srv.us/mcp -H 'content-type: application/json' -d @- <<'EOF' \
  | jq '.result.structuredContent.accepts[0] | {scheme, network, amount}'
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call",
  "params": { "name": "attestpage_verify_document",
    "arguments": { "document": "\"These domains may be used as illustrative examples in documents\" ([IANA](https://www.iana.org/help/example-domains)).", "format": "markdown", "part": 0 },
    "_meta": { "attestpage/trial": false } } }
EOF
{
  "scheme": "exact",
  "network": "eip155:8453",
  "amount": "100000"
}

The result has isError: true and the x402 payment requirements in structuredContent, with the same amount as step 2. Sign one of accepts, put the payload in _meta["x402/payment"] and call again: structuredContent is then the same JSON as answer.json, and _meta["x402/payment-response"] holds the settlement.

Questions

How much does a CI run cost?

US$0.10 for each part of up to 10 pairs. A file with 23 quotes is 3 parts, so 3 calls. The preview in step 1 shows the count and the total before anything is paid. Path filters keep runs to the commits that change Markdown.

Does a page that will not load fail the job?

No. A page that returns a 404, a bot wall or a timeout is still a result, and the part is charged. content_kind says what it was, and the script prints a warning. Only a quote that is missing from a page that loaded fails the job. A part where no URL resolves, or where every quote was too costly to match, gets 422 and the job stops; fix or remove those links.

The preview answers parts: 0 and the script passes without a paid call. To fail instead, add [ "$parts" -gt 0 ] || exit 1. To check every quote, also fail when the preview's skipped_counts.beyond_limit is set.

Does a pass mean the claims in the file are right?

No. It means those words were on the page as fetched at that time. Whether the page is right is a separate question.

What if the file is longer than 60,000 bytes?

The call is a 400 for a document over 60,000 bytes (UTF-8). Split the file by section and check each piece. Up to 100 pairs are checked per document. Any pair after the first 100 is counted in the preview's skipped counts as beyond_limit and is not checked. The paid answer does not list skipped pairs, so read the preview's skipped counts and fail the job yourself if you need every quote checked.

What does the receipt show?

It is an Ed25519 signature from the service over the document's SHA-256, the options, the pages as fetched (with the hash of each page's content) and each pair's verdict with a hash of its quote. It does not hold the quote text, so keep the file with the receipt. Check it with step 5.