# How to check the citations at the end of an answer before you send it, step by step with curl

Last updated: 2026-10-13

To check up to 10 citations in one call, send each quote and the URL it is attributed to to `POST /v1/verify/citations` (US$0.10 per batch). AttestPage fetches each distinct page once, looks for each quote on its page, says whether the page loaded as real content, and returns one signed receipt for the whole batch.

This guide runs the call with `curl` and `jq` against `https://vehcdj664efetfrsolne5umanq.srv.us`, then shows the same call as an MCP tool. For every field and error, see [Verify citations](/docs/verify-citations).

## What the check does

- **Fetches each page once.** Several quotes on the same URL share one fetch, following redirects.
- **Matches each quote.** Both texts are normalised first. `exact` means the words are there; `fuzzy` means a close match at or above `threshold` (default 0.9); `none` means neither. With `"match": "passages"` each quote is a claim and you get the closest passages of the page to judge yourself.
- **Judges each page.** `content_kind` is `real` when the page loaded as content, or `bot_wall`, `js_shell`, `paywall`, `http_error` and others when it did not.
- **Adds paper data.** A page with a DOI also gets its title, authors, year and a retraction flag from Crossref.
- **Signs a receipt.** One Ed25519 receipt covers the pages, the options and each verdict. It holds the hash of each quote, not its text.

## Price

US$0.10 per batch of up to 10 citations, paid in USDC with x402 v2 on Base mainnet (real USDC). No account, key or sign-up. A request with a malformed URL, an empty quote or a page that robots.txt disallows gets an error before payment and is never charged. A batch where no host name resolves is a 422 and is not charged either. A dead domain in a batch that has other live pages is a result, and the batch is charged. For one to nine citations, one [verify/quote](/docs/verify-quote) call per pair costs less. All prices: [pricing.json](https://vehcdj664efetfrsolne5umanq.srv.us/pricing.json).

## 1. See an answer for free

The free sample checks three citations on the demo site: one close match, one quote that is not on the page, and one page that does not exist.

```sh
curl -s https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/citations | jq '.results[] | {url, match, score, kind: .content_kind}'
```

```json
{
  "url": "https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/page",
  "match": "fuzzy",
  "score": 0.963,
  "kind": "real"
}
{
  "url": "https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/page",
  "match": "none",
  "score": null,
  "kind": "real"
}
{
  "url": "https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/missing",
  "match": "none",
  "score": null,
  "kind": "http_error"
}
```

## 2. Read the price from the 402 offer

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

```sh
curl -si -X POST https://vehcdj664efetfrsolne5umanq.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" }, { "url": "https://www.iana.org/domains/reserved", "quote": "IANA-managed Reserved Domains" } ] }' \
  | 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 citations

Send `citations`, a list of 1 to 10 objects with a `url` and a `quote` (at most 1,000 characters after normalisation). Optional fields: `match` (`fuzzy` by default, `exact` or `passages`), `case_sensitive` and `threshold`. This body checks two pages on iana.org:

```sh
cat > body.json <<'EOF'
{ "citations": [
  { "url": "https://www.iana.org/help/example-domains", "quote": "These domains may be used as illustrative examples in documents" },
  { "url": "https://www.iana.org/domains/reserved", "quote": "IANA-managed Reserved Domains" }
] }
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](/docs/examples/fetch-x402) shows this with plain `fetch`, and the [MCP server](/docs/mcp) does it for you. Save the answer as `answer.json`.

## 4. Read the answer

```sh
jq '{summary, results: [.results[] | {match, score, kind: .content_kind, advice}]}' answer.json
```

`results` has one entry per citation, in request order. `summary` counts `exact`, `fuzzy` and `none` verdicts, and `failed` for citations that carry an `error` instead of a verdict.

| `match` | `content_kind` | What it means |
|---|---|---|
| `exact` or `fuzzy` | `real` | The quote, or a close version of it, is on the page. `start`, `end` and `context` show where. |
| `none` | `real` | The page loaded and the quote is not on it. Check the quote before you cite it. |
| `none` | anything else | We could not read the page (404, bot wall, JavaScript shell), so we cannot say whether the quote is on it. Read `advice`. |

A citation whose quote is too costly to match fuzzily gets an `error` with code `match_too_costly` on that citation only; the rest are still checked. Use `"match": "exact"` or a shorter quote.

## 5. Keep the receipt

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

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

## With MCP

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

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

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:

```sh
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_citations",
    "arguments": { "citations": [
      { "url": "https://www.iana.org/help/example-domains", "quote": "These domains may be used as illustrative examples in documents" },
      { "url": "https://www.iana.org/domains/reserved", "quote": "IANA-managed Reserved Domains" }
    ] },
    "_meta": { "attestpage/trial": false } } }
EOF
```

```json
{
  "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`.

## Questions

### How much does it cost to check citations?

US$0.10 per batch of up to 10 citations. Ten citations in one call cost the same as one.

### Is a batch charged if a page is dead?

Yes, when other pages in the batch resolve: a page that is gone or behind a bot wall is a result, and the batch is charged. If no host name in the batch resolves, the call is a 422 and is not charged. If a page turns out to be disallowed by robots.txt after payment, the whole batch is answered with an error and not charged.

### Does `none` mean the quote is not on the page?

Only when `content_kind` is `real`. On a 404, bot wall or JavaScript shell we could not read the page, so `none` says nothing about the quote.
