# How to check a quote against text you already hold, step by step with curl

Last updated: 2026-10-13

To check that a quote really appears in a text you already have, such as a file you read, a tool result or a document, send the text and the quote, word for word, to `POST /v1/verify/text-quote` (US$0.005 per call). Nothing is fetched. AttestPage searches the text you sent and answers `exact`, `fuzzy` or `none`, with a score, the offsets of the match and a signed receipt.

This guide runs the check with `curl` and `jq` against `https://vehcdj664efetfrsolne5umanq.srv.us`, then shows the same call as an MCP tool. To check a quote against a live web page instead, use [Verify a quote](/docs/verify-quote), which fetches the page.

## What the check does

- **Reads the text you send.** The text is not fetched, so there are no redirects, bot walls or DNS failures. The text is hashed as sent, and the receipt records that hash.
- **Matches the quote.** Both texts are normalised first (Unicode NFKC, plain quotes and dashes, one space for any whitespace). `exact` means the words are there after normalisation; `fuzzy` means a close match scored at or above `threshold` (default 0.9); `none` means neither.
- **Checks a slice if you ask.** Send `start` and `end` and only that part of the text is matched, so the answer is whether the text at those offsets says the quote.
- **Flags instructions in the text.** `injection_flags` lists instruction-like wording found in the text. Agents paste untrusted output, so read this list.
- **Signs a receipt.** An Ed25519 receipt records the hash of the text, the quote hash and the match. Anyone can check it later against our published keys.

## Price

US$0.005 per call, paid in USDC with x402 v2 on Base mainnet (real USDC). No account, key or sign-up. A request that fails validation gets 400 before payment and is never charged. All prices: [pricing.json](https://vehcdj664efetfrsolne5umanq.srv.us/pricing.json).

## 1. See an answer for free

The free sample runs the same route on a demo sentence:

```sh
curl -s https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/textquote | jq '{match: .result.match, score: .result.score, start: .result.start, end: .result.end, advice}'
```

```json
{
  "match": "fuzzy",
  "score": 0.963,
  "start": 43,
  "end": 70,
  "advice": "The supplied text contains a close match to this quote, with small differences. This says nothing about where the text came from."
}
```

## 2. Read the price from the 402 offer

A request with an empty 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/text-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}'
```

`amount` is in USDC base units (6 decimals), so `5000` is US$0.005.

## 3. Check a quote

Put the text and the quote, exactly as the LLM gave it, in a file:

```sh
cat > body.json <<'EOF'
{ "text": "Refunds are paid within 14 days of purchase, to the card that was used.", "quote": "refunds are paid within 14 days", "match": "fuzzy" }
EOF
```

`text` may be up to 50,000 characters and 64 KB of UTF-8. `quote` may be up to 1,000 characters. Do not send a `url` field: this route never fetches, so an unknown field gets 400.

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` for the steps below.

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

Read `result.match` first, then `injection_flags`:

| `match` | What it means |
|---|---|
| `exact` | The quoted words are in the text after normalisation. |
| `fuzzy` | A close match. Compare `result.context` with the quote: a word may have changed or two sentences may have been joined. |
| `none` | The quote is not in the text. Do not quote it from this source. |

`result.occurrences` says how many times the quote was found. `result.start` and `result.end` are offsets into the normalised text, not into what you sent, and `result.normalised_sha256` is the hash of that normalised text.

To check one part of the text, add `start` and `end` to the body. They are offsets into your text as sent, counted in characters (code points), with `end` not included, and the slice may be up to 1,000 characters. The answer then has `result.declared` with `slice_matches`, which is `true` when the text at those offsets says the quote. Offsets are raw; the result offsets from a search are normalised, so do not mix the two.

If the LLM paraphrased rather than quoted, send `"match": "passages"` with the claim as `quote`. You get the passages of the text that share most of its words, as evidence to read, not a verdict. Offsets cannot be used with `passages`.

```json
{
  "result": {
    "match": "exact", "score": 1, "start": 0, "end": 31,
    "context": "Refunds are paid within 14 days of purchase, to the card that was used.",
    "occurrences": 1, "normalised_sha256": "…",
    "source": { "kind": "supplied", "text_sha256": "…", "code_points": 71 }
  },
  "injection_flags": [],
  "advice": "The supplied text contains this quote. This says nothing about where the text came from.",
  "receipt": "eyJ…",
  "tier": "trial"
}
```

The `advice` line is a reminder, not a verdict on the source. The check says what the text you sent says; it does not say where the text came from.

## 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
```

Or check it offline with our single-file checker: [Verify receipts offline](/docs/verify-offline). The receipt attests the hash of the text and the match. It does not show that the text came from any particular site.

## With MCP

The same check is the MCP tool `attestpage_verify_text_quote`, 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" }
  }
}
```

Or run the local server, which pays from your own wallet key within limits you set: [MCP server](/docs/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_text_quote",
    "arguments": { "text": "Refunds are paid within 14 days of purchase, to the card that was used.", "quote": "refunds are paid within 14 days", "match": "fuzzy" },
    "_meta": { "attestpage/trial": false } } }
EOF
```

```json
{
  "scheme": "exact",
  "network": "eip155:8453",
  "amount": "5000"
}
```

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 one check cost?

US$0.005 per call with `POST /v1/verify/text-quote`. A check against a live web page costs more, because it fetches the page: [Verify a quote](/docs/verify-quote).

### Does an exact match mean the quoted claim is right?

No. It means those words are in the text you sent. Whether that text is right, or where it came from, is a separate question.

### Is a call charged when it fails?

Only answers below 400 are charged. A request that fails validation, such as an unknown field, a missing `quote`, or offsets that are out of range, is never charged. Every case is in [Payments](/docs/payments) and [Errors](/docs/errors).
