# AttestPage > Signed evidence about web pages for AI agents, paid per call with x402. Check whether a quote appears on a page or check all the citations in an answer at once, fetch a page with a verdict on what came back, check links, check that package names exist on npm, PyPI or crates.io, or timestamp a hash. Every answer carries an Ed25519-signed receipt you can check offline. A receipt records what our fetcher saw at a given time; it does not show that a statement is correct. Prices are per call in US dollars, paid in USDC with x402 v2 on Base Sepolia (a test network). Bad requests are refused before payment and never charged. Every page below is also served as HTML at the same URL without `.md`. 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/quotes or check/links covers at most 3 citations, quotes or URLs; a larger batch is not run as a trial but answered with the normal 402 offer, `details.reason` trial_too_large. ## Paid endpoints - [POST /v1/verify/quote](https://vehcdj664efetfrsolne5umanq.srv.us/docs/verify-quote.md): US$0.01. Use when you are about to quote or cite a page. Checks your exact words against the page and returns offsets and page hashes, signed, in one call. Does this quote appear on this page? exact, fuzzy or none, with offsets, context, page verdict and receipt; or, for a paraphrased claim, the 3 passages that share most of its words (evidence to judge, not a verdict). For 9 or more quotes on one page, verify/quotes costs less. - [POST /v1/verify/quotes](https://vehcdj664efetfrsolne5umanq.srv.us/docs/verify-quotes.md): US$0.08 per batch of up to 20 quotes on one page. Use when you are about to quote one page several times. The verify/quote verdict for each quote, the page fetched once, under one receipt; one price for 1 to 20 quotes, so from 9 quotes up it costs less than one verify/quote call each. A call where every quote fails answers 422, not charged, under the same per-IP limit as verify/citations. - [POST /v1/verify/citations](https://vehcdj664efetfrsolne5umanq.srv.us/docs/verify-citations.md): US$0.10 per batch of up to 10 quote and URL pairs. Use before giving an answer that cites web pages. The verify/quote verdict for each citation and the verdict on each page, each page fetched once, under one receipt. For 1 to 9 pairs, one verify/quote call per pair costs less. A batch where every citation fails answers 422, not charged; each IP address gets 6 of those a minute, then 429 `rate_limited` (`details.reason` `uncharged_failures`) with `Retry-After`. - [POST /v1/verify/document](https://vehcdj664efetfrsolne5umanq.srv.us/docs/verify-document.md): US$0.10 per part of up to 10 quote and link pairs. Use before sending a finished answer or report that quotes web pages. Send it as written; its quotes and their links are found and checked on the live pages, with offsets in your document and on each page. For a part of 1 to 9 pairs, one verify/quote call per pair costs less. Free preview at POST /v1/verify/document/preview (pairs, doc_spans/doc_spans_truncated for repeats, skipped with skipped_truncated and skipped_counts by reason, parts, price). - [POST /v1/fetch](https://vehcdj664efetfrsolne5umanq.srv.us/docs/fetch.md): US$0.002. Use when you need a page as clean text and must know what came back. Fetch a URL and get content_kind (real, bot_wall, js_shell, paywall, http_error and others), status, final URL, title, hashes, injection flags and optional clean text. - [POST /v1/check/links](https://vehcdj664efetfrsolne5umanq.srv.us/docs/check-links.md): US$0.005 per batch of up to 10 URLs. Use before giving a user links or citations. Status, redirects, final URL, content_kind and hash per URL. - [POST /v1/check/packages](https://vehcdj664efetfrsolne5umanq.srv.us/docs/check-packages.md): US$0.005 per batch of up to 10 package names on npm, PyPI or crates.io. Use before installing or recommending a package an LLM suggested. Whether the registry lists each name, latest version, description, licence, links and flags. Says whether the registry lists the name, nothing more: not a security verdict. - [POST /v1/attest](https://vehcdj664efetfrsolne5umanq.srv.us/docs/attest.md): US$0.002. Use when you need to show later that a file, answer or record existed unchanged. Signed, timestamped receipt over a SHA-256 you supply. ## Free - [POST /v1/receipt/verify](https://vehcdj664efetfrsolne5umanq.srv.us/docs/receipt-verify.md): check a receipt against our published keys. - [GET /v1/sample/{quote,citations,document,fetch,links,packages,attest,quotes}](https://vehcdj664efetfrsolne5umanq.srv.us/docs/samples.md): each paid route run on fixed demo data, signed with the sample key. Try these first; `/v1/sample/quote?match=passages` shows match "passages" on a paraphrased claim. In openapi.json each paid operation names its sample in `x-402.sample`. - [Receipt demo](https://vehcdj664efetfrsolne5umanq.srv.us/docs/receipt-demo.md): a signed receipt end to end with free calls: sample, verify, a changed receipt failing, offline check, then the free trial on a real route. - [Demo: one paid call and its signed receipt](https://vehcdj664efetfrsolne5umanq.srv.us/demo.md): one `POST /v1/verify/quote` call recorded in full: the request, the 402 offer, the paid retry with x402, the answer and its receipt, and how to check the receipt signature with Node.js. - [Verify a receipt offline](https://vehcdj664efetfrsolne5umanq.srv.us/docs/verify-offline.md): a single-file Node.js checker and the JWKS. - [MCP server](https://vehcdj664efetfrsolne5umanq.srv.us/docs/mcp.md): `quoteproof-mcp`, a stdio MCP server with one tool per route. It pays with x402 from your own key or uses the free trial. Install it from [the download list](https://vehcdj664efetfrsolne5umanq.srv.us/dl/index.json). - Remote MCP endpoint: `POST https://vehcdj664efetfrsolne5umanq.srv.us/mcp` (streamable HTTP, JSON answers, no session). The same tools; pay per call by putting an x402 payment payload in `_meta["x402/payment"]`, otherwise paid tools use the free trial and then return the payment requirements. Server card: [/mcp/server-card](https://vehcdj664efetfrsolne5umanq.srv.us/mcp/server-card). See [MCP server](https://vehcdj664efetfrsolne5umanq.srv.us/docs/mcp.md). - [Agent Skill](https://vehcdj664efetfrsolne5umanq.srv.us/docs/skill.md): `attestpage-check-sources`, a SKILL.md that tells an agent when to check a source and how to read the verdict. Index with digests: [/.well-known/agent-skills/index.json](https://vehcdj664efetfrsolne5umanq.srv.us/.well-known/agent-skills/index.json). ## How to use it - [Docs index](https://vehcdj664efetfrsolne5umanq.srv.us/docs/index.md): all pages. - [Payments](https://vehcdj664efetfrsolne5umanq.srv.us/docs/payments.md): the 402 flow, headers, network and when you are charged. - [Example recipes](https://vehcdj664efetfrsolne5umanq.srv.us/docs/recipes.md): LangChain, LlamaIndex, OpenAI Agents SDK and Claude Agent SDK agents that check a source before citing it, on the free trial or paying with x402. - [More examples](https://vehcdj664efetfrsolne5umanq.srv.us/docs/examples/index.md): runnable recipes with a route and price table: [LangChain](https://vehcdj664efetfrsolne5umanq.srv.us/docs/examples/langchain.md), [OpenAI Agents SDK](https://vehcdj664efetfrsolne5umanq.srv.us/docs/examples/openai-agents-sdk.md), [Claude Agent SDK with the stdio MCP server](https://vehcdj664efetfrsolne5umanq.srv.us/docs/examples/claude-agent-sdk.md) and [one x402 payment step by step with plain `fetch`](https://vehcdj664efetfrsolne5umanq.srv.us/docs/examples/fetch-x402.md). Their clients ask for the price once the day's trial is used up. - [How it works](https://vehcdj664efetfrsolne5umanq.srv.us/docs/how-it-works.md): what the fetcher does, content_kind values and receipt fields (including the site's IP, TLS certificate and key headers). - [Limits](https://vehcdj664efetfrsolne5umanq.srv.us/docs/limits.md): size caps, timeouts, rate limits and what the fetcher does not do. - [Errors](https://vehcdj664efetfrsolne5umanq.srv.us/docs/errors.md): every error code and whether it was charged. - [Status](https://vehcdj664efetfrsolne5umanq.srv.us/status.md): current state of each route and uptime for 24 hours, 7 days and 30 days. JSON: [/status.json](https://vehcdj664efetfrsolne5umanq.srv.us/status.json). ## Answers - [How to verify a quote an LLM cites](https://vehcdj664efetfrsolne5umanq.srv.us/docs/verify-a-quote-an-llm-cites.md): fetch the cited page and match the quote, allowing for small differences, with a page verdict and a signed receipt. - [How to check a quote an LLM cites, step by step with curl](https://vehcdj664efetfrsolne5umanq.srv.us/guides/verify-llm-quote.md): the free sample, the 402 offer, one check on the trial or with x402, how to read the answer, how to check the receipt and the same call as an MCP tool. - [How to get a signed receipt of a web page](https://vehcdj664efetfrsolne5umanq.srv.us/docs/signed-receipt-of-a-web-page.md): what a receipt records about one fetch and how to check it offline. - [How to get a signed receipt of a web page, step by step with curl](https://vehcdj664efetfrsolne5umanq.srv.us/guides/signed-page-receipt.md): the free sample, the 402 offer, one receipt on the trial or with x402, how to check it and match the page text to its hash, and the same call as an MCP tool. - [How to check links before citing them](https://vehcdj664efetfrsolne5umanq.srv.us/docs/check-links-before-citing.md): dead, moved, unreadable and invented links, checked 10 at a time. - [How to check links before citing them, step by step with curl](https://vehcdj664efetfrsolne5umanq.srv.us/guides/check-links-before-citing.md): the free sample, the 402 offer, one batch on the trial or with x402, how to read each result, how to check the cited words, how to check the receipt and the same call as an MCP tool. - [How to check a package name an LLM suggested before installing it](https://vehcdj664efetfrsolne5umanq.srv.us/docs/check-package-names-before-install.md): invented package names on npm, PyPI and crates.io, caught before install, 10 at a time. - [How to tell a bot wall from the real page](https://vehcdj664efetfrsolne5umanq.srv.us/docs/tell-a-bot-wall-from-the-real-page.md): bot challenges, JavaScript shells, paywalls and redirects that return 200 but are not the page. ## Machine-readable - [OpenAPI 3.1](https://vehcdj664efetfrsolne5umanq.srv.us/openapi.json) - [Prices](https://vehcdj664efetfrsolne5umanq.srv.us/pricing.json) - [x402 discovery](https://vehcdj664efetfrsolne5umanq.srv.us/.well-known/x402) - [API catalog (RFC 9727)](https://vehcdj664efetfrsolne5umanq.srv.us/.well-known/api-catalog) - [Agent Skills index](https://vehcdj664efetfrsolne5umanq.srv.us/.well-known/agent-skills/index.json) - [MCP server card (SEP-2127)](https://vehcdj664efetfrsolne5umanq.srv.us/mcp/server-card) - [AI Catalog](https://vehcdj664efetfrsolne5umanq.srv.us/.well-known/ai-catalog.json) - [Receipt signing keys (JWKS)](https://vehcdj664efetfrsolne5umanq.srv.us/.well-known/jwks.json) - [All docs in one file](https://vehcdj664efetfrsolne5umanq.srv.us/llms-full.txt) ## About - [Home](https://vehcdj664efetfrsolne5umanq.srv.us/index.md) - [Our fetcher and how to opt out](https://vehcdj664efetfrsolne5umanq.srv.us/bot.md) - [Terms](https://vehcdj664efetfrsolne5umanq.srv.us/terms.md) - [Privacy](https://vehcdj664efetfrsolne5umanq.srv.us/privacy.md) --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/docs/verify-quote.md # Verify a quote Last updated: 2026-10-10 `POST /v1/verify/quote` fetches a page and reports whether a quote appears on it: `exact`, `fuzzy` or `none`, with offsets, surrounding context, the page verdict and a signed receipt. For a paraphrased claim, `"match": "passages"` returns instead the passages that share most of its words (below). Price: US$0.01 per call. It checks your exact words, not a reworded version, against the page and returns offsets and page hashes in a signed receipt, all in one US$0.01 call. Use it before citing a page, to check that the words you attribute to it are on it, and that the page you got is real content rather than a bot wall or an error page. For 9 or more quotes on one page, [verify/quotes](/docs/verify-quotes) costs less: one call checks up to 20. ## Request ```json { "url": "https://www.iana.org/help/example-domains", "quote": "These domains may be used as illustrative examples in documents", "match": "fuzzy", "case_sensitive": false, "threshold": 0.9 } ``` | Field | Required | Meaning | |---|---|---| | `url` | yes | http or https URL, at most 2,048 characters | | `quote` | yes | text to look for, or with `passages` the claim to find support for; at most 1,000 characters after normalisation | | `match` | no | `fuzzy` (default) also accepts small differences; `exact` needs the same text after normalisation; `passages` finds the passages that best share a paraphrased claim's words | | `case_sensitive` | no | default `false`; must be `false` with `passages` | | `threshold` | no | fuzzy similarity needed, 0.5 to 1, default 0.9 | Before matching, both the page text and the quote are normalised: Unicode NFKC, curly quotes and dashes become plain ones, invisible characters are dropped, and all whitespace becomes one space. ## Response From the free sample (`GET /v1/sample/quote`), shortened: ```json { "result": { "match": "fuzzy", "score": 0.963, "start": 211, "end": 238, "context": "...the agency said that emissions fell 12 % in 2025, after two years of slow growth...", "occurrences": 1, "normalised_sha256": "086a29af…" }, "page": { "requested_url": "https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/page", "final_url": "https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/page", "http_status": 200, "content_kind": "real", "signals": ["status_200"], "title": "Regional climate report 2025 (demo page)", "content_sha256": "0e429607…", "retrieved_at": "2026-10-09T14:19:34.903Z" }, "injection_flags": [], "advice": "The page loaded and looks like real content.", "receipt": "eyJhbGciOiJFZERTQSIs…", "tier": "paid", "rail": "x402", "network": "eip155:84532" } ``` - `match` is `exact`, `fuzzy` or `none`. `score` is the similarity from 0 to 1. - `start` and `end` are code-point offsets into the normalised page text. `normalised_sha256` is the SHA-256 of that text, so anyone holding the page text can check the offsets. - `occurrences` counts exact matches. - Read `page.content_kind` before relying on `match`. A `none` on a `bot_wall` or `js_shell` page means we could not read the page, not that the quote is absent from it. See [How it works](/docs/how-it-works). ## Passages: a paraphrased claim An agent rarely quotes word for word. With `"match": "passages"`, `quote` is a claim (for example "The agency reported a 12 percent drop in emissions during 2025") and the answer is the 3 passages of the page that share most of its words: On the demo page ([`https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/page`](https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/page)), shortened; try it free with [`GET /v1/sample/quote?match=passages`](https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/quote?match=passages): ```json { "result": { "match": "passages", "score": 0.583, "start": 115, "end": 271, "occurrences": 3, "passages": [ { "start": 115, "end": 271, "score": 0.583, "text": "It is not a real report and its numbers are made up. In its annual summary the agency said that emissions fell 12 % in 2025, after two years of slow growth.", "missing": ["drop"], "flags": ["negation_mismatch"] }, { "start": 357, "end": 513, "score": 0.217, "text": "The agency also noted that the share of renewable power rose to 41 percent, and that the next report will cover water use and air quality across the region.", "missing": ["12", "drop", "emissions", "2025"], "flags": ["number_mismatch"] }, { "start": 0, "end": 114, "score": 0.136, "text": "Regional climate report 2025 This is a demo page served by AttestPage so that agents can try every route for free.", "missing": ["agency", "12", "percent", "drop", "emissions"], "flags": [] } ], "scorer": "lexical-v2", "note": "Passages ranked by how many of the claim's words (weighted by rarity on the page) and word pairs they share. This is evidence for you to judge, not a verdict: ..." } } ``` Here "reported" counted as found because the passage has "report". The best passage does support the claim, but only a reader can tell that "fell 12 %" means a 12 percent drop. It is flagged `negation_mismatch` only because its first sentence says "not a real report": a flag tells you where to look, not that the passage contradicts the claim. - **This is evidence, not a verdict.** A score measures shared words, not meaning. "Emissions did not fall" shares as many words as "emissions fell". Read each passage, and check negation, numbers, dates and the words in `missing`. - **Passages.** A passage is a sentence, or two neighbouring ones; text with no sentence ends is cut every 40 words. A sentence ends at `.`, `!` or `?` before a space, or at `。`, `!` or `?`, but not after a short abbreviation such as "e.g.", "i.e.", "Dr.", "Mr." or an initial ("J. Smith"); "etc." ends one only before a capital. Passages do not overlap and are sorted by `score`, highest first. The top-level `score`, `start`, `end` and `context` are those of the best passage, and `occurrences` counts the passages. - **Score.** From 0 to 1: 0.75 × the share of the claim's words the passage holds, each weighted by how rare it is on the page, plus 0.25 × the share of the claim's neighbouring word pairs it holds. - **How words are compared.** Without case, with light English stemming ("emissions" meets "emission"), and ignoring common English words such as "the" and "was". `%` counts as "percent". In Chinese and Japanese, each character counts as a word. - **Offsets and text.** `start` and `end` are code-point offsets into the normalised page text, as above. `text` is cut to 800 characters with `...` when the passage is longer. - **missing.** Lists the claim's words that the passage lacks, at most 20. - **flags.** Marks a passage worth a closer read; it never changes the score or the order. `negation_mismatch`: the claim or the passage has an English negation ("not", "no", "never", "n't", "without" and similar) and the other has none. `number_mismatch`: the claim has a number the passage lacks and the passage has one the claim lacks, such as 12 against 15 (`12 %`, `12 percent` and `12.0` count as the same number, and so do `1,000` and `1000`). An empty list does not mean the passage agrees with the claim. - **No match.** `match` is `none` with an empty `passages` list when no passage shares a word. - **Errors.** A claim made only of common words or punctuation gets 400. - **Same answer every time.** No language model is used, so the same page and claim give the same answer. `scorer` names the method (now `lexical-v2`) and changes whenever the same page and claim could give different passages, scores or flags. - **Cost.** The cost is linear in the page size, so passages never gets `match_too_costly`. - **Receipt.** The receipt signs `mode`, `scorer` and each passage's `start`, `end` and `score`. The text can be recovered from the page at those offsets, and `missing` and `flags` from that text, the claim and the `scorer`. - **Price.** The same as any other `verify/quote` call. ## PDF A PDF page is matched like any other page. Its text layer is read as described in [Fetch: PDF](/docs/fetch), with no OCR. - A match on a PDF also has `pdf_page` and `pdf_page_end`: the 1-based pages where it starts and ends. Each passage has them too. - `start` and `end` are still offsets into the whole normalised text. In that text, each page break is one space. - Only text inside each page's box is read, as a reader sees it. A quote from text drawn outside the page is not found. - The receipt signs `pdf_page` and `pdf_page_end` next to the offsets. Only the full service reads PDFs: an edge server answers 501 `not_on_edge` (`details.reason` `pdf`) for one, not charged. Limits are in [Limits](/docs/limits). ## Charging The quote and options are checked before payment: a bad request gets 400 and is not charged. A quote too costly to match fuzzily gets 422 `match_too_costly`, not charged; use `"match": "exact"` or a shorter quote. A URL whose host name does not resolve gets 422 `host_not_found`, not charged. Results about the page itself, including 404s, bot walls and timeouts, are charged. See [Payments](/docs/payments). --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/docs/verify-quotes.md # Verify quotes on one page Last updated: 2026-10-10 `POST /v1/verify/quotes` checks 1 to 20 quotes against one web page in one call. The page is fetched once, and each quote gets the same answer as [Verify a quote](/docs/verify-quote) (`exact`, `fuzzy` or `none`, score, offsets, context), all under one signed receipt. Price: US$0.08 per call, whether it carries 1 quote or 20, so from 9 quotes up it costs less than one `verify/quote` call per quote. Use it when an answer quotes the same source several times: every quote is checked against the same copy of the page in one paid call. ## Request ```json { "url": "https://www.iana.org/help/example-domains", "quotes": [ "These domains may be used as illustrative examples in documents", "without prior coordination with us" ], "match": "fuzzy" } ``` | Field | Required | Meaning | |---|---|---| | `url` | yes | the page, http or https, at most 2,048 characters | | `quotes` | yes | 1 to 20 strings, each at most 1,000 characters after normalisation | | `match` | no | for every quote: `fuzzy` (default) also accepts small differences; `exact` needs the same text after normalisation; `passages` treats each quote as a paraphrased claim (below) | | `case_sensitive` | no | default `false`; must be `false` with `passages` | | `threshold` | no | fuzzy similarity needed, 0.5 to 1, default 0.9 | The URL and every quote are checked before payment. If the URL is malformed, blocked or disallowed by robots.txt, or any quote is empty, too long or not a string (or, with `passages`, has no word to compare), the whole request is refused with 400 or 403 and not charged. The error's `details.field` names the quote, for example `quotes[2]`. Other fields are refused too. ## Response From the free sample (`GET /v1/sample/quotes`), shortened: ```json { "results": [ { "match": "exact", "score": 1, "start": 384, "end": 431, "context": "... The agency also noted that the share of renewable power rose to 41 percent, and that the next report will cover...", "occurrences": 1, "normalised_sha256": "086a29af…" }, { "match": "fuzzy", "score": 0.963, "start": 211, "end": 238, "context": "...In its annual summary the agency said that emissions fell 12 % in 2025, after two years of slow growth...", "occurrences": 1, "normalised_sha256": "086a29af…" }, { "match": "none", "score": null, "start": null, "end": null, "context": null, "occurrences": 0, "normalised_sha256": "086a29af…" } ], "page": { "requested_url": "https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/page", "final_url": "https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/page", "http_status": 200, "content_kind": "real", "title": "Regional climate report 2025 (demo page)", "content_sha256": "0e429607…" }, "injection_flags": [], "advice": "The page loaded and looks like real content.", "summary": { "exact": 1, "fuzzy": 1, "none": 1, "failed": 0 }, "receipt": "eyJhbGciOiJFZERTQSIs…", "tier": "paid" } ``` - `results` has one entry per quote, in request order. Each is the `result` of [Verify a quote](/docs/verify-quote): `match`, `score`, `start`, `end`, `context`, `occurrences` and `normalised_sha256` mean the same, and give the same values a `verify/quote` call on that page would. - `page` is the page verdict, hashes and network evidence, once for all the quotes. - Read `content_kind` before relying on `match`. A `none` on a page that is not `real` (a 404, bot wall or JavaScript shell) means we could not read the page, not that the quote is absent from it. - `summary` counts the verdicts; `failed` counts quotes that carry an `error` instead of a verdict (below). - The receipt signs the page's URL, status, `content_kind`, hashes and evidence, and per quote the SHA-256 of the quote and the verdict. It does not contain the quote text. Check it with [`POST /v1/receipt/verify`](/docs/receipt-verify). ## Passages: paraphrased claims With `"match": "passages"`, each quote is a claim. Each result is then the one described in [Verify a quote: passages](/docs/verify-quote): up to 3 passages from the page that share most of the claim's words, each with `start`, `end`, `score`, `text`, `missing` and `flags`, plus `scorer` and `note`. These are evidence for you to judge, not a verdict. - `match` is `passages` or `none`, and `summary` adds a `passages` count. - The receipt signs each claim's passages (`start`, `end`, `score`) and the `scorer`. - The work is linear in page size, so passages never fail with `match_too_costly`. The price is the same. ## PDF A PDF page is read and matched as in [Verify a quote: PDF](/docs/verify-quote). Each match and passage on it has `pdf_page` and `pdf_page_end`, and the receipt signs them. If the PDF cannot be queued for reading, the call gets 503 `over_capacity` (`details.reason` `pdf_queue_full`) and is not charged. ## Partial failures A quote that would cost too much to match fuzzily gets `"error": { "code": "match_too_costly", ... }` and null match fields; the other quotes are still checked and the call is charged. The fuzzy searches of one call also share a work limit (twice what one `verify/quote` call may use), spent in request order: a quote reached after it is used up gets the same error with a message saying so. If every quote fails this way, the call gets 422 `match_too_costly`, not charged, and `details.failed` lists only which quotes failed. Use `"match": "exact"` or shorter quotes. This route shares the per-address count of uncharged failed batches and the match queue with [Verify citations](/docs/verify-citations): over the count it answers 429 `rate_limited` with `details.reason` `uncharged_failures`, and when the queue is full 503 `over_capacity` with `details.reason` `match_queue_full`, both with a `Retry-After` header and not charged. See [Limits](/docs/limits). ## Edge servers Only the full service runs this route: an edge server answers 501 `not_on_edge`, not charged. ## Charging A URL whose host name does not resolve gets 422 `host_not_found`, not charged. If the page turns out to be disallowed by robots.txt or rate limited after payment (for example after a redirect), the call answers with an error and is not charged. Results about the page itself, including 404s, bot walls and timeouts, are charged. See [Payments](/docs/payments). A free-trial call covers at most 3 quotes. A larger one is not run as a trial: it gets the normal 402 offer, with `details.reason` `trial_too_large`, and is not charged. --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/docs/verify-citations.md # Verify citations Last updated: 2026-10-10 `POST /v1/verify/citations` checks 1 to 10 citations, each a quote and the URL it is attributed to, in one call. For each one you get the same answer as [Verify a quote](/docs/verify-quote) (`exact`, `fuzzy` or `none`, score, offsets, context) plus the verdict on its page, all under one signed receipt. Price: US$0.10 per batch. Use it on the sources at the end of an answer before the user sees it: a made-up quote, a misquote, or a link to a dead or blocked page is caught in one paid call. For 1 to 9 pairs, one [verify/quote](/docs/verify-quote) call per pair costs less, without the Crossref data. ## Request ```json { "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" } ], "match": "fuzzy" } ``` | Field | Required | Meaning | |---|---|---| | `citations` | yes | 1 to 10 objects `{ "url", "quote" }`. `url` is http or https, at most 2,048 characters; `quote` is at most 1,000 characters after normalisation | | `match` | no | for every citation: `fuzzy` (default) also accepts small differences; `exact` needs the same text after normalisation; `passages` treats each quote as a paraphrased claim (below) | | `case_sensitive` | no | default `false`; must be `false` with `passages` | | `threshold` | no | fuzzy similarity needed, 0.5 to 1, default 0.9 | Several citations may share a URL: each distinct URL is fetched once and every quote on it is matched against the same copy. Every URL is checked before payment; if one is malformed, blocked or disallowed by robots.txt, or any quote is empty or too long (or, with `passages`, has no word to compare), the whole request is refused with 400 or 403 and not charged. The error's `details.field` names the citation, for example `citations[2].quote`. ## Response From the free sample (`GET /v1/sample/citations`), shortened: ```json { "results": [ { "url": "https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/page", "page": 0, "final_url": "https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/page", "http_status": 200, "content_kind": "real", "match": "fuzzy", "score": 0.963, "start": 211, "end": 238, "context": "...the agency said that emissions fell 12 % in 2025, after two years of slow growth...", "occurrences": 1, "advice": "The page loaded and looks like real content." }, { "url": "https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/missing", "page": 1, "final_url": "https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/missing", "http_status": 404, "content_kind": "http_error", "match": "none", "score": null, "start": null, "end": null, "context": null, "occurrences": 0 } ], "pages": [ { "requested_url": "https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/page", "final_url": "https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/page", "http_status": 200, "content_kind": "real", "title": "Regional climate report 2025 (demo page)", "content_sha256": "0e429607…", "normalised_sha256": "086a29af…", "injection_flags": [] } ], "summary": { "exact": 0, "fuzzy": 1, "none": 2, "failed": 0, "retracted": 0, "doi_unchecked": 0 }, "receipt": "eyJhbGciOiJFZERTQSIs…", "tier": "paid" } ``` - `results` has one entry per citation, in request order. `page` is its index in `pages`, which has one entry per distinct URL with the page verdict, hashes and network evidence. - `match`, `score`, `start`, `end`, `context` and `occurrences` mean what they mean in [Verify a quote](/docs/verify-quote). Offsets are into the normalised text of that citation's page; `normalised_sha256` on the page lets anyone holding the text check them. - Read `content_kind` before relying on `match`. A `none` on a page that is not `real` (a 404, bot wall or JavaScript shell) means we could not read the page, not that the quote is absent from it. - `summary` counts the verdicts; `failed` counts citations that carry an `error` instead of a verdict (below). `retracted` counts citations on a paper Crossref marks retracted, and `doi_unchecked` citations whose page's DOI lookup was skipped, so their retraction status is unknown (see Retracted papers; both absent when DOI lookups are off). - The receipt signs, per page, the URL, status, `content_kind`, hashes and evidence, and per citation its page index, the SHA-256 of the quote and the verdict. It does not contain the quote text. Check it with [`POST /v1/receipt/verify`](/docs/receipt-verify). ## Passages: paraphrased claims With `"match": "passages"`, each citation's `quote` is a claim. Each result is then the one described in [Verify a quote: passages](/docs/verify-quote): up to 3 passages from the page that share most of the claim's words, each with `start`, `end`, `score`, `text`, `missing` and `flags`, plus `scorer`. - `match` is `passages` or `none`, and `summary` adds a `passages` count. - The response has a top-level `note`: the passages are evidence for you to judge, not a verdict. - The receipt signs each citation's passages (`start`, `end`, `score`) and the `scorer`. - The work is linear in page size, so passages share no work limit and never fail with `match_too_costly`. The price is the same. ## PDF A cited PDF is read and matched as in [Verify a quote: PDF](/docs/verify-quote). Each match and passage on it has `pdf_page` and `pdf_page_end`, and the receipt signs them. Its entry in `pages` has `pdf` (`page_count`, `pages_read`, `truncated`). If one PDF in the batch cannot be queued for reading, the whole batch gets 503 `over_capacity` (`details.reason` `pdf_queue_full`) and is not charged. ## Retracted papers (DOI) A cited page with a DOI gets `crossref` in its `pages` entry: the work's `title`, `authors` (up to 10, with `authors_total`), `year`, `container_title` (the journal) and `type` from Crossref's public metadata, and `retracted`: `true` when Crossref lists a retraction, withdrawal or removal notice for it (Crossref carries the Retraction Watch database). `updates` lists every notice that updates the work (`type` such as `retraction`, `correction` or `expression_of_concern`, notice `doi`, `source`, `date`), and `update_to` is filled when the cited DOI is itself such a notice. `summary.retracted` counts the citations on retracted works. A citation that matches its quote on a retracted paper still needs a warning before you use it. The DOI is taken from a `doi.org` link, else the page's own `citation_doi` (or similar) meta tag, else a DOI in the URL path; `doi_source` says which (`url` or `page`). `status` is `found`, `not_found` (not a Crossref DOI; DataCite and other agencies are not checked) or `skipped` with a `reason` (`rate_limited`, `busy`, `timeout` or `unavailable`) when Crossref is slow or we are at our share of its public limits. A skipped lookup never fails the call or changes the charge; `summary.doi_unchecked` counts the citations whose page's DOI lookup was skipped, so `retracted`: 0 with `doi_unchecked` above 0 means not every DOI was checked. Lookups are made one at a time across all callers (at most 120 a minute for paid calls; free trial calls have their own budget of 6 a minute, so they never use the paid one) and answers are cached for a day. The receipt signs each page's `doi`, `status` and `retracted`. ## Partial failures A quote that would cost too much to match fuzzily on its page gets `"error": { "code": "match_too_costly", ... }` and null match fields; the rest of the batch is still checked and the batch is charged. The fuzzy searches of one batch also share a work limit (twice what one `verify/quote` call may use), spent in page order: a citation reached after it is used up gets the same error with a message saying so, while an exact match is still found. If every citation in the batch fails this way, the call gets 422 `match_too_costly`, not charged, and `details.failed` lists only which citations failed. Use `"match": "exact"` or shorter quotes. Each caller IP address gets 6 uncharged failed batches a minute (a bucket that refills one every 10 seconds). A batch holds a place in that bucket while it runs and gives it back unless every citation failed, so a caller with 6 batches still running also waits. Over the limit, `verify/citations` answers 429 `rate_limited` with `details.reason` `uncharged_failures` and a `Retry-After` header, before any page is fetched, and nothing is charged. This normally comes before the 402; if your own parallel batches take the last place meanwhile, it comes after your payment is verified and before it is settled. Charged batches never use the bucket up. A few batches are matched at once across all callers and a short queue waits behind them. When that queue is full, `verify/citations` answers 503 `over_capacity` with `details.reason` `match_queue_full` and a `Retry-After` header (a few seconds), and nothing is charged: before the 402 if the queue is already full, or after your payment is verified and before it is settled if it filled while your pages were fetched. Retry the same request after `Retry-After`. ## Dead domains A URL whose host name does not resolve is reported as a result: `content_kind` `http_error`, `http_status` null, `match` `none`, and the batch is charged. If no URL in the batch resolves, the call gets 422 `host_not_found` and is not charged. ## Charging If a page turns out to be disallowed by robots.txt or rate limited after payment (for example after a redirect), the whole batch answers with an error and is not charged. Results about the pages themselves, including 404s, bot walls and timeouts, are charged. See [Payments](/docs/payments). A free-trial call covers at most 3 citations. A larger batch is not run as a trial: it gets the normal 402 offer, with `details.reason` `trial_too_large`, and is not charged. --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/docs/verify-document.md # Verify a document Last updated: 2026-10-10 `POST /v1/verify/document` takes an answer or report as written, finds every quote in it with its link by fixed rules, and checks each pair on the live page, as [Verify citations](/docs/verify-citations) does: `exact`, `fuzzy` or `none`, score, offsets in your document and on the page, and the verdict on each page, under one signed receipt. Price: US$0.10 per part of up to 10 pairs. Use it before sending a finished answer or report that quotes web pages. Call the free preview first: it lists the pairs, what was skipped and why, the number of parts and the exact price. For a part of 1 to 9 pairs, one [verify/quote](/docs/verify-quote) call per pair costs less; the free preview lists each pair and its part. ## Request ```json { "document": "WHO says \"Air pollution kills an estimated seven million people every year\" ([WHO](https://www.who.int/health-topics/air-pollution)).", "format": "markdown", "match": "fuzzy", "part": 0 } ``` | Field | Required | Meaning | |---|---|---| | `document` | yes | The answer or report as text, 1 character to 60,000 bytes UTF-8 | | `format` | no | `markdown` (default) or `text` (only bare URLs and `[n]` references are links) | | `match` | no | `fuzzy` (default), `exact` or `passages`, as verify/citations; it also chooses what is found (below) | | `case_sensitive` | no | default `false`; must be `false` with `passages` | | `threshold` | no | 0.5 to 1, default 0.9 (fuzzy only) | | `min_words` | no | 1 to 20, default 4: a quoted span with fewer words is skipped as `too_short` | | `part_size` | no | 1 to 10, default 10: pairs per part. A free trial call needs 3 or fewer | | `part` | no | 0-based part to check, default 0. Ignored by the preview | Unknown fields are refused with 400. Every URL in the part is checked before payment, as on verify/citations: a blocked or robots-disallowed URL refuses the call with 400 or 403, `details.field` naming the pair (for example `pairs[3].url`), and nothing is charged. ## Preview `POST /v1/verify/document/preview` is free. Send the same body; nothing is fetched or charged. ```json { "document_sha256": "9f2c…", "extractor": "doc-v1", "pairs_total": 1, "part_size": 10, "parts": 1, "price": { "usd_per_part": "0.10", "sats_per_part": 100, "usd_total": "0.10", "sats_total": 100 }, "pairs": [ { "pair": 0, "part": 0, "quote": "Air pollution kills an estimated seven million people every year", "url": "https://www.who.int/health-topics/air-pollution", "doc_start": 10, "doc_end": 74, "source": "inline_link" } ], "skipped": [], "skipped_truncated": 0, "skipped_counts": {}, "limit_reached": false } ``` - `doc_start`/`doc_end`: code-point offsets of the quoted words in `document` as sent. A pair found more than once (same quote, same URL) is one pair with `doc_spans`, checked and charged once. `doc_spans` holds the first 20 places; `doc_spans_truncated` counts the rest. - `source`: `inline_link`, `reference`, `footnote`, `numbered`, `bare_url`, `blockquote` or `sentence` (passages). - `skipped[]`: `{ doc_start, doc_end, reason, text }`; reasons `too_short`, `too_long`, `no_url`, `bad_url`, `beyond_limit`. It lists the first 100; `skipped_truncated` counts the rest and `skipped_counts` gives every skip by reason. - The preview contacts no URL, so a dead or blocked URL shows up only on the paid call. The preview takes from the same per-address limit as the paid routes and works on the edge server too. ## Response The verify/citations response for this part, plus the document fields: ```json { "document_sha256": "9f2c…", "extractor": "doc-v1", "part": 0, "parts": 1, "pairs_total": 1, "results": [ { "pair": 0, "quote": "Air pollution kills…", "url": "https://www.who.int/…", "doc_start": 10, "doc_end": 74, "page": 0, "final_url": "…", "http_status": 200, "content_kind": "real", "match": "fuzzy", "score": 0.96, "start": 412, "end": 478, "context": "…", "occurrences": 1 } ], "pages": [ "as verify/citations" ], "summary": { "exact": 0, "fuzzy": 1, "none": 0, "failed": 0 }, "receipt": "eyJ…", "tier": "paid" } ``` - `doc_start`/`doc_end` are offsets in your document; `start`/`end` are offsets on the page, as in [Verify a quote](/docs/verify-quote). Read `content_kind` before relying on `match`. - The receipt (kind `document`) signs `document_sha256`, the extractor, the options, the part and, per pair, its page, the SHA-256 of the quote, its document offsets (with `doc_spans` and `doc_spans_truncated` for a repeated pair) and its verdict. It does not contain the quote text. Whoever holds the document can tie every part's receipt to it. Check it with [`POST /v1/receipt/verify`](/docs/receipt-verify). ## How pairs are found The rules are fixed and named by `extractor` (`doc-v1`) in every answer and receipt; a change gets a new name. - Code blocks and inline code are ignored. With `format` `markdown`, links are inline links, reference links, footnotes, `[n]` references with a numbered list and bare URLs; with `text`, only bare URLs and `[n]` references. - A quote is text between paired `"…"`, `“…”`, `«…»` or `„…“` inside one paragraph, or a blockquote (its last line starting with `—`, `--`, `-` or `Source:` is the attribution and its link the source). Single quotes are not used. - A quote's link is the first link after it before the next quote or the paragraph end; else the last link before it in the same paragraph; else the blockquote's attribution; else it is skipped as `no_url`. - With `match` `passages`, every sentence that carries a link is a claim and gets its best-matching passages as evidence, as in verify/citations. - Then `bad_url`, `too_short` (`min_words`, not for passages), `too_long` (over 1,000 characters), duplicates and the 100-pair limit (`beyond_limit`) are applied, and pairs are numbered in document order. Part `p` holds pairs `p × part_size` to `p × part_size + part_size − 1`. ### Fixes to doc-v1 - 2026-10-10: in an indented code block (4 spaces or a tab, after a blank line or heading), every line is now code. Before, only its first line was skipped, so a quote and link on a later line of the block could be taken as a pair. Nothing else changed. ## Parts and price Each paid call checks one part of up to 10 pairs, at US$0.10: the same price as a verify/citations batch. | Document | Calls | |---|---| | 4 quotes | 1 | | 10 quotes | 1 | | 23 quotes | 3 | ## Limits | Limit | Value | |---|---| | `document` | 60,000 bytes UTF-8 (400 over it) | | Pairs per document | 100 (10 parts of 10); the rest are skipped as `beyond_limit` | | Pairs per part | `part_size`, at most 10 | | Places listed per repeated pair | 20 in `doc_spans`; the rest counted in `doc_spans_truncated` | | Skipped items listed | 100 in `skipped`; the rest counted in `skipped_truncated` | | Quote or claim | 1,000 characters | | Free-trial part | 3 pairs; a larger part gets the normal 402 offer (`details.reason` `trial_too_large`) | Matching shares its slots, queue and failure limit with verify/citations: 6 uncharged failed calls a minute per caller address across both routes, then 429 `rate_limited` (`details.reason` `uncharged_failures`); a full match queue gives 503 `over_capacity` with `Retry-After`. See [Limits](/docs/limits). ## Charging A part is charged when it is checked, including pages that are 404s, bot walls or timeouts, and a pair whose fuzzy work ran out (it carries an `error`). Never charged: any 400, 422 `no_pairs` (no quote with a link was found), 422 `host_not_found` (no URL in the part resolves), 422 `match_too_costly` (every pair in the part failed), 429, 503, and the edge server's 501 `not_on_edge`. See [Payments](/docs/payments). ## Errors See [Errors](/docs/errors). `part` past the last part gets 400 with `details.parts`. --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/docs/fetch.md # Fetch a page Last updated: 2026-10-10 `POST /v1/fetch` fetches one URL and tells you what came back: a `content_kind` verdict, HTTP status, final URL after redirects, title, hashes, injection flags and, if you ask, up to 20,000 characters of clean text. Price: US$0.002 per call. Use it when you need to know whether a URL gave you real content, a bot challenge, a JavaScript shell, a paywall or an error, with a receipt that records it. ## Request ```json { "url": "https://www.iana.org/help/example-domains", "return_text": true } ``` | Field | Required | Meaning | |---|---|---| | `url` | yes | http or https URL, at most 2,048 characters | | `return_text` | no | also return up to 20,000 characters of clean text; default `false` | ## Response From the free sample (`GET /v1/sample/fetch`), shortened: ```json { "page": { "requested_url": "https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/page", "final_url": "https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/page", "http_status": 200, "content_type": "text/html", "content_kind": "real", "signals": ["status_200"], "title": "Regional climate report 2025 (demo page)", "redirects": [], "content_sha256": "0e429607…", "raw_sha256": "179b9ac4…", "retrieved_at": "2026-10-09T14:19:34.909Z", "bytes": 847, "truncated": false }, "text": "Regional climate report 2025\n\nThis is a demo page…", "text_chars": 636, "text_truncated": false, "injection_flags": [], "advice": "The page loaded and looks like real content.", "receipt": "eyJhbGciOiJFZERTQSIs…", "tier": "paid", "rail": "x402", "network": "eip155:84532" } ``` - `content_kind` and `signals` are explained in [How it works](/docs/how-it-works). `advice` is one plain sentence about the verdict. - `content_sha256` hashes the full extracted text, not the 20,000-character copy. `raw_sha256` hashes the bytes as received. `content_sha256` is null when no body was read. - `injection_flags` lists prompt-injection patterns found in the text, such as `instruction_override` or `hidden_instructions`. They are flags, not a verdict: an empty list means none of our patterns matched. - `text` is null unless you set `return_text`. ## PDF An `application/pdf` page is read too, within the same 2 MB and 10-second limits. Only its text layer is read, with no OCR, so a scanned PDF comes back as `empty` with the signal `pdf_no_text_layer`. - `signals` includes `pdf`. `title` is the PDF's own title, if it has one. - `text` has the pages in order, separated by a form feed (`\f`). - Only text inside each page's box is read, as a reader sees it. Text drawn outside the page is not in `text`. - `page.pdf` gives `page_count`, `pages_read` and `truncated`. Very long PDFs stop at a page or character cap, with the signal `pdf_truncated`. The receipt signs these as well. - A PDF we could not read is `unsupported_type` with a signal saying why: `pdf_encrypted`, `pdf_invalid`, `pdf_over_budget`, `pdf_timeout` or `pdf_out_of_memory`. It is charged like any other page we cannot read. - When too many PDFs are waiting to be read, the call gets 503 `over_capacity` (`details.reason` `pdf_queue_full`) with `Retry-After`. It is not charged. Only the full service reads PDFs: an edge server answers 501 `not_on_edge` (`details.reason` `pdf`) for one, not charged. Limits are in [Limits](/docs/limits). ## Charging A bad URL, a blocked address or a host name that does not resolve is refused before payment and not charged. Results about the page itself, including 404s, bot walls and timeouts, are charged: that is the answer you paid for. See [Payments](/docs/payments). --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/docs/check-links.md # Check links Last updated: 2026-10-10 `POST /v1/check/links` checks 1 to 10 URLs in one call: status, redirect chain, final URL, `content_kind`, title and content hash for each, under one signed receipt. Price: US$0.005 per batch. Use it to check the sources in a draft before you publish or hand it on. ## Request ```json { "urls": ["https://www.iana.org/help/example-domains", "https://www.iana.org/domains/reserved"] } ``` `urls` is a list of 1 to 10 http or https URLs, each at most 2,048 characters. Every URL is checked before payment; if any one is malformed, blocked or disallowed by robots.txt, the whole request is refused and not charged. ## Response From the free sample (`GET /v1/sample/links`), shortened: ```json { "results": [ { "url": "https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/page", "final_url": "https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/page", "http_status": 200, "redirects": [], "content_kind": "real", "title": "Regional climate report 2025 (demo page)", "content_sha256": "0e429607…" }, { "url": "https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/missing", "final_url": "https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/missing", "http_status": 404, "redirects": [], "content_kind": "http_error", "title": null, "content_sha256": "cb699046…" } ], "receipt": "eyJhbGciOiJFZERTQSIs…", "tier": "paid" } ``` ## Dead domains A URL whose host name does not resolve is reported as a result: `content_kind` `http_error`, `http_status` null, and the batch is charged. If no URL in the batch resolves, the call gets 422 `host_not_found` and is not charged. ## PDF links A live PDF link (status 2xx, content type `application/pdf`, on the same site as the URL) is reported with `content_kind` `pdf`. check/links does not read PDFs, so its `title` and `content_sha256` are null. To read a PDF's text or check a quote in it, use [fetch](/docs/fetch) or [verify/quote](/docs/verify-quote) on the full service. ## Links that are not read check/links reads HTML, text and JSON only. Any other link (a PDF, an image, a download) with a 4xx or 5xx status is `http_error`, so a dead PDF reads as dead. Any link that redirects to another site is `off_site`, whatever its type, as for an HTML page. If the other site answers with a 4xx or 5xx status, the result also has `"off_site_error": true`, so a link that moved to a dead page is not missed; otherwise the field is left out. Where archive snapshots are on, a paid call also looks such a link up (the URL you sent) and adds `wayback`, as for a failed URL. A live link of another type on the same site is `unsupported_type`. ## Charging If a URL turns out to be disallowed by robots.txt or rate limited after payment (for example after a redirect), the whole batch answers with an error and is not charged. See [Payments](/docs/payments). A free-trial call covers at most 3 URLs. A larger batch is not run as a trial: it gets the normal 402 offer, with `details.reason` `trial_too_large`, and is not charged. --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/docs/check-packages.md # Check packages Last updated: 2026-10-10 `POST /v1/check/packages` looks up 1 to 10 package names on npm, PyPI or crates.io in one call and says, for each, whether the registry lists that name, with its latest version, description, licence, links and a few flags, under one signed receipt. Price: US$0.005 per batch. Use it before you install a package an agent or a model suggested: a name that does not exist may be a hallucination, and a squatter can register it later. **It answers "exists in registry", nothing more. It is not a security verdict.** A listed package can still be malicious, abandoned, typo-squatted or not the one you meant. Description, licence and links are the publisher's own text, copied from the registry. ## Request ```json { "packages": [ { "ecosystem": "npm", "name": "left-pad" }, { "ecosystem": "pypi", "name": "requests" }, { "ecosystem": "crates", "name": "serde" } ] } ``` `packages` is a list of 1 to 10 objects, each with exactly two fields: - `ecosystem`: `npm`, `pypi` or `crates` (crates.io) - `name`: the package name as you would install it. npm names may be scoped (`@scope/name`). Every name is checked against its registry's naming rules before payment; if any one is invalid, the whole request is refused with 400 and not charged. No lookup happens before payment. ## Response From the free sample (`GET /v1/sample/packages`), shortened: ```json { "results": [ { "ecosystem": "npm", "name": "left-pad", "exists": true, "status": "found", "registry_name": "left-pad", "latest_version": "1.3.0", "description": "String left pad (demo copy of registry data)", "license": "WTFPL", "homepage": "https://github.com/stevemao/left-pad#readme", "repository": "git+ssh://git@github.com/stevemao/left-pad.git", "created_at": "2014-03-14T09:09:20.762Z", "deprecated": "use String.prototype.padStart()", "yanked": null, "signals": ["deprecated"], "registry_url": "https://www.npmjs.com/package/left-pad", "http_status": 200, "raw_sha256": "09093cc2…" }, { "ecosystem": "npm", "name": "left-pad-demo-missing", "exists": false, "status": "not_found", "latest_version": null, "signals": [], "registry_url": "https://www.npmjs.com/package/left-pad-demo-missing", "http_status": 404, "raw_sha256": "c8d3eae1…" } ], "note": "exists means the registry listed this name when we looked. It is not a security verdict: the package may still be malicious, unmaintained or not the one you meant; …", "receipt": "eyJhbGciOiJFZERTQSIs…", "tier": "paid" } ``` | Field | Meaning | |---|---| | `exists` | `true` the registry lists the name, `false` it answered 404, `null` the lookup failed | | `status` | `found`, `not_found` or `lookup_failed` (then `reason`: `timeout`, `network_error`, `too_large`, `registry_rate_limited`, `http_` or `bad_json`) | | `registry_name` | the name as the registry spells it | | `latest_version` | npm `latest` tag, PyPI's current version, crates.io's default version | | `created_at` | npm: the package document's `time.created` (null if that document is over 4 MB or cannot be read); PyPI: first file upload; crates.io: crate creation | | `license` | npm and PyPI: the latest version's licence field; crates.io: the default version's licence | | `deprecated` | npm deprecation message of the latest version, else null | | `yanked` | PyPI: latest release yanked; crates.io: crate yanked; npm: null | | `signals` | flags worth a second look: `name_differs`, `deprecated`, `yanked`, `created_recently` (under 30 days). `name_differs` means the registry's own spelling (`registry_name`) is not the name you sent; PyPI and crates.io ignore case and treat `-` and `_` (PyPI also `.`) as the same, so it is often only a spelling variant of the same package | | `raw_sha256` | SHA-256 of the registry's answer as we received it (npm: the `/latest` answer) | Signals are hints, not a security verdict: a package with no signals can still be malicious. The receipt has `kind` `packages` and, per name, the ecosystem, name, `exists`, `status`, `latest_version` and `raw_sha256`. ## Rate limits crates.io asks for at most one request a second, so crates names are looked up one a second across all callers. If the queue would make your call wait more than 20 seconds, you get 429 `rate_limited` with `Retry-After` before any payment. Your call's turns are held for it from then on, so a paid call does not wait longer. Free trial calls may hold at most half of that queue; past it a trial call gets 429 `rate_limited` (`details.scope` `trial`) and can retry later or pay. ## Charging A failed lookup is a result and the batch is charged. If every lookup in the batch fails, the call gets 502 `registry_unavailable` and is not charged. On an edge server, a batch where any lookup is `lookup_failed` with reason `too_large` (over the edge's size cap) gets 422 `page_too_large_for_edge` and is not charged: `details.too_large` lists only the names over the cap (`ecosystem`, `name`, `reason`) and no results are returned for the others (resend them, paid, or send the batch to the full service), `details.note` starts "not charged: edge size limit", and `details.full_service` names the full service, which reads the whole answer. See [Payments](/docs/payments). --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/docs/attest.md # Attest a hash Last updated: 2026-10-10 `POST /v1/attest` signs a timestamped receipt over a SHA-256 you supply. Price: US$0.002 per call. Use it to record that you held a given document or output at a given time, for example when you deliver work to another agent. We never see the document, only its hash. ## Request ```json { "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "note": "delivery of report v3 to order 77" } ``` With an x402 payment you can add `"idempotency_key": "order-77-delivery"` so a retry gets the same receipt. Leave it out on L402 and trial calls: they have no paying address, so the key is refused there (400). | Field | Required | Meaning | |---|---|---| | `sha256` | yes | 64 hex characters | | `note` | no | up to 280 characters; only its SHA-256 goes into the receipt and our records | | `idempotency_key` | no, x402 only | 1 to 128 printable ASCII characters without spaces; a retry with the same key, hash and note gets the same receipt | ## Response From the free sample (`GET /v1/sample/attest`), shortened: ```json { "attestation": { "sha256": "e3b0c442…", "note_sha256": "…", "seq": 1, "issued_at": "2026-10-09T14:20:14.636Z", "idempotency_key": null, "idempotency_key_sha256": null, "first_seen": true }, "receipt": "eyJhbGciOiJFZERTQSIs…", "tier": "paid" } ``` - `seq` is our running count of attestations. - `idempotency_key` and `idempotency_key_sha256` are null when no key was sent, as in the sample. - `first_seen` is false when the same key returns an earlier receipt. - Reusing an `idempotency_key` with a different hash or note gets 409 `idempotency_conflict`, not charged. The payment is verified first, then released without being taken. - We keep only the SHA-256 of your key. The receipt carries `idempotency_key_sha256`, and the live answer also echoes the key you sent. - If the payment fails to settle (502 `settle_failed`), the receipt was already made and is kept. A retry with the same key and hash gets it (`first_seen: false`) and pays once. Its `payment` block names the payer and price but is not a record that a payment settled: the receipt attests the hash and the time, not the payment. To avoid paying twice when the failed payment did go through, retry with the same payment-identifier (see [payments](payments.md)). - Keys are scoped to the paying address, so two payers can use the same key without affecting each other. Trial calls and L402 calls have no paying address, so they cannot use `idempotency_key` (400, and an L402 token is not spent). ## What it shows The receipt shows that this SHA-256 was sent to us by the stated time. It does not show who made the document, what it contains, or that it was not made earlier. --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/docs/receipt-verify.md # Verify a receipt Last updated: 2026-10-10 `POST /v1/receipt/verify` checks an AttestPage receipt against this service's published keys and returns its decoded payload. Free. If you would rather not trust our server to check our own receipts, use the [offline checker](/docs/verify-offline) instead. ## Request ```json { "receipt": "eyJhbGciOiJFZERTQSIs…" } ``` `receipt` is the compact JWS from the `receipt` field of any response, at most 163,840 characters (160 KB). A 10-URL `check/links` or `verify/citations` receipt with long URLs and every network-evidence field at its cap is about 128 KB. The evidence fields are capped in bytes, so a site whose certificate names or headers use non-ASCII or escaped characters cannot make it bigger. The request body may be up to 168 KB, here and on `/mcp`. ## Response ```json { "valid": true, "kid": "https://vehcdj664efetfrsolne5umanq.srv.us/.well-known/jwks.json#ap-1a2b3c4d5e6f7a8b", "tier": "paid", "payload": { "v": 1, "iss": "https://vehcdj664efetfrsolne5umanq.srv.us", "kind": "quote", "…": "…" } } ``` When the receipt does not check out, `valid` is false and `reason` and `message` say why. Reasons include `malformed`, `bad_alg`, `bad_signature`, `unknown_kid`, `bad_iss` and `tier_key_mismatch`. `tier` is `paid`, `trial` or `sample`. Sample receipts are signed with a separate sample key (kid ending `#sample`) and are not evidence about any third-party page. ## Errors 400 when the body is not `{"receipt": ""}` or the receipt is too long; 503 `signing_unavailable` when keys are not loaded. --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/docs/samples.md # Samples Last updated: 2026-10-10 Each paid route has a free sample that runs the route's real code on a fixed demo page (for packages, a built-in demo copy of registry data) and returns the full response, signed with the sample key. Samples make no outbound fetch, cost nothing and need no payment. | Sample | Route it shows | Demo input | |---|---|---| | `GET /v1/sample/quote` | `POST /v1/verify/quote` | the quote "emissions fell 12% in 2025", fuzzy match | | `GET /v1/sample/quote?match=passages` | `POST /v1/verify/quote` with `"match": "passages"` | the paraphrased claim "The agency reported a 12 percent drop in emissions during 2025": the 3 passages that share most of its words | | `GET /v1/sample/citations` | `POST /v1/verify/citations` | three citations: one misformatted (fuzzy), one made up (none) and one on a missing page (404) | | `GET /v1/sample/document` | `POST /v1/verify/document` | the citations sample's three pairs, written as a short document | | `GET /v1/sample/fetch` | `POST /v1/fetch` | the demo page, with text | | `GET /v1/sample/links` | `POST /v1/check/links` | the demo page and a missing page (404) | | `GET /v1/sample/packages` | `POST /v1/check/packages` | left-pad (npm), requests (PyPI), serde (crates.io) and a missing npm name, from a built-in demo copy of registry data | | `GET /v1/sample/attest` | `POST /v1/attest` | the SHA-256 of the demo page | | `GET /v1/sample/quotes` | `POST /v1/verify/quotes` | three quotes on the demo page: one exact, one misformatted (fuzzy) and one made up (none) | The demo page itself is at [/v1/sample/page](https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/page). Its text is made up. ## What a sample returns The same fields as the paid route, plus: - `sample: true` and a `note` - `input`: the demo request body used - `tier: "sample"`, `rail: null`, `network: null` The receipt is signed with the sample key, whose kid ends in `#sample`. [POST /v1/receipt/verify](/docs/receipt-verify) and the [offline checker](/docs/verify-offline) accept it and report tier `sample`. A sample receipt says nothing about any page other than the demo page. ## Try it ```sh curl https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/quote curl "https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/quote?match=passages" ``` `match` is read only by the quote sample: `fuzzy` (the default) or `passages`; any other value gets 400. --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/docs/receipt-demo.md # Receipt demo Last updated: 2026-10-10 A signed receipt from start to finish, using only free calls: get a receipt from a free sample, check it with `POST /v1/receipt/verify`, see a changed receipt fail, then check it offline. Step 5 adds the free trial on a real route. No payment, wallet or account is needed. The commands use `curl` and `jq`. ## 1. Get a receipt `GET /v1/sample/fetch` runs the real [fetch](/docs/fetch) code on the [demo page](https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/page) and signs the result with the sample key. ```sh curl -s https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/fetch > sample.json jq '{tier, page: .page.http_status, kind: .page.content_kind, sha: .page.content_sha256}' sample.json jq -r .receipt sample.json > receipt.txt ``` The response has the page result and a `receipt` field: a compact JWS (`header.payload.signature`). The receipt records the URL, final URL, status, content_kind, page hashes, time of the fetch and a hash of the request. ## 2. Check it ```sh jq -n --rawfile r receipt.txt '{receipt: ($r | rtrimstr("\n"))}' > body.json curl -s -X POST https://vehcdj664efetfrsolne5umanq.srv.us/v1/receipt/verify -H 'Content-Type: application/json' -d @body.json ``` The answer: ```json { "valid": true, "kid": "https://vehcdj664efetfrsolne5umanq.srv.us/.well-known/jwks.json#sample", "tier": "sample", "payload": { "v": 1, "iss": "https://vehcdj664efetfrsolne5umanq.srv.us", "kind": "fetch", "tier": "sample", "url": "https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/page", "http_status": 200, "content_kind": "real", "content_sha256": "0e429607…", "payment": null, "…": "…" } } ``` `payload.content_sha256` matches `page.content_sha256` from step 1. ## 3. Change one character ```sh jq -n --rawfile r receipt.txt '{receipt: ($r | rtrimstr("\n") | .[0:-2] + (if .[-2:-1] == "A" then "B" else "A" end) + .[-1:])}' > bad.json curl -s -X POST https://vehcdj664efetfrsolne5umanq.srv.us/v1/receipt/verify -H 'Content-Type: application/json' -d @bad.json ``` The answer has `"valid": false` and `"reason": "bad_signature"`. ## 4. Check it offline The [offline checker](/docs/verify-offline) is one Node.js file. It checks the receipt against the published keys at [/.well-known/jwks.json](https://vehcdj664efetfrsolne5umanq.srv.us/.well-known/jwks.json) without calling the verify route. ```sh curl -sO https://vehcdj664efetfrsolne5umanq.srv.us/dl/verify-receipt.mjs node verify-receipt.mjs "$(cat receipt.txt)" --issuer https://still-rapids-9yt7.here.now ``` ## 5. Try a real route For a receipt about a URL of your choice, call a paid route such as [fetch](/docs/fetch). 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/quotes or check/links covers at most 3 citations, quotes or URLs; a larger batch is not run as a trial but answered with the normal 402 offer, `details.reason` trial_too_large. A sample receipt is signed with the sample key and says nothing about any page other than the demo page. Paid and trial receipts cover the URL you asked for. See [Samples](/docs/samples), [Verify a receipt](/docs/receipt-verify) and [Payments](/docs/payments). --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/demo.md # Demo: one paid call and its signed receipt Last updated: 2026-10-10 One call to `POST /v1/verify/quote`, recorded in full: the request, the 402 payment offer, 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 is signed with a demo key, published in step 5, so every check on this page works offline. Receipts from https://vehcdj664efetfrsolne5umanq.srv.us are signed with the keys at [https://still-rapids-9yt7.here.now/.well-known/jwks.json](https://still-rapids-9yt7.here.now/.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: ```http 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 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)." } } ``` Decoded, without the route description and the extensions: ```json { "x402Version": 2, "error": "Payment required", "resource": { "url": "https://attestpage.example/v1/verify/quote" }, "accepts": [ { "scheme": "exact", "network": "eip155:84532", "amount": "10000", "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", "payTo": "0x08dE2e056F6BE74162c400fF877449593579D1EA", "maxTimeoutSeconds": 120, "extra": { "name": "USDC", "version": "2" } } ] } ``` `amount` is in USDC base units (6 decimals), so `10000` is US$0.01, on Base Sepolia (`eip155:84532`, a test network). Prices for every route: [pricing.json](https://vehcdj664efetfrsolne5umanq.srv.us/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: ```json { "x402Version": 2, "resource": { "url": "https://attestpage.example/v1/verify/quote" }, "accepted": { "scheme": "exact", "network": "eip155:84532", "amount": "10000", "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", "payTo": "0x08dE2e056F6BE74162c400fF877449593579D1EA", "maxTimeoutSeconds": 120, "extra": { "name": "USDC", "version": "2" } }, "payload": { "authorization": { "from": "0x94310DbA0d34ec79FE88E7c361FEd4d97Ba253D3", "to": "0x08dE2e056F6BE74162c400fF877449593579D1EA", "value": "10000", "validAfter": "0", "validBefore": "1791641445", "nonce": "0xf6a01de3ed3452c574551ca36e5da1be0a049a2dd8fef948e005363869e3df31" }, "signature": "0x85d7070591…0a1b" } } ``` Any x402 v2 client does this step. [One x402 payment step by step](/docs/examples/fetch-x402) shows it with plain `fetch`, and the [MCP server](/docs/mcp) 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: ```json { "success": true, "payer": "0x94310DbA0d34ec79FE88E7c361FEd4d97Ba253D3", "network": "eip155:84532", "amount": "10000" } ``` The body: ```json { "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-10T14:08:45.771Z", "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": "Sat, 10 Oct 2026 14:08:45 GMT", "last-modified": "Sat, 10 Oct 2026 13:40:39 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": "eyJhbGciOiJFZERTQSIsImtpZCI6Imh0dHBzOi8vYXR0ZXN0cGFnZS5leGFtcGxlLy53ZWxsLWtub3duL2p3a3MuanNvbiNhcC1XZDRDU1FwNzFFR2hUU1RtIiwidHlwIjoiYXR0ZXN0cGFnZS1ldmlkZW5jZStqd3MifQ.eyJjb250ZW50X2tpbmQiOiJyZWFsIiwiY29udGVudF9zaGEyNTYiOiI2YTdjY2EwNDNjNjJkNWQxM2Y3ZDRjNmYyZWJhMjgzOWM2NDQ2YzA2YjVjN2JlNmNiMjlkNzdhZDZhYjgwZTQ2IiwiZmluYWxfdXJsIjoiaHR0cHM6Ly93d3cuaWFuYS5vcmcvaGVscC9leGFtcGxlLWRvbWFpbnMiLCJoZWFkZXJzIjp7ImNvbnRlbnQtbGVuZ3RoIjoiMTc2MiIsImNvbnRlbnQtdHlwZSI6InRleHQvaHRtbDsgY2hhcnNldD11dGYtOCIsImRhdGUiOiJTYXQsIDEwIE9jdCAyMDI2IDE0OjA4OjQ1IEdNVCIsImxhc3QtbW9kaWZpZWQiOiJTYXQsIDEwIE9jdCAyMDI2IDEzOjQwOjM5IEdNVCJ9LCJodHRwX3N0YXR1cyI6MjAwLCJpYXQiOjE3OTE2NDEzMjUsImlzcyI6Imh0dHBzOi8vYXR0ZXN0cGFnZS5leGFtcGxlIiwia2luZCI6InF1b3RlIiwicGF5bWVudCI6eyJhbW91bnQiOiIxMDAwMCIsImFzc2V0IjoiMHgwMzZDYkQ1Mzg0MmM1NDI2NjM0ZTc5Mjk1NDFlQzIzMThmM2RDRjdlIiwibmV0d29yayI6ImVpcDE1NTo4NDUzMiIsInBheWVyIjoiMHg5NDMxMERiQTBkMzRlYzc5RkU4OEU3YzM2MUZFZDRkOTdCYTI1M0QzIiwicmFpbCI6Ing0MDIifSwicXVvdGVfc2hhMjU2IjoiZGYzNGJlMGZjZWFiNTZmMjY5NmRkMDgxNjk0YjVjOTI0Y2JkZGE1MjA2NDdhMThjZWJjMDFmNWQ3ZTIyMzY0ZiIsInJhd19zaGEyNTYiOiJlZWI5NDExNWZlNjVjNzcxZjAxYTBjYzA3MDFjZGVkMjc2YTc1Y2FiM2MwYmI3MWNiZjM0ZGFmODAyY2U0YTQ0IiwicmVxdWVzdF9zaGEyNTYiOiI5MTM1N2I3MDE0YzE1ZDcyYjIxOGVkMmE1NWY3YjM4ZTg1NmRmMjZmMTk4N2ViNjhlYTg5MjVhODkxNmI5MWVhIiwicmVzdWx0Ijp7ImNhc2Vfc2Vuc2l0aXZlIjpmYWxzZSwiZW5kIjoyMzQsIm1hdGNoIjoiZXhhY3QiLCJtb2RlIjoiZnV6enkiLCJub3JtYWxpc2VkX3NoYTI1NiI6Ijc1YTkzYjZkOWIxN2ExMWJiMjI3N2RkZTFkZTBlNTdhYzM1Nzk2NzA1MmQ0YWUzZTRkNjYwODU2ZmYyMmU4MWQiLCJvY2N1cnJlbmNlcyI6MSwic2NvcmUiOjEsInN0YXJ0IjoxNzF9LCJyZXRyaWV2ZWRfYXQiOiIyMDI2LTEwLTEwVDE0OjA4OjQ1Ljc3MVoiLCJzZXJ2ZXJfaXAiOiIxMDQuMTguMjQuMjMyIiwidGllciI6InBhaWQiLCJ0bHMiOnsiY2VydF9zaGEyNTYiOiJjYmYzYTIwMGE0MmUyYjZiNzUxZmI2YmViNmQxYjcyYTQ2NGEwYWI3ZmZlYmZjYTBmZmYwNzFhZjJjMTg1YjY0IiwiaXNzdWVyIjoiQz1VUywgTz1Hb29nbGUgVHJ1c3QgU2VydmljZXMsIENOPVdFMSIsInN1YmplY3QiOiJDTj13d3cuaWFuYS5vcmciLCJ2YWxpZF9mcm9tIjoiMjAyNi0wOC0yMVQyMDo1MTozMi4wMDBaIiwidmFsaWRfdG8iOiIyMDI2LTExLTE5VDIxOjUxOjMwLjAwMFoifSwidXJsIjoiaHR0cHM6Ly93d3cuaWFuYS5vcmcvaGVscC9leGFtcGxlLWRvbWFpbnMiLCJ2IjoxfQ.8-fl77yE3sNKc3N6fxl66PqRc_suw221_4GXBVxHxmGB4bn2cwPZuqgeLetjjxTMGCSU4rqOhy3vbI3xd5UqBw", "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](/docs/how-it-works). ## 5. Check the receipt signature The receipt's header names the signing key: ```json { "alg": "EdDSA", "kid": "https://attestpage.example/.well-known/jwks.json#ap-Wd4CSQp71EGhTSTm", "typ": "attestpage-evidence+jws" } ``` The key is published in the issuer's JWKS at `/.well-known/jwks.json`. For this example it is: ```json { "keys": [ { "kty": "OKP", "crv": "Ed25519", "x": "C8UR8Mb1iCu0SutA6j0Zn5yG965ifppI3A0kVqcsy-M", "kid": "https://attestpage.example/.well-known/jwks.json#ap-Wd4CSQp71EGhTSTm", "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`: ```js 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); ``` ```sh node check.mjs receipt.txt jwks.json ``` ```text 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 sample 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://vehcdj664efetfrsolne5umanq.srv.us, use the issuer's keys: ```sh curl -s https://still-rapids-9yt7.here.now/.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](/docs/receipt-verify)), or use our single-file checker, which also checks the issuer and the receipt fields: [Verify receipts offline](/docs/verify-offline). To make the call yourself, step by step with `curl`: [How to check a quote an LLM cites](/guides/verify-llm-quote). --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/docs/verify-offline.md # Verify receipts offline Last updated: 2026-10-10 You do not need to trust our server to check a receipt. A single-file checker (Node.js 20 or later, no dependencies) verifies the Ed25519 signature and the payload against our public keys. ## Get the checker ```sh curl -O https://vehcdj664efetfrsolne5umanq.srv.us/dl/verify-receipt.mjs node verify-receipt.mjs --selftest ``` [https://vehcdj664efetfrsolne5umanq.srv.us/dl/index.json](https://vehcdj664efetfrsolne5umanq.srv.us/dl/index.json) lists the checker (`verify-receipt`) with its URL, size and sha256, so you can check the file you downloaded. ## Check a receipt ```sh node verify-receipt.mjs '' --issuer https://still-rapids-9yt7.here.now ``` This fetches `https://still-rapids-9yt7.here.now/.well-known/jwks.json` and checks that the receipt's `iss` and key id belong to that issuer. The issuer is a fixed address that does not change when the service moves, so a receipt keeps verifying after the service address changes. Always pin `--issuer` (or a key file you trust): without it, the key location comes from the receipt itself. To work fully offline, save the keys once and pass the file: ```sh curl -o jwks.json https://still-rapids-9yt7.here.now/.well-known/jwks.json node verify-receipt.mjs '' --jwks jwks.json --issuer https://still-rapids-9yt7.here.now ``` Pass `-` instead of the receipt to read it from standard input. ## Output JSON `{valid, reason?, kid, tier, payload}`. Exit code 0 means valid, 1 invalid, 2 a usage error. ## Doing it yourself A receipt is a compact JWS: `header.payload.signature`, each base64url. Check that the header has `alg` `EdDSA` and `typ` `attestpage-evidence+jws`, find the key in the JWKS whose `kid` matches the header's `kid`, and verify the Ed25519 signature over `header.payload`. Then check that `iss` is the issuer you expect. Field meanings are in [How it works](/docs/how-it-works). --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/docs/mcp.md # MCP server Last updated: 2026-10-10 `quoteproof-mcp` is an MCP server (stdio) that gives an agent one tool per route. It needs Node.js 20.3 or later and has no install scripts. | Tool | Route | Price | |---|---|---| | `attestpage_verify_quote` | [POST /v1/verify/quote](/docs/verify-quote) | US$0.01 | | `attestpage_verify_quotes` | [POST /v1/verify/quotes](/docs/verify-quotes) | US$0.08 | | `attestpage_verify_citations` | [POST /v1/verify/citations](/docs/verify-citations) | US$0.10 | | `attestpage_fetch` | [POST /v1/fetch](/docs/fetch) | US$0.002 | | `attestpage_check_links` | [POST /v1/check/links](/docs/check-links) | US$0.005 | | `attestpage_check_packages` | [POST /v1/check/packages](/docs/check-packages) | US$0.005 | | `attestpage_attest` | [POST /v1/attest](/docs/attest) | US$0.002 | | `attestpage_verify_receipt` | [POST /v1/receipt/verify](/docs/receipt-verify) | free | ## Install ```sh npx -y https://vehcdj664efetfrsolne5umanq.srv.us/dl/quoteproof-mcp-0.1.25.tgz ``` [https://vehcdj664efetfrsolne5umanq.srv.us/dl/index.json](https://vehcdj664efetfrsolne5umanq.srv.us/dl/index.json) lists the current version with its sha256 and npm integrity string. `https://vehcdj664efetfrsolne5umanq.srv.us/dl/quoteproof-mcp.tgz` always redirects to the newest version. Most MCP clients take a block like this: ```json { "mcpServers": { "quoteproof": { "command": "npx", "args": ["-y", "https://vehcdj664efetfrsolne5umanq.srv.us/dl/quoteproof-mcp-0.1.25.tgz"], "env": { "QUOTEPROOF_URL": "https://vehcdj664efetfrsolne5umanq.srv.us", "QUOTEPROOF_X402_KEY": "0x..." } } } } ``` ## Paying There are two ways to pay: x402 (USDC, `QUOTEPROOF_X402_KEY`) and L402 (sats, `QUOTEPROOF_NWC_URL`). Set either or both. With `QUOTEPROOF_X402_KEY` set to a wallet private key, paid tools pay the 402 with x402 (see [Payments](/docs/payments)). The key stays in the process and is never logged. Before signing, the server checks the offer against local limits: `QUOTEPROOF_MAX_USD` per call (default 0.10; each tool also pays no more than its listed price), `QUOTEPROOF_SESSION_USD` per process (default 1) and `QUOTEPROOF_NETWORKS` (default `eip155:84532`, Base Sepolia; Base mainnet `eip155:8453` only when you list it). An offer over a limit, or with no amount, fails with `payment_refused_locally` and nothing is signed. A paid result carries the settlement in `_meta["x402/payment-response"]`. Without a key, paid tools use the free trial where it is on, and fail with `trial_exhausted` once it is used up. A trial call to `attestpage_verify_citations` or `attestpage_check_links` takes at most 3 items. Set `QUOTEPROOF_TRIAL=0` to turn the trial off. With `QUOTEPROOF_NWC_URL` set to a Nostr Wallet Connect connection string (Node.js 22 or later), paid tools pay in sats with L402 where the service offers it (see [Payments](https://vehcdj664efetfrsolne5umanq.srv.us/docs/payments.md)). Create a wallet connection just for this server that may only pay invoices, with a small daily budget. The connection string is a secret and is never logged or returned. Before paying, the server checks the invoice and its token against `QUOTEPROOF_MAX_SATS` per call (default 100; each tool also pays no more than its listed sats), `QUOTEPROOF_SESSION_SATS` per process (default 1000; routing fees are counted after each payment, so one payment can go over this by its fee; the wallet's own budget is the hard limit) and `QUOTEPROOF_LN_NETWORKS` (default `regtest,signet,testnet`; add `bitcoin` for real sats). L402 is paid before the call runs: if the call then fails, the token is kept in memory and pays your next call to the same tool. `attestpage_attest` with `idempotency_key` needs x402. When the service has too many unpaid invoices open for your address, its 402 has no invoice and the call fails with `payment_refused_locally`, saying Lightning is busy and how many seconds to wait. With only `QUOTEPROOF_NWC_URL` set, a call the service offers no L402 for uses the free trial, as with no wallet. With both a key and a wallet, `QUOTEPROOF_PAY_ORDER` (default `x402,l402`) picks the rail tried first. A paid result carries the payment in `_meta["attestpage/l402"]`. ## Remote endpoint The same tools are served over MCP at `POST https://vehcdj664efetfrsolne5umanq.srv.us/mcp` (streamable HTTP), with nothing to install. Each POST carries one JSON-RPC message and gets one JSON answer; there are no sessions and no event stream, so GET returns 405. A tool call runs the tool's route on this server, so prices, limits and refunds are the same as calling the route. Protocol versions: `2026-07-28` and the older `2025-11-25`, `2025-06-18`, `2025-03-26` and `2024-11-05`. A `2026-07-28` client skips `initialize`: it puts the version and its capabilities in each request's `_meta` (`io.modelcontextprotocol/protocolVersion`, `io.modelcontextprotocol/clientCapabilities`) and sends the `MCP-Protocol-Version`, `Mcp-Method` and (for `tools/call`) `Mcp-Name` headers, which must match the body (else 400 with error `-32020`, HeaderMismatch). `server/discover` returns the supported versions, capabilities and server identity; an unsupported version gets 400 with error `-32022` and the supported list. Results carry `resultType: "complete"` and `_meta["io.modelcontextprotocol/serverInfo"]`; `tools/list` adds `ttlMs` and `cacheScope`. Older clients use `initialize` as before. The server card (SEP-2127) is at [`GET https://vehcdj664efetfrsolne5umanq.srv.us/mcp/server-card`](https://vehcdj664efetfrsolne5umanq.srv.us/mcp/server-card) (`application/mcp-server-card+json`): name, version and the remote with its supported protocol versions; it lists no tools, so call `tools/list`. The site's [AI Catalog](https://vehcdj664efetfrsolne5umanq.srv.us/.well-known/ai-catalog.json) (`application/ai-catalog+json`) points at the card, the OpenAPI document and the agent skill. `/.well-known/mcp/server-card.json` redirects to the card. ```json { "mcpServers": { "attestpage": { "type": "http", "url": "https://vehcdj664efetfrsolne5umanq.srv.us/mcp" } } } ``` To pay, put an x402 payment payload in the call's `_meta["x402/payment"]`, as `@x402/mcp` clients do. Without one, a paid tool uses the free trial where it is on, and once that is used up, or for a `verify_citations` or `check_links` call with more than 3 items (the requirements' `error` then says so), it returns a result with `isError: true` whose `structuredContent` is the x402 payment requirements; sign one of `accepts` and call again with the payload. Set `_meta["attestpage/trial"]` to `false` to get the requirements straight away. A paid result carries the settlement in `_meta["x402/payment-response"]`. The free trial and the per-address limits count the address that calls `/mcp`. ## Errors A failed call is a tool result with `isError: true` whose text is JSON: `status`, `code`, `message` and, where useful, `details` and `hint`. The codes are the ones in [Errors](/docs/errors), plus `payment_refused_locally`, `lightning_payment_failed`, `lightning_payment_unknown`, `timeout` and `network_error` from the MCP server itself. A bad request is refused before payment and never charged. --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/docs/skill.md # Agent Skill Last updated: 2026-10-10 `attestpage-check-sources` is an Agent Skill: a `SKILL.md` file that tells an agent when to check a source with AttestPage (before it quotes a page, cites a URL or installs a package an LLM suggested), how to call the sample, the trial or the paid route, and how to read the verdict. Clients that read `SKILL.md` files, such as Claude, Codex and Cursor, load its name and description up front and the rest only when a task matches. ## Get it - The skill: [https://vehcdj664efetfrsolne5umanq.srv.us/dl/skills/attestpage-check-sources/SKILL.md](https://vehcdj664efetfrsolne5umanq.srv.us/dl/skills/attestpage-check-sources/SKILL.md) - The index: [https://vehcdj664efetfrsolne5umanq.srv.us/.well-known/agent-skills/index.json](https://vehcdj664efetfrsolne5umanq.srv.us/.well-known/agent-skills/index.json), in the format of the Agent Skills Discovery draft (v0.2.0). Each entry has `name`, `type` (`skill-md`), `description`, `url` and `digest` (`sha256:` and the SHA-256 of the file as served). Check the digest before you load the file. - [The download list](https://vehcdj664efetfrsolne5umanq.srv.us/dl/index.json) lists it too, with its size and sha256. ```sh npx skills add https://vehcdj664efetfrsolne5umanq.srv.us ``` The skill names the MCP tools (`attestpage_verify_quote` and the others) and the remote endpoint `POST https://vehcdj664efetfrsolne5umanq.srv.us/mcp`; see [MCP server](https://vehcdj664efetfrsolne5umanq.srv.us/docs/mcp.md). --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/docs/index.md # Docs Last updated: 2026-10-10 AttestPage returns signed evidence for AI agents: whether a quote or a whole set of citations appears on the cited pages, what our fetcher got back from a URL, whether links are live, whether package names exist on npm, PyPI or crates.io, and signed timestamps over hashes. Each page here is also markdown: add `.md` to its URL or send `Accept: text/markdown`. ## Routes - [Verify a quote](/docs/verify-quote): `POST /v1/verify/quote`, US$0.01 - [Verify quotes on one page](/docs/verify-quotes): `POST /v1/verify/quotes`, US$0.08 per call of up to 20 quotes - [Verify citations](/docs/verify-citations): `POST /v1/verify/citations`, US$0.10 per batch - [Verify a document](/docs/verify-document): `POST /v1/verify/document`, US$0.10 per part of up to 10 pairs, free preview - [Fetch a page](/docs/fetch): `POST /v1/fetch`, US$0.002 - [Check links](/docs/check-links): `POST /v1/check/links`, US$0.005 per batch - [Check packages](/docs/check-packages): `POST /v1/check/packages`, US$0.005 per batch - [Attest a hash](/docs/attest): `POST /v1/attest`, US$0.002 - [Verify a receipt](/docs/receipt-verify): `POST /v1/receipt/verify`, free - [Samples](/docs/samples): `GET /v1/sample/{name}`, free ## Guides - [Payments](/docs/payments): the x402 flow and when a call is charged - [How it works](/docs/how-it-works): the fetcher, content_kind values and receipts - [Receipt demo](/docs/receipt-demo): a signed receipt end to end, with free calls only - [Demo: one paid call and its signed receipt](/demo): the request, the 402 offer, the paid retry with x402 and the receipt, with a signature check in Node.js - [Verify receipts offline](/docs/verify-offline): the checker script and keys - [MCP server](/docs/mcp): one MCP tool per route, paying with x402 or the free trial - [Agent Skill](/docs/skill): a SKILL.md telling an agent when to check a source and how to read the verdict - [Example recipes](/docs/recipes): LangChain, LlamaIndex, OpenAI Agents SDK and Claude Agent SDK agents that call these routes on the trial or with x402 - [More examples](/docs/examples): runnable recipes, including one x402 payment step by step with plain `fetch` - [Limits](/docs/limits): caps, timeouts and rate limits - [Errors](/docs/errors): error codes and charging - [Status](/status): current state and uptime ([JSON](/status.json)) ## Answers - [How to verify a quote an LLM cites](/docs/verify-a-quote-an-llm-cites) - [How to check a quote an LLM cites, step by step with curl](/guides/verify-llm-quote): the free sample, the 402 offer, one check, the receipt, and the same call over MCP - [How to get a signed receipt of a web page](/docs/signed-receipt-of-a-web-page) - [How to get a signed receipt of a web page, step by step with curl](/guides/signed-page-receipt): the free sample, the 402 offer, one receipt, its signature and text hash, and the same call over MCP - [How to check links before citing them](/docs/check-links-before-citing) - [How to check links before citing them, step by step with curl](/guides/check-links-before-citing): the free sample, the 402 offer, one batch of links, the receipt, and the same call over MCP - [How to check a package name an LLM suggested before installing it](/docs/check-package-names-before-install) - [How to tell a bot wall from the real page when an agent fetches a URL](/docs/tell-a-bot-wall-from-the-real-page) ## Machine-readable - [OpenAPI 3.1](https://vehcdj664efetfrsolne5umanq.srv.us/openapi.json) - [pricing.json](https://vehcdj664efetfrsolne5umanq.srv.us/pricing.json) - [llms.txt](https://vehcdj664efetfrsolne5umanq.srv.us/llms.txt) and [llms-full.txt](https://vehcdj664efetfrsolne5umanq.srv.us/llms-full.txt) - [x402 discovery](https://vehcdj664efetfrsolne5umanq.srv.us/.well-known/x402) and [API catalog](https://vehcdj664efetfrsolne5umanq.srv.us/.well-known/api-catalog) --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/docs/payments.md # Payments Last updated: 2026-10-10 Paid routes use x402 version 2. You pay per call in USDC; there is no account, key or sign-up. ## Network | Item | Value | |---|---| | Protocol | x402 v2, scheme `exact` | | Network | Base Sepolia (`eip155:84532`), a test network | | Asset | USDC | | Facilitator | PayAI (`facilitator.payai.network`), with x402.org as the fallback | Test USDC on Base Sepolia has no value. ## Prices | Route | Price per call | |---|---| | `POST /v1/verify/quote` | US$0.01 | | `POST /v1/verify/quotes` | US$0.08 per call of 1 to 20 quotes on one page (less than one verify/quote call each from 9 quotes up) | | `POST /v1/verify/citations` | US$0.10 per batch | | `POST /v1/verify/document` | US$0.10 per part of up to 10 pairs | | `POST /v1/fetch` | US$0.002 | | `POST /v1/check/links` | US$0.005 per batch | | `POST /v1/check/packages` | US$0.005 per batch | | `POST /v1/attest` | US$0.002 | ## Flow 1. Send the request with no payment header. 2. If the request is valid, you get `402 Payment Required`. A request with an empty body (or `{}`) gets the same 402, so a directory or client can read the price before it has a real input; anything else that fails validation gets 400. The `PAYMENT-REQUIRED` header (base64 JSON) holds the offer: amount, asset, network, pay-to address and a description of the route. The body is a JSON error with code `payment_required`. 3. Sign the offer with your wallet and send the same request again with the signed payment in the `PAYMENT-SIGNATURE` header. 4. We verify the payment with the facilitator, run the route, and settle the payment only if the route answers with a status below 400. A settled call carries a `PAYMENT-RESPONSE` header. Any x402 v2 client library handles steps 2 and 3. The offer also carries Bazaar discovery data (input and output schemas). ## When you are charged - **Never charged:** a request that fails validation (400, before any 402; a paid retry is validated again before the payment is used), a blocked or disallowed URL, a host name that does not resolve, rate limits, refused payers, and any answer with status 400 or above. - **Charged:** any answer below 400. This includes results about the target page that you may not like: a 404, a bot wall, a timeout. That result is what the call checks. - **Retries:** if your payment carries a payment id (the x402 `payment-identifier` extension) and you resend the same request with the same id within one hour, you get the stored response again, marked `idempotent-replayed: true`, and are not charged twice. Reusing a payment id for a different request gets 409 `payment_id_conflict`. - **Settle failures:** if a call ends in 502 `settle_failed`, resend the same request with the same payment id. If the payment went through on chain after all, you get the withheld result, marked `payment-redelivered: true`, without paying again. If it did not, the same signed payment is settled at most once; a new signature for that id gets 409 `settle_pending` until the first one expires (see `Retry-After`). This needs a payment id: without one, a charge that went through is recorded for reconciliation, and the result is not sent. Redelivery is a full-service feature: the edge server cannot match a resend across its instances. Here the withheld result is kept in memory for an hour (and at least until its payment has expired), so a server restart also loses it; a resend of the same signed payment is then still never charged twice. ## Lightning (L402) Lightning (L402) is not enabled on this server. The sats prices in [pricing.json](https://vehcdj664efetfrsolne5umanq.srv.us/pricing.json) are for when it is. ## Prepaid credits (Bitcoin signet) Prepaid credit keys (Bitcoin signet) are not enabled on this server. Where they are on (full service only), this section explains how to buy and spend one. ## Free trial 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/quotes or check/links covers at most 3 citations, quotes or URLs; a larger batch is not run as a trial but answered with the normal 402 offer, `details.reason` trial_too_large. ## Sanctions screening Paying addresses are checked against a public sanctions list. A listed address gets 403 `payer_refused` and its payment is not settled. ## Receipts A paid receipt records the payment: rail, network, asset, amount, paying address and payment id (for L402: the payment hash and preimage instead of an address and id). See [How it works](/docs/how-it-works). --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/docs/recipes.md # Example recipes Last updated: 2026-10-10 Four short agents that check a source before citing it with AttestPage: LangChain and LlamaIndex in Python, the OpenAI Agents SDK and the Claude Agent SDK in JavaScript. Each runs on the free trial, returns the price of a call it cannot pay to the agent as data instead of failing, and (OpenAI Agents SDK, or the Claude Agent SDK through MCP) can pay per call with x402 on Base Sepolia, a test network. The same files, each with a README, are in the `examples/` folder of the source. For a step-by-step x402 payment with plain `fetch`, and clients that ask for the price once the trial is used up, see [More recipes](/docs/examples). Every recipe reads `ATTESTPAGE_URL`; set it to `https://vehcdj664efetfrsolne5umanq.srv.us`. ## How the clients call a route 1. Send the request. While the free trial is on (below), the clients add its header. 2. A 200 is the answer, with a signed `receipt`. Check it with the free [POST /v1/receipt/verify](/docs/receipt-verify). 3. A 429 with code `trial_exhausted` means the day's trial calls are used; it is not charged, and `Retry-After` gives the seconds until the trial resets. The clients return it as `{ "error": "trial_exhausted", "status": 429, … }`. 4. A 402 means the call needs payment: the trial header was not sent, or a trial batch is over 3 items. The offer is in the `PAYMENT-REQUIRED` header (base64 JSON, USDC amounts with 6 decimals). The clients return `{ "payment_required": true, "price_usd": …, "network": "eip155:84532" }`; an x402 client signs the offer instead (see [Payments](/docs/payments)). 5. A 400 is bad input and is never charged. See [Errors](/docs/errors). 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/quotes or check/links covers at most 3 citations, quotes or URLs; a larger batch is not run as a trial but answered with the normal 402 offer, `details.reason` trial_too_large. ```sh curl -s https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/quote # free: what a verify/quote answer looks like curl -si -X POST https://vehcdj664efetfrsolne5umanq.srv.us/v1/verify/quote -H 'content-type: application/json' -d '{}' | grep -i payment-required # the 402 offer ``` ## Several quotes in one call The recipes call `POST /v1/verify/quote` once per quote. Two batch routes check several quotes in one call, under one signed receipt: - [POST /v1/verify/quotes](/docs/verify-quotes): up to 20 quotes on one page, US$0.08 per call. The page is fetched once; from 9 quotes up it costs less than one verify/quote call per quote. - [POST /v1/verify/citations](/docs/verify-citations): up to 10 quote and URL pairs, US$0.10 per batch, each page fetched once. A cited paper with a DOI also gets its Crossref data and a retracted flag. ## LangChain (Python) `pip install "langchain>=1.0" langchain-anthropic`, save the client below as `attestpage.py` next to it, then `python agent.py`. ```python """LangChain agent that checks its sources with AttestPage before it cites them.""" import os import sys from langchain.agents import create_agent from langchain.tools import tool import attestpage @tool def verify_quote(url: str, quote: str) -> dict: """Use before citing a page: does this exact or near-exact quote appear on the page at url? Returns match (exact, fuzzy or none), the page's content_kind and a signed receipt.""" return attestpage.verify_quote(url, quote) @tool def check_links(urls: list[str]) -> dict: """Use before putting links in an answer: status, final URL and content_kind for up to 3 URLs on the trial.""" return attestpage.check_links(urls) agent = create_agent( model=os.environ.get("MODEL", "anthropic:claude-sonnet-5-5"), tools=[verify_quote, check_links], system_prompt="Before you quote a web page, call verify_quote. Only cite a quote whose match is exact or fuzzy " "on a page whose content_kind is real. If a tool returns payment_required or the error trial_exhausted, say so and stop.", ) if __name__ == "__main__": question = " ".join(sys.argv[1:]) or ( 'Does https://www.iana.org/help/example-domains say "These domains may be used as illustrative examples in documents"?' ) result = agent.invoke({"messages": [{"role": "user", "content": question}]}) print(result["messages"][-1].content) ``` ## LlamaIndex (Python) `pip install "llama-index-core>=0.12" llama-index-llms-anthropic`, with the same `attestpage.py`. ```python """LlamaIndex agent that checks its sources with AttestPage before it cites them.""" import asyncio import os import sys from llama_index.core.agent.workflow import FunctionAgent from llama_index.core.tools import FunctionTool from llama_index.llms.anthropic import Anthropic import attestpage def verify_quote(url: str, quote: str) -> dict: """Use before citing a page: does this exact or near-exact quote appear on the page at url? Returns match (exact, fuzzy or none), the page's content_kind and a signed receipt.""" return attestpage.verify_quote(url, quote) def fetch_page(url: str) -> dict: """Use when you need a page's clean text and a verdict on what came back (real, bot_wall, js_shell, paywall, http_error).""" return attestpage.fetch_page(url) agent = FunctionAgent( tools=[FunctionTool.from_defaults(fn=verify_quote), FunctionTool.from_defaults(fn=fetch_page)], llm=Anthropic(model=os.environ.get("MODEL", "claude-sonnet-5-5")), system_prompt="Before you quote a web page, call verify_quote. Only cite a quote whose match is exact or fuzzy " "on a page whose content_kind is real. If a tool returns payment_required or the error trial_exhausted, say so and stop.", ) async def main(question): print(await agent.run(user_msg=question)) if __name__ == "__main__": asyncio.run(main(" ".join(sys.argv[1:]) or ( 'Does https://www.iana.org/help/example-domains say "These domains may be used as illustrative examples in documents"?' ))) ``` ## The Python client Standard library only. `trial=False` skips the trial header. ```python """AttestPage client for agent tools (Python standard library only). Calls the free trial by default (header Quoteproof-Trial: 1, a few calls per IP a day). Once the day's trial calls are used, a trial call answers 429 trial_exhausted (not charged), returned as an error dict. Pass trial=False to skip the trial header. A paid route then answers 402: the tool returns the x402 offer (price, network) instead of raising, so the agent can say what a call would cost. ATTESTPAGE_URL is the AttestPage origin. """ import base64 import json import os import urllib.error import urllib.request BASE_URL = os.environ.get("ATTESTPAGE_URL", "https://vehcdj664efetfrsolne5umanq.srv.us").rstrip("/") def _offer(header): # PAYMENT-REQUIRED is base64 JSON; amounts are USDC atomic units (6 decimals). try: req = json.loads(base64.b64decode(header)) first = req["accepts"][0] return {"price_usd": int(first["amount"]) / 1_000_000, "network": first["network"], "scheme": first["scheme"]} except (ValueError, KeyError, IndexError, TypeError): return {} def call(path, body, trial=True, base_url=None, timeout=60): """POST body to path; returns the JSON answer, a payment_required dict, or an error dict.""" headers = {"content-type": "application/json", "accept": "application/json"} if trial: headers["quoteproof-trial"] = "1" req = urllib.request.Request((base_url or BASE_URL) + path, data=json.dumps(body).encode(), headers=headers, method="POST") try: with urllib.request.urlopen(req, timeout=timeout) as res: return json.loads(res.read()) except urllib.error.HTTPError as e: try: err = json.loads(e.read()).get("error", {}) except ValueError: err = {} if e.code == 402: return {"payment_required": True, **_offer(e.headers.get("payment-required", "")), "message": "This call needs an x402 payment; pay with an x402 v2 client (see /docs/payments)."} return {"error": err.get("code", "http_%d" % e.code), "status": e.code, "message": err.get("message", "")} def verify_quote(url, quote, match="fuzzy", **kw): return call("/v1/verify/quote", {"url": url, "quote": quote, "match": match}, **kw) def check_links(urls, **kw): return call("/v1/check/links", {"urls": list(urls)}, **kw) def fetch_page(url, return_text=True, **kw): return call("/v1/fetch", {"url": url, "return_text": return_text}, **kw) def verify_receipt(receipt, **kw): return call("/v1/receipt/verify", {"receipt": receipt}, trial=False, **kw) ``` ## OpenAI Agents SDK (JavaScript) `npm install @openai/agents zod`, save the client below as `attestpage.mjs`, then `node agent.mjs`. ```js // OpenAI Agents SDK agent that checks its sources with AttestPage before it cites them. import { Agent, run, tool } from '@openai/agents'; import { z } from 'zod'; import { createClient } from './attestpage.mjs'; const attestpage = await createClient(); const verifyQuote = tool({ name: 'verify_quote', description: 'Use before citing a page: does this exact or near-exact quote appear on the page at url? ' + 'Returns match (exact, fuzzy or none), the page content_kind and a signed receipt.', parameters: z.object({ url: z.string(), quote: z.string() }), execute: async ({ url, quote }) => attestpage.verifyQuote({ url, quote }), }); const checkLinks = tool({ name: 'check_links', description: 'Use before putting links in an answer: status, final URL and content_kind for each URL (up to 3 on the trial, 10 paid).', parameters: z.object({ urls: z.array(z.string()).min(1).max(10) }), execute: async ({ urls }) => attestpage.checkLinks({ urls }), }); const agent = new Agent({ name: 'Source checker', instructions: 'Before you quote a web page, call verify_quote. Only cite a quote whose match is exact or fuzzy ' + 'on a page whose content_kind is real. If a tool returns payment_required or the error trial_exhausted, say so and stop.', tools: [verifyQuote, checkLinks], }); const question = process.argv.slice(2).join(' ') || 'Does https://www.iana.org/help/example-domains say "These domains may be used as illustrative examples in documents"?'; const result = await run(agent, question); console.log(result.finalOutput); ``` ## The JavaScript client To pay with x402, `npm install @x402/fetch @x402/evm viem` and set `ATTESTPAGE_X402_KEY` to the key of a Base Sepolia wallet made only for testing, holding test USDC. ```js // AttestPage client for agent tools. With no key it calls the free trial (header Quoteproof-Trial: 1, // a few calls per IP a day). With ATTESTPAGE_X402_KEY (a 0x… key for a Base Sepolia test wallet holding // test USDC) it pays each call with x402 through @x402/fetch. A 402 the client cannot pay comes back as // { payment_required, price_usd, network } instead of an exception, so the agent can say what a call costs. // ATTESTPAGE_URL is the AttestPage origin. const DEFAULT_URL = 'https://vehcdj664efetfrsolne5umanq.srv.us'; // A fetch that pays x402 offers from key. The x402 packages load only when a key is set. export async function payingFetch(key) { const [{ wrapFetchWithPayment, x402Client }, { registerExactEvmScheme }, { privateKeyToAccount }] = await Promise.all([ import('@x402/fetch'), import('@x402/evm/exact/client'), import('viem/accounts'), ]); const client = new x402Client(); registerExactEvmScheme(client, { signer: privateKeyToAccount(key) }); return wrapFetchWithPayment(fetch, client); } // PAYMENT-REQUIRED is base64 JSON; amounts are USDC atomic units (6 decimals). function offer(header) { try { const first = JSON.parse(Buffer.from(header, 'base64').toString('utf8')).accepts[0]; return { price_usd: Number(first.amount) / 1e6, network: first.network, scheme: first.scheme }; } catch { return {}; } } // fetch: a fetch to use instead (for example one from payingFetch); trial: send the trial header // (default: only when the client does not pay). export async function createClient({ url = process.env.ATTESTPAGE_URL || DEFAULT_URL, key = process.env.ATTESTPAGE_X402_KEY, fetch: fetchFn, trial } = {}) { const paying = Boolean(fetchFn || key); fetchFn ??= key ? await payingFetch(key) : fetch; trial ??= !paying; const base = url.replace(/\/+$/, ''); async function call(path, body, { useTrial = trial } = {}) { const headers = { 'content-type': 'application/json', accept: 'application/json' }; if (useTrial) headers['quoteproof-trial'] = '1'; const res = await fetchFn(`${base}${path}`, { method: 'POST', headers, body: JSON.stringify(body) }); const json = await res.json().catch(() => ({})); if (res.ok) return json; if (res.status === 402) { return { payment_required: true, ...offer(res.headers.get('payment-required') ?? ''), message: 'This call needs an x402 payment; set ATTESTPAGE_X402_KEY to pay (see /docs/payments).' }; } return { error: json.error?.code ?? `http_${res.status}`, status: res.status, message: json.error?.message ?? '' }; } return { verifyQuote: ({ url, quote, match = 'fuzzy' }) => call('/v1/verify/quote', { url, quote, match }), checkLinks: ({ urls }) => call('/v1/check/links', { urls }), fetchPage: ({ url, return_text = true }) => call('/v1/fetch', { url, return_text }), verifyReceipt: ({ receipt }) => call('/v1/receipt/verify', { receipt }, { useTrial: false }), }; } ``` ## Claude Agent SDK (JavaScript) `npm install @anthropic-ai/claude-agent-sdk`, then `node agent.mjs`. It connects the remote MCP endpoint `POST https://vehcdj664efetfrsolne5umanq.srv.us/mcp`, so there is no client code; paid tools use the free trial. A call that needs payment returns the x402 payment requirements. To pay from the agent's own key, use the stdio server instead (see [MCP server](/docs/mcp)). ```js // Claude Agent SDK agent that checks its sources with AttestPage's remote MCP endpoint (POST /mcp). // Without a payment the paid tools run on the free trial. A call that needs payment returns the x402 payment requirements. import { query } from '@anthropic-ai/claude-agent-sdk'; const url = (process.env.ATTESTPAGE_URL || 'https://vehcdj664efetfrsolne5umanq.srv.us').replace(/\/+$/, ''); // Tools from an MCP server are named mcp____. const TOOLS = ['attestpage_verify_quote', 'attestpage_check_links', 'attestpage_verify_receipt']; const prompt = process.argv.slice(2).join(' ') || 'Does https://www.iana.org/help/example-domains say "These domains may be used as illustrative examples in documents"?'; for await (const message of query({ prompt, options: { mcpServers: { attestpage: { type: 'http', url: `${url}/mcp` } }, allowedTools: TOOLS.map((t) => `mcp__attestpage__${t}`), systemPrompt: 'Before you quote a web page, call attestpage_verify_quote. Only cite a quote whose match is exact or fuzzy ' + 'on a page whose content_kind is real. If a tool returns payment requirements, say so and stop.', maxTurns: 6, }, })) { if (message.type === 'result') console.log(message.subtype === 'success' ? message.result : `stopped: ${message.subtype}`); } ``` --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/docs/examples/index.md # AttestPage recipes Last updated: 2026-10-10 Short, runnable recipes for calling AttestPage from an agent. In each one, the agent checks a source before it cites it. Copy one, run it, then change the tools to fit your agent. | Recipe | Runnable file | Framework | Language | How it calls AttestPage | Pays with x402 | |---|---|---|---|---|---| | [langchain.md](/docs/examples/langchain) | [langchain.mjs](/docs/examples/langchain.mjs) | LangChain (`create_agent`, `createAgent`) | Python and JavaScript | Python: a small client using only the standard library. JavaScript: the client from the OpenAI Agents SDK recipe | Python: no, free trial, then returns the price. JavaScript: yes, when a key is set | | [openai-agents-sdk.md](/docs/examples/openai-agents-sdk) | [openai-agents-sdk.mjs](/docs/examples/openai-agents-sdk.mjs) | OpenAI Agents SDK | JavaScript | A small client over `fetch` | Yes, with `@x402/fetch` when a key is set | | [claude-agent-sdk.md](/docs/examples/claude-agent-sdk) | [claude-agent-sdk.mjs](/docs/examples/claude-agent-sdk.mjs) | Claude Agent SDK | JavaScript | The `quoteproof-mcp` MCP server (stdio) | Yes, when `QUOTEPROOF_X402_KEY` is set | | [fetch-x402.md](/docs/examples/fetch-x402) | [fetch-x402.mjs](/docs/examples/fetch-x402.mjs) | None | JavaScript | Plain `fetch`, one x402 payment step by step | Yes | ## Run a recipe Each page shows its code in full, with what to install and the command that runs it. The LangChain (JavaScript) and OpenAI Agents SDK recipes share one client, [`attestpage.mjs`](/docs/examples/attestpage.mjs), shown on the OpenAI Agents SDK page. - Plain fetch with x402 always pays, so it needs `ATTESTPAGE_X402_KEY`, the key of a wallet made only for testing. - OpenAI Agents SDK runs on the free trial with no wallet key; it needs `OPENAI_API_KEY` for the model. - LangChain (JavaScript) and Claude Agent SDK run on the free trial with no wallet key; they need `ANTHROPIC_API_KEY` for the model. ## Service address ``` https://vehcdj664efetfrsolne5umanq.srv.us ``` Every recipe uses this address by default. To call another address, set `ATTESTPAGE_URL` (the MCP server reads `QUOTEPROOF_URL`). ## Routes and prices Prices are per call in USDC. The full list is at [`/pricing.json`](https://vehcdj664efetfrsolne5umanq.srv.us/pricing.json) and in the [OpenAPI document](https://vehcdj664efetfrsolne5umanq.srv.us/openapi.json). | Route | What it answers | Price | |---|---|---| | `POST /v1/verify/quote` | Is this quote on the page at this URL: `exact`, `fuzzy` or `none`, with offsets and page hashes | US$0.01 | | `POST /v1/verify/citations` | The same for up to 10 quote and URL pairs | US$0.10 per batch | | `POST /v1/verify/quotes` | The same for up to 20 quotes on one page, fetched once | US$0.08 per batch | | `POST /v1/verify/document` | Every quote with its link in a finished document, checked on the live page | US$0.10 per part of up to 10 pairs | | `POST /v1/verify/document/preview` | The pairs a document check would make, and its price | free | | `POST /v1/fetch` | A page as clean text, with a verdict on what came back (`real`, `bot_wall`, `js_shell`, `paywall`, `http_error` and others) | US$0.002 | | `POST /v1/check/links` | Status, redirects, final URL and verdict for up to 10 URLs | US$0.005 per batch | | `POST /v1/check/packages` | Whether up to 10 npm, PyPI or crates.io names exist, with latest version | US$0.005 per batch | | `POST /v1/attest` | A signed, timestamped receipt over a SHA-256 you send | US$0.002 | | `POST /v1/receipt/verify` | Checks a receipt's signature against the published keys | free | Every answer from a paid route carries a `receipt`: an Ed25519-signed JWS of what the fetcher saw and when. A receipt records what was on the page at that time, not whether a statement on it is correct. ## How a call is paid 1. **Free trial.** Send the header `Quoteproof-Trial: 1` and a paid route answers without payment, a few calls a day, with at most 3 URLs or pairs per batch. Once the day's trial calls are used, a trial request gets `429 trial_exhausted` and is not charged. See [Limits](https://vehcdj664efetfrsolne5umanq.srv.us/docs/limits). 2. **402 Payment Required.** A valid request gets `402` when it has no trial header or is a trial batch over 3 items. The offer is in the `PAYMENT-REQUIRED` header: base64 JSON with the amount (USDC, 6 decimals), network and pay-to address. 3. **x402.** Sign the offer and send the same request again with the `PAYMENT-SIGNATURE` header. Payment settles only when the route answers with a status below 400; the answer then carries a `PAYMENT-RESPONSE` header. Payments run on Base Sepolia (`eip155:84532`), a test network. Test USDC has no value. Use a wallet made only for testing, holding only test USDC. A bad request gets `400` before any offer and is never charged. Error codes are listed at [`/docs/errors`](https://vehcdj664efetfrsolne5umanq.srv.us/docs/errors). ## Try it without code ```sh # Free: what a verify/quote answer looks like curl -s https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/quote # The 402 offer for verify/quote, decoded curl -s -o /dev/null -D - -X POST https://vehcdj664efetfrsolne5umanq.srv.us/v1/verify/quote \ -H 'content-type: application/json' -d '{}' \ | grep -i '^payment-required:' | cut -d' ' -f2 | tr -d '\r' | base64 -d ``` ## More - [Payments](https://vehcdj664efetfrsolne5umanq.srv.us/docs/payments) - [MCP server](https://vehcdj664efetfrsolne5umanq.srv.us/docs/mcp) - [llms.txt](https://vehcdj664efetfrsolne5umanq.srv.us/llms.txt) --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/docs/examples/langchain.md # LangChain: check a quote before citing it Last updated: 2026-10-10 A LangChain agent with two AttestPage tools: `verify_quote` (US$0.01 a call) and `check_links` (US$0.005 per batch of up to 10 URLs). The client below uses only the Python standard library. It calls the free trial; when a call needs payment it returns the price to the agent instead of raising, so the agent can say what the call would cost. To pay per call, see [fetch-x402.md](/docs/examples/fetch-x402) or [claude-agent-sdk.md](/docs/examples/claude-agent-sdk). For JavaScript, see [LangChain in JavaScript](#langchain-in-javascript) below: its client can pay per call. ## Install Python 3.10 or later. ```sh pip install "langchain>=1.0" langchain-anthropic export ANTHROPIC_API_KEY=... ``` Any chat model LangChain supports works; set `MODEL` (for example `openai:gpt-5`) and install its package. ## attestpage.py ```python """AttestPage client for agent tools (Python standard library only). Sends the free-trial header (Quoteproof-Trial: 1). When the trial does not cover a call (a batch over 3 URLs gets 402; a trial used up for the day gets 429, and this asks again without it for the 402), this returns {"payment_required": True, "price_usd": ..., "network": ...} instead of raising. """ import base64 import json import os import urllib.error import urllib.request BASE_URL = os.environ.get("ATTESTPAGE_URL", "https://vehcdj664efetfrsolne5umanq.srv.us").rstrip("/") def _offer(header): # PAYMENT-REQUIRED is base64 JSON; amounts are USDC atomic units (6 decimals). try: first = json.loads(base64.b64decode(header))["accepts"][0] return {"price_usd": int(first["amount"]) / 1_000_000, "network": first["network"]} except (ValueError, KeyError, IndexError, TypeError): return {} def call(path, body, trial=True, timeout=60): """POST body to path; returns the JSON answer, a payment_required dict or an error dict.""" headers = {"content-type": "application/json", "accept": "application/json"} if trial: headers["quoteproof-trial"] = "1" req = urllib.request.Request(BASE_URL + path, data=json.dumps(body).encode(), headers=headers, method="POST") try: with urllib.request.urlopen(req, timeout=timeout) as res: return json.loads(res.read()) except urllib.error.HTTPError as e: try: err = json.loads(e.read()).get("error", {}) except ValueError: err = {} if e.code == 429 and trial and err.get("code") == "trial_exhausted": return call(path, body, trial=False, timeout=timeout) # ask again without the trial, for the price if e.code == 402: return {"payment_required": True, **_offer(e.headers.get("payment-required", "")), "message": "This call needs an x402 payment; see /docs/payments."} return {"error": err.get("code", "http_%d" % e.code), "status": e.code, "message": err.get("message", "")} def verify_quote(url, quote, match="fuzzy"): return call("/v1/verify/quote", {"url": url, "quote": quote, "match": match}) def check_links(urls): return call("/v1/check/links", {"urls": list(urls)}) def verify_receipt(receipt): return call("/v1/receipt/verify", {"receipt": receipt}, trial=False) ``` ## agent.py ```python """LangChain agent that checks a quote with AttestPage before it cites it.""" import os import sys from langchain.agents import create_agent from langchain.tools import tool import attestpage @tool def verify_quote(url: str, quote: str) -> dict: """Use before quoting a web page: is this exact or near-exact quote on the page at url? Returns result.match (exact, fuzzy or none), page.content_kind and a signed receipt.""" return attestpage.verify_quote(url, quote) @tool def check_links(urls: list[str]) -> dict: """Use before putting links in an answer: status, final URL and content_kind for each URL (up to 3 on the trial).""" return attestpage.check_links(urls) agent = create_agent( model=os.environ.get("MODEL", "anthropic:claude-sonnet-5-5"), tools=[verify_quote, check_links], system_prompt=( "Before you quote a web page, call verify_quote. Cite a quote only when result.match is exact or fuzzy " "and page.content_kind is real. If a tool returns payment_required, give the price and stop." ), ) if __name__ == "__main__": question = " ".join(sys.argv[1:]) or ( 'Does https://www.iana.org/help/example-domains say ' '"These domains may be used as illustrative examples in documents"?' ) result = agent.invoke({"messages": [{"role": "user", "content": question}]}) print(result["messages"][-1].content) ``` ## Run ```sh python agent.py python agent.py 'Is "Example Domain" the title of https://example.com?' ``` ## What comes back `verify_quote` returns the route's JSON. The parts an agent needs: - `result.match`: `exact`, `fuzzy` or `none`, with `result.score` from 0 to 1. - `page.content_kind`: read it before trusting `match`. A `none` on a `bot_wall` or `js_shell` page means the page could not be read, not that the quote is missing. - `receipt`: keep it with the answer. `attestpage.verify_receipt(receipt)` checks it for free. On a paraphrased claim, call `attestpage.verify_quote(url, claim, match="passages")`: it returns the 3 passages that share most of the claim's words, as evidence to read, not a verdict. ## LangChain in JavaScript The same agent in JavaScript. It uses the `attestpage.mjs` client from [openai-agents-sdk.md](/docs/examples/openai-agents-sdk): with no key it calls the free trial, and with `ATTESTPAGE_X402_KEY` set it pays each call with x402 on Base Sepolia, a test network, in test USDC. Node.js 20 or later. Save the client as `attestpage.mjs` next to `langchain.mjs`. ```sh npm init -y && npm pkg set type=module npm install langchain @langchain/core @langchain/anthropic zod npm install @x402/fetch @x402/evm viem # only needed to pay export ANTHROPIC_API_KEY=... ``` ### langchain.mjs ```js // LangChain.js agent that checks a quote with AttestPage before it cites it. import { createAgent, tool } from 'langchain'; import * as z from 'zod'; import { createClient } from './attestpage.mjs'; const attestpage = await createClient(); // Tools return the route's JSON as text. const verifyQuote = tool(async ({ url, quote }) => JSON.stringify(await attestpage.verifyQuote({ url, quote })), { name: 'verify_quote', description: 'Use before quoting a web page: is this exact or near-exact quote on the page at url? ' + 'Returns result.match (exact, fuzzy or none), page.content_kind and a signed receipt. US$0.01.', schema: z.object({ url: z.string(), quote: z.string() }), }); const checkLinks = tool(async ({ urls }) => JSON.stringify(await attestpage.checkLinks({ urls })), { name: 'check_links', description: 'Use before putting links in an answer: status, final URL and content_kind for each URL. ' + 'Up to 10 URLs (3 on the free trial). US$0.005 per batch.', schema: z.object({ urls: z.array(z.string()).min(1).max(10) }), }); const agent = createAgent({ model: process.env.MODEL || 'anthropic:claude-sonnet-5-5', tools: [verifyQuote, checkLinks], systemPrompt: 'Before you quote a web page, call verify_quote. Cite a quote only when result.match is exact or fuzzy ' + 'and page.content_kind is real. If a tool returns payment_required, give the price and stop.', }); const question = process.argv.slice(2).join(' ') || 'Does https://www.iana.org/help/example-domains say "These domains may be used as illustrative examples in documents"?'; const result = await agent.invoke({ messages: [{ role: 'user', content: question }] }); console.log(result.messages.at(-1).content); ``` ```sh node langchain.mjs # free trial ATTESTPAGE_X402_KEY=0x... node langchain.mjs # pays US$0.01 per verify_quote in test USDC ``` --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/docs/examples/openai-agents-sdk.md # OpenAI Agents SDK: check a quote before citing it, paid per call Last updated: 2026-10-10 An OpenAI Agents SDK agent with two AttestPage tools: `verify_quote` (US$0.01 a call) and `check_links` (US$0.005 per batch of up to 10 URLs). With no key, the client uses the free trial and returns the price when a call needs payment. With `ATTESTPAGE_X402_KEY` set, it pays each call with x402 through `@x402/fetch`, and refuses any offer above its limit. ## Install Node.js 20 or later. ```sh npm init -y && npm pkg set type=module npm install @openai/agents zod npm install @x402/fetch @x402/evm viem # only needed to pay export OPENAI_API_KEY=... ``` To pay, set `ATTESTPAGE_X402_KEY` to the private key (`0x` and 64 hex digits) of a Base Sepolia wallet made only for testing, holding test USDC. The key stays in this process. ## attestpage.mjs ```js // AttestPage client for agent tools. With no key it sends the free-trial header. With ATTESTPAGE_X402_KEY // it pays each call with x402 on Base Sepolia (test USDC), up to MAX_USD a call. A 402 it does not pay // comes back as { payment_required, price_usd, network } instead of an exception. const BASE = (process.env.ATTESTPAGE_URL || 'https://vehcdj664efetfrsolne5umanq.srv.us').replace(/\/+$/, ''); const NETWORK = 'eip155:84532'; // Base Sepolia const MAX_USD = Number(process.env.MAX_USD || '0.01'); // the most one call may cost // A fetch that answers 402 offers with a payment signed by key. Loaded only when a key is set. async function payingFetch(key) { const [{ wrapFetchWithPayment, x402Client }, { registerExactEvmScheme }, { privateKeyToAccount }] = await Promise.all([ import('@x402/fetch'), import('@x402/evm/exact/client'), import('viem/accounts'), ]); const client = new x402Client(); registerExactEvmScheme(client, { signer: privateKeyToAccount(key) }); // Sign only offers on Base Sepolia at or under MAX_USD (amounts are USDC with 6 decimals). client.registerPolicy((_version, reqs) => reqs.filter((r) => r.network === NETWORK && Number(r.amount) <= MAX_USD * 1e6)); return wrapFetchWithPayment(fetch, client); } function offer(header) { try { const first = JSON.parse(Buffer.from(header, 'base64').toString('utf8')).accepts[0]; return { price_usd: Number(first.amount) / 1e6, network: first.network }; } catch { return {}; } } export async function createClient({ key = process.env.ATTESTPAGE_X402_KEY } = {}) { const fetchFn = key ? await payingFetch(key) : fetch; async function call(path, body, { trial = !key } = {}) { const headers = { 'content-type': 'application/json', accept: 'application/json' }; if (trial) headers['quoteproof-trial'] = '1'; let res; try { res = await fetchFn(`${BASE}${path}`, { method: 'POST', headers, body: JSON.stringify(body) }); } catch (err) { // @x402/fetch throws when no offer passes the policy, or when signing fails. return { error: 'payment_not_made', message: String(err?.message ?? err) }; } const json = await res.json().catch(() => ({})); if (res.ok) return json; // The trial is used up for today: ask again without it, for the price. if (res.status === 429 && trial && json.error?.code === 'trial_exhausted') return call(path, body, { trial: false }); if (res.status === 402) { return { payment_required: true, ...offer(res.headers.get('payment-required') ?? ''), message: 'This call needs an x402 payment; set ATTESTPAGE_X402_KEY to pay.' }; } return { error: json.error?.code ?? `http_${res.status}`, status: res.status, message: json.error?.message ?? '' }; } return { verifyQuote: ({ url, quote, match = 'fuzzy' }) => call('/v1/verify/quote', { url, quote, match }), checkLinks: ({ urls }) => call('/v1/check/links', { urls }), verifyReceipt: ({ receipt }) => call('/v1/receipt/verify', { receipt }, { trial: false }), }; } ``` ## openai-agents-sdk.mjs ```js // OpenAI Agents SDK agent that checks a quote with AttestPage before it cites it. import { Agent, run, tool } from '@openai/agents'; import { z } from 'zod'; import { createClient } from './attestpage.mjs'; const attestpage = await createClient(); const verifyQuote = tool({ name: 'verify_quote', description: 'Use before quoting a web page: is this exact or near-exact quote on the page at url? ' + 'Returns result.match (exact, fuzzy or none), page.content_kind and a signed receipt. US$0.01.', parameters: z.object({ url: z.string(), quote: z.string() }), execute: async ({ url, quote }) => attestpage.verifyQuote({ url, quote }), }); const checkLinks = tool({ name: 'check_links', description: 'Use before putting links in an answer: status, final URL and content_kind for each URL. ' + 'Up to 10 URLs (3 on the free trial). US$0.005 per batch.', parameters: z.object({ urls: z.array(z.string()).min(1).max(10) }), execute: async ({ urls }) => attestpage.checkLinks({ urls }), }); const agent = new Agent({ name: 'Source checker', instructions: 'Before you quote a web page, call verify_quote. Cite a quote only when result.match is exact or fuzzy ' + 'and page.content_kind is real. If a tool returns payment_required, give the price and stop.', tools: [verifyQuote, checkLinks], }); const question = process.argv.slice(2).join(' ') || 'Does https://www.iana.org/help/example-domains say "These domains may be used as illustrative examples in documents"?'; const result = await run(agent, question); console.log(result.finalOutput); ``` ## Run ```sh node openai-agents-sdk.mjs # free trial ATTESTPAGE_X402_KEY=0x... node openai-agents-sdk.mjs # pays US$0.01 per verify_quote in test USDC ``` ## What comes back - `result.match`: `exact`, `fuzzy` or `none`. Read `page.content_kind` first: a `none` on a `bot_wall` or `js_shell` page means the page could not be read. - `receipt`: an Ed25519-signed JWS of what was on the page and when. `attestpage.verifyReceipt({ receipt })` checks it for free. - A paid answer has `"tier": "paid"` and `"rail": "x402"`. Payment settles only when the route answers with a status below 400; a bad request is refused before payment. --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/docs/examples/claude-agent-sdk.md # Claude Agent SDK: AttestPage tools through quoteproof-mcp Last updated: 2026-10-10 A Claude Agent SDK agent that gets AttestPage's tools from `quoteproof-mcp`, an MCP server run over stdio. There is no client code to write: the server lists ten tools with their prices, pays with x402 from your own key when `QUOTEPROOF_X402_KEY` is set, and otherwise sends the free-trial header. | Tool | Route | Price | |---|---|---| | `attestpage_verify_quote` | `POST /v1/verify/quote` | US$0.01 | | `attestpage_verify_citations` | `POST /v1/verify/citations` | US$0.10 per batch of up to 10 | | `attestpage_verify_quotes` | `POST /v1/verify/quotes` | US$0.08 per batch of up to 20 on one page | | `attestpage_verify_document` | `POST /v1/verify/document` | US$0.10 per part of up to 10 pairs | | `attestpage_preview_document` | `POST /v1/verify/document/preview` | free | | `attestpage_fetch` | `POST /v1/fetch` | US$0.002 | | `attestpage_check_links` | `POST /v1/check/links` | US$0.005 per batch of up to 10 | | `attestpage_check_packages` | `POST /v1/check/packages` | US$0.005 per batch of up to 10 | | `attestpage_attest` | `POST /v1/attest` | US$0.002 | | `attestpage_verify_receipt` | `POST /v1/receipt/verify` | free | ## Install Node.js 20.3 or later. ```sh npm init -y && npm pkg set type=module npm install @anthropic-ai/claude-agent-sdk export ANTHROPIC_API_KEY=... ``` The MCP server needs no install: `npx` fetches it from the service as a tarball. [`/dl/index.json`](https://vehcdj664efetfrsolne5umanq.srv.us/dl/index.json) lists the current version with its sha256 and npm integrity string. The package has no install scripts. To pay, set `QUOTEPROOF_X402_KEY` to the private key (`0x` and 64 hex digits) of a Base Sepolia wallet made only for testing, holding test USDC. The key stays in the MCP server's process and is never logged or returned. ## claude-agent-sdk.mjs ```js // Claude Agent SDK agent that checks a quote with AttestPage (quoteproof-mcp over stdio) before it cites it. import { query } from '@anthropic-ai/claude-agent-sdk'; const ORIGIN = 'https://vehcdj664efetfrsolne5umanq.srv.us'; const SERVICE = (process.env.ATTESTPAGE_URL || ORIGIN).replace(/\/+$/, ''); // Settings for the MCP server. Without QUOTEPROOF_X402_KEY it uses the free trial. const env = { QUOTEPROOF_URL: SERVICE, QUOTEPROOF_MAX_USD: '0.01', // the most one call may cost QUOTEPROOF_SESSION_USD: '0.10', // the most this run may spend in total }; if (process.env.QUOTEPROOF_X402_KEY) env.QUOTEPROOF_X402_KEY = process.env.QUOTEPROOF_X402_KEY; // Tools from an MCP server are named mcp____. const TOOLS = ['attestpage_verify_quote', 'attestpage_check_links', 'attestpage_verify_receipt']; const prompt = process.argv.slice(2).join(' ') || 'Does https://www.iana.org/help/example-domains say "These domains may be used as illustrative examples in documents"?'; for await (const message of query({ prompt, options: { mcpServers: { attestpage: { type: 'stdio', command: 'npx', args: ['-y', `${ORIGIN}/dl/quoteproof-mcp-0.1.25.tgz`], env }, }, allowedTools: TOOLS.map((t) => `mcp__attestpage__${t}`), systemPrompt: 'Before you quote a web page, call attestpage_verify_quote. Cite a quote only when result.match is exact ' + 'or fuzzy and page.content_kind is real. If a tool fails with trial_exhausted or payment_refused_locally, say so and stop.', maxTurns: 6, }, })) { if (message.type === 'result') console.log(message.subtype === 'success' ? message.result : `stopped: ${message.subtype}`); } ``` ## Run ```sh node claude-agent-sdk.mjs # free trial QUOTEPROOF_X402_KEY=0x... node claude-agent-sdk.mjs # pays in test USDC, at most US$0.01 a call ``` ## Limits the server applies before it signs | Variable | Default | Meaning | |---|---|---| | `QUOTEPROOF_MAX_USD` | `0.10` | The most it pays for one call; each tool also pays no more than its listed price | | `QUOTEPROOF_SESSION_USD` | `1` | The most it signs in total while the process runs | | `QUOTEPROOF_NETWORKS` | `eip155:84532` | Networks it pays on (Base Sepolia, test USDC) | | `QUOTEPROOF_TRIAL` | on | Set `0` to turn off the free-trial header | An offer over a limit is refused with `payment_refused_locally` and nothing is signed. A paid result carries the settlement (transaction hash, payer, network) in `_meta["x402/payment-response"]`. ## The same server in other MCP clients Most MCP clients take a block like this: ```json { "mcpServers": { "attestpage": { "command": "npx", "args": ["-y", "https://vehcdj664efetfrsolne5umanq.srv.us/dl/quoteproof-mcp-0.1.25.tgz"], "env": { "QUOTEPROOF_URL": "https://vehcdj664efetfrsolne5umanq.srv.us", "QUOTEPROOF_X402_KEY": "0x..." } } } } ``` Leave out `QUOTEPROOF_X402_KEY` to use the free trial. With no install at all, the service also answers MCP over HTTP at `POST /mcp`; see [MCP server](https://vehcdj664efetfrsolne5umanq.srv.us/docs/mcp). --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/docs/examples/fetch-x402.md # Plain fetch with x402: one paid call, step by step Last updated: 2026-10-10 One paid call to `POST /v1/verify/quote` (US$0.01) with plain `fetch`, showing each step of an x402 payment: read the offer, check it, sign it, send the request again, read the settlement, then check the receipt for free. Use this to wire AttestPage into any agent framework, or to see what an x402 client library does for you. Payments run on Base Sepolia (`eip155:84532`), a test network, in test USDC. ## Install Node.js 20 or later. ```sh npm init -y && npm pkg set type=module npm install @x402/core @x402/evm viem ``` Set `ATTESTPAGE_X402_KEY` to the private key (`0x` and 64 hex digits) of a Base Sepolia wallet made only for testing, holding test USDC. The key stays in this process. ## fetch-x402.mjs ```js // One paid AttestPage call with plain fetch and x402, step by step. import { x402Client, x402HTTPClient } from '@x402/core/client'; import { registerExactEvmScheme } from '@x402/evm/exact/client'; import { privateKeyToAccount } from 'viem/accounts'; const BASE = (process.env.ATTESTPAGE_URL || 'https://vehcdj664efetfrsolne5umanq.srv.us').replace(/\/+$/, ''); const NETWORK = 'eip155:84532'; // Base Sepolia const MAX_USD = 0.01; // the most this script will pay; verify/quote is listed at US$0.01 const client = new x402Client(); registerExactEvmScheme(client, { signer: privateKeyToAccount(process.env.ATTESTPAGE_X402_KEY) }); const x402 = new x402HTTPClient(client); const url = `${BASE}/v1/verify/quote`; const request = { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ url: process.argv[2] || 'https://www.iana.org/help/example-domains', quote: process.argv[3] || 'These domains may be used as illustrative examples in documents', }), }; // 1. Send the request with no payment. A valid request gets 402, with the offer in PAYMENT-REQUIRED. let res = await fetch(url, request); if (res.status !== 402) throw new Error(`expected 402, got ${res.status}: ${await res.text()}`); const offer = x402.getPaymentRequiredResponse((name) => res.headers.get(name), await res.json()); // 2. Check the offer before signing: right network, price within MAX_USD (USDC has 6 decimals). const accepts = offer.accepts.filter((a) => a.network === NETWORK && Number(a.amount) <= MAX_USD * 1e6); if (accepts.length === 0) throw new Error(`no acceptable offer: ${JSON.stringify(offer.accepts)}`); console.log(`price: US$${Number(accepts[0].amount) / 1e6} on ${accepts[0].network}, pay to ${accepts[0].payTo}`); // 3. Sign the offer and send the same request again with PAYMENT-SIGNATURE. const payment = await x402.createPaymentPayload({ ...offer, accepts }); res = await fetch(url, { ...request, headers: { ...request.headers, ...x402.encodePaymentSignatureHeader(payment) } }); const answer = await res.json(); if (!res.ok) throw new Error(`${res.status} ${answer.error?.code}: ${answer.error?.message}`); // 4. A settled call carries PAYMENT-RESPONSE: the transaction, payer and network. const settlement = x402.getPaymentSettleResponse((name) => res.headers.get(name)); console.log('settled:', settlement.transaction, 'from', settlement.payer, 'on', settlement.network); console.log('match:', answer.result.match, 'score:', answer.result.score, 'page:', answer.page.content_kind); // 5. Check the receipt's signature for free. const check = await fetch(`${BASE}/v1/receipt/verify`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ receipt: answer.receipt }), }).then((r) => r.json()); console.log('receipt valid:', check.valid); ``` ## Run ```sh ATTESTPAGE_X402_KEY=0x... node fetch-x402.mjs ATTESTPAGE_X402_KEY=0x... node fetch-x402.mjs https://example.com 'Example Domain' ``` ## Notes - **Only the price.** An unpaid request with an empty body (`{}`) also gets the 402 offer, so a client can read a route's price before it has a real input. - **Bad input is free.** A request that fails validation gets `400` before any offer and is never charged. - **Settled only on success.** The payment settles only when the route answers with a status below 400. - **Spent signatures.** A payment signature pays for one call. Sending it again gets `409 payment_already_used`; sign a new offer instead. - **Shorter.** `wrapFetchWithPayment` from `@x402/fetch` does steps 1 to 3 inside `fetch`; [openai-agents-sdk.md](/docs/examples/openai-agents-sdk) uses it with a price limit. - **Other routes.** The same steps work for every paid route; only the body and the price change. See [README.md](/docs/examples#routes-and-prices). --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/docs/how-it-works.md # How it works Last updated: 2026-10-10 How AttestPage fetches a page, what each `content_kind` verdict means, and what a signed receipt records about the fetch. ## The fetcher When a route reads a page, our fetcher requests the URL you sent: - GET only, with no cookies, logins or headers passed through from you. User agent `QuoteproofBot/1.0 (+https://vehcdj664efetfrsolne5umanq.srv.us/bot)`. - http and https on ports 80 and 443. Private, loopback and other internal addresses are refused; the address is resolved once and the connection is pinned to it, and each redirect is checked again. - Up to 5 redirects, 5 seconds to connect, 10 seconds in all, 2 MB of body. - HTML, XHTML, plain text and JSON. PDFs too, for `fetch`, `verify/quote`, `verify/quotes` and `verify/citations`: the text layer only (no OCR, so a scanned page has no text), and each match gives its page number. No JavaScript is run. - robots.txt and our opt-out list are honoured; see [Our fetcher](/bot). - No proxies and no attempt to get past CAPTCHAs or bot checks. A challenge page is reported as `bot_wall`. Pages are not stored. The text is extracted, hashed, matched, and dropped when the response is sent. ## content_kind Every page result has one `content_kind`, from automated rules over the status, headers and text. They can be wrong; `signals` lists the clues behind each verdict. | content_kind | Meaning | |---|---| | `real` | The page loaded and looks like real content. | | `bot_wall` | The site served a bot challenge to our fetcher. Do not treat this as the page's content. | | `js_shell` | The page needs JavaScript to show its content; the text we read is mostly empty scaffolding. | | `paywall` | The page appears to be behind a paywall or login; the text may be a teaser only. | | `empty` | The page loaded but has little or no readable text. | | `off_site` | The URL redirected to a different site; the content is from that site. In check/links, `off_site_error: true` marks one whose site answers 4xx/5xx. | | `http_error` | Error status, or a connection problem (signals such as `outcome_dns_error`, `outcome_connect_error`). | | `unsupported_type` | Not HTML, text, JSON or a readable PDF, so not read. A PDF we could not read has the signal `pdf` and one of `pdf_encrypted` (needs a password), `pdf_invalid` (damaged or not a PDF), `pdf_over_budget` (needs more memory than we allow), `pdf_timeout` or `pdf_out_of_memory`. | | `pdf` | check/links only: a live PDF link (status 2xx, `application/pdf`, same site), not read. In check/links an unread link with a 4xx/5xx status is `http_error` and one redirected to another site is `off_site` (with `off_site_error: true` if that site answers 4xx/5xx). | | `robots_disallowed` | robots.txt or an opt-out disallows our fetcher. | | `timeout` | The site did not answer in time. | | `too_large` | Over the 2 MB cap, so not read in full. | ## injection_flags Patterns often used for prompt injection, found in the page text: `instruction_override`, `role_reassignment`, `prompt_markers`, `prompt_extraction`, `addresses_ai`, `command_execution`, `exfiltration_request`, plus `hidden_instructions` when a match is in text a browser does not show. They are flags, not a verdict. An empty list means none of our patterns matched. ## Receipts Every paid answer carries a `receipt`: a compact JWS signed with Ed25519 (`alg` EdDSA, `typ` `attestpage-evidence+jws`). The key id is `https://still-rapids-9yt7.here.now/.well-known/jwks.json#ap-`, where `https://still-rapids-9yt7.here.now` is the fixed issuer address (it stays the same when the service address changes); the public keys are at [/.well-known/jwks.json](https://vehcdj664efetfrsolne5umanq.srv.us/.well-known/jwks.json) and [/.well-known/did.json](https://vehcdj664efetfrsolne5umanq.srv.us/.well-known/did.json). Payload fields (version 1): | Field | Meaning | |---|---| | `v` | schema version, 1 | | `iss` | `https://still-rapids-9yt7.here.now` | | `iat` | signing time, Unix seconds | | `kind` | `quote`, `quotes`, `citations`, `document`, `fetch`, `links`, `packages` or `attest` | | `request_sha256` | SHA-256 of your request body as canonical JSON | | `quote_sha256` | SHA-256 of the quote (quote receipts) | | `url`, `final_url`, `http_status`, `content_kind` | what was requested and what came back | | `content_sha256` | SHA-256 of the full extracted text | | `raw_sha256` | SHA-256 of the body bytes as received | | `retrieved_at` | when the page was fetched | | `server_ip` | the vetted IP address our fetcher connected to for the final response (the site's address, not yours) | | `tls` | the site's certificate on the final response over HTTPS: `cert_sha256` (SHA-256 of the DER certificate), `issuer`, `subject` (each up to 1,024 bytes), `valid_from`, `valid_to`; null over plain HTTP | | `headers` | these response headers when the site sent them: `date`, `last-modified`, `etag`, `content-type`, `content-length` (each up to 512 bytes) | | `result` | the route's result (match, signals, per-link results, per-page and per-citation results, or attestation) | | `tier` | `paid`, `trial` or `sample` | | `payment` | paid calls: rail, network, asset, amount, payer, payment_id (L402: payer null, payment_hash and preimage); otherwise null | `server_ip`, `tls` and `headers` are on fetch and quote receipts and on each entry of a links receipt and each page of a citations receipt. They are null when no response came back, and always null on an edge server, which cannot pin the connection to an IP or read the certificate. Those byte caps count the value as UTF-8 once JSON-encoded (a `"` or `\` counts 2, a CJK character 3); a longer value is cut on a whole character and ends with `…`. Receipts issued before these fields existed do not have them and still verify. A receipt records what our fetcher saw at that time. Pages change, and a page can show different content to different visitors. It does not show that anything the page says is correct. Check receipts with [POST /v1/receipt/verify](/docs/receipt-verify) or [offline](/docs/verify-offline). --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/docs/limits.md # Limits Last updated: 2026-10-10 ## Requests | Limit | Value | |---|---| | Request body | 64 KB; 168 KB for `receipt/verify` and `/mcp` (413 `body_too_large`) | | URL length | 2,048 characters, http or https, ports 80 and 443 | | Quote | 1,000 characters after normalisation | | Quotes per `verify/quotes` call | 20 | | Citations per `verify/citations` call | 10 | | `verify/document`: document size, pairs per document, pairs per part | 60,000 bytes; 100 (the rest skipped as `beyond_limit`); 10 | | URLs per `check/links` call | 10 | | Citations, quotes, pairs or URLs per free-trial call (`verify/citations`, `verify/quotes`, `verify/document`, `check/links`) | 3; a larger batch gets the normal 402 offer (`details.reason` `trial_too_large`) | | Packages per `check/packages` call | 10 | | Attest note | 280 characters | | Receipt sent to `receipt/verify` | 163,840 characters (160 KB; a receipt with every field at its cap is about 128 KB, whatever characters the site's certificate names and headers use) | ## Fetching | Limit | Value | |---|---| | Redirects | 5, each checked again | | Connect timeout | 5 seconds | | Total timeout | 10 seconds | | Body size | 2 MB as received, 2 MB after decompression | | Content types | HTML, XHTML, plain text, JSON | | PDF | `fetch`, `verify/quote`, `verify/quotes` and `verify/citations` only. Text layer only, no OCR; the body cap above applies. Up to 1,000 pages and 2,097,152 characters (then signal `pdf_truncated`). Each PDF gets 5 seconds and 64 MB of buffers plus a 64 MB heap (else `pdf_timeout`, `pdf_over_budget` or `pdf_out_of_memory`, charged like any page we cannot read). 2 PDFs are read at once across all callers, with a queue of 16; then 503 `over_capacity` (`details.reason` `pdf_queue_full`) with `Retry-After`, not charged | | Text returned by `fetch` | 20,000 characters (hashes cover the full text) | | robots.txt | cached up to 1 hour; 5-second timeout | ## Rates | Limit | Value | |---|---| | Per caller IP address | 60 paid-route requests a minute | | `verify/citations`, `verify/quotes` and `verify/document` calls where every citation or quote fails (422, not charged), per caller IP address, counted together | 6 a minute, counting batches still running; then 429 with `details.reason` `uncharged_failures` | | `verify/citations`, `verify/quotes` and `verify/document` calls being matched, across all callers, shared | 2 at once, with a queue of 8; then 503 `over_capacity` with `Retry-After`, not charged | | Per target host | 1 request a second and 30 a minute, across all callers | | crates.io lookups (`check/packages`) | 1 a second across all callers; a call that would wait over 20 seconds gets 429; free trial calls hold at most half of that queue | | Whole service | 8 fetches at once, with a queue of 32 | | Lightning (L402) invoices, where L402 is on | 30 unpaid per caller IP address in 10 minutes, 1,000 open in all. An invoice whose request you then pay with x402 does not count | Over a limit you get 429 `rate_limited` with a `Retry-After` header, before any payment is asked for. Back off for the time it gives. ## What the fetcher does not do - Run JavaScript, use proxies or rotate IP addresses. - Solve or get past CAPTCHAs or bot checks. - Send cookies, logins or your headers. - Reach private or internal addresses. - Fetch pages that robots.txt or our opt-out list disallows. ## Network Payments run on Base Sepolia, a test network. See [Payments](/docs/payments). --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/docs/errors.md # Errors Last updated: 2026-10-10 Every error code the API can return, what it means and whether the call was charged. All errors share one JSON shape: ```json { "error": { "code": "invalid_url", "message": "url: …. You were not charged.", "details": { "field": "url" } } } ``` `code` is stable and meant for programs; `message` is for people; `details` is optional (for example `field` or `retry_after_ms`). An answer with status 400 or above is never charged (with L402, the token is not spent and still works). The one exception is `settle_failed`: the payment may have gone through even though settling reported a failure. Resend the same request with the same payment id (the x402 `payment-identifier` extension): if the charge went through, you get the withheld result without paying again (marked `payment-redelivered: true`); if not, the payment is settled at most once. A charge that went through is also recorded for reconciliation. So a `payment_already_used` answer can follow a charged `settle_failed`; that call itself charged nothing. | Status | Code | Meaning | |---|---|---| | 400 | `bad_request` | Body is not a JSON object, or a field has the wrong type. | | 400 | `invalid_request` | A field is missing or out of range (for example a quote over 1,000 characters), or both an x402 payment header and an L402 `Authorization` header or a credit key were sent (`details.reason` `two_payment_headers`). | | 400 | `invalid_url` | The URL is malformed, too long, not http(s) or on another port; `details.reason` says which. | | 400 | `blocked_target` | The URL points at a private, loopback or otherwise internal address. | | 402 | `payment_required` | Pay with x402: see the `PAYMENT-REQUIRED` header and [Payments](/docs/payments). Where L402 is on, the `WWW-Authenticate: L402` header carries a macaroon and a Lightning invoice too. A free-trial call to `verify/citations`, `verify/quotes`, `verify/document` or `check/links` with more than 3 citations, quotes, pairs or URLs gets this too, with `details.reason` `trial_too_large`: pay for it, or send at most 3 per trial call. | | 402 | `payment_invalid` | The payment did not verify (`details.reason` says why). Sign a new one from the `PAYMENT-REQUIRED` header. For an L402 token the reason starts with `l402_` (for example `l402_wrong_route`, `l402_expired`, `l402_preimage_mismatch`) and a fresh `WWW-Authenticate` offer is included. A resend for a withheld `settle_failed` result whose signature is not the payer's own gets this with `details.reason` `invalid_exact_evm_payload_signature`: resend the original signed payment, or a new one signed with the payer's key. | | 403 | `robots_disallowed` | robots.txt or our opt-out list disallows the page (or a redirect from it). | | 403 | `payer_refused` | The paying address is on a sanctions list. For a credit key: a payment to it came from a listed address, so the key is blocked (`details.reason` `credits_payer_refused`). | | 404 | `not_found` | No such route or sample, or (`GET /v1/credits`) no such credit key. | | 409 | `payment_id_conflict` | The payment id was already used for a different request. | | 409 | `payment_already_used` | This payment signature was already spent, and this call did not charge again. Sign a new payment; the `PAYMENT-REQUIRED` header is included. If an earlier call with it ended in `settle_failed`, resend that request with its payment id to get the result. `details.reason` `result_not_kept`: that earlier call was charged but its result is no longer kept; the charge is recorded for reconciliation. For L402: the token already paid for another call (`details.reason` `l402_spent`); pay the fresh invoice in `WWW-Authenticate`. | | 409 | `settle_pending` | An earlier call with this payment id ended in `settle_failed`, it is not charged on chain yet, and its payment could still go through. Resend the same signed payment now, or a new one after `Retry-After` (`details.retry_after_ms`). | | 409 | `idempotency_conflict` | The attest `idempotency_key` was already used with a different hash or note. | | 413 | `body_too_large` | Body over 64 KB (168 KB on `receipt/verify` and `/mcp`). | | 422 | `host_not_found` | The URL's host name does not resolve (for `check/links` and `verify/citations`: none of the URLs resolves; for `verify/document`: none in the part). | | 422 | `page_too_large_for_edge` | Edge server only: the page is over the 64 KB it reads, or (check/packages) a registry answer is over the 256 KB it reads, with only the names over that size in `details.too_large` (no results for the others); `details.full_service` is the server to send the call to. | | 422 | `edge_subrequest_limit` | Edge server only: the call needs more outbound requests than the edge allows (many hosts or redirects); use `details.full_service`. | | 422 | `match_too_costly` | Fuzzy matching this quote would take too long; use `"match": "exact"` or a shorter quote. For `verify/citations` and `verify/quotes` this is an `error` on that citation or quote only and the call is charged; only when every one fails does the call get 422, with `details.failed` listing them. `verify/document` works the same way per part. | | 422 | `no_pairs` | `verify/document`: no quote with a link was found in the document; `details.skipped` counts what was skipped and why. Never charged. | | 429 | `rate_limited` | Too many requests to that host or from your address, or (for `check/packages`) the crates.io queue of one lookup a second is full; see `Retry-After`. Where L402 is the only offer, also too many unpaid Lightning invoices (`details.reason` `l402_invoice_cap`). For `verify/citations`, `verify/quotes` and `verify/document` (one shared count), also too many calls from your address that failed uncharged (`details.reason` `uncharged_failures`). | | 429 | `trial_exhausted` | Free trial calls used up for the day; see `Retry-After`. | | 500 | `internal_error` | Something failed on our side. | | 502 | `registry_unavailable` | `check/packages` only: every registry lookup in the batch failed (`details.reasons`), so nothing was charged. Try again later. | | 502 | `facilitator_unavailable` | The payment facilitator could not be reached, so no payment was taken. Try again later. | | 502 | `settle_failed` | The work ran but the payment could not be settled, so the result is withheld. Resend the same request with the same payment id: if the charge went through on chain, you get the result without paying again; if not, the payment is settled at most once. If a charge went through, it is recorded for reconciliation. Redelivery runs on the full service only; on the edge server see [Payments](/docs/payments). | | 501 | `not_on_edge` | Edge server only: `POST /v1/attest`, `POST /v1/verify/citations`, `POST /v1/verify/quotes` and `POST /v1/verify/document` run on the full service (the free `verify/document/preview` works on the edge) (`details.full_service`). Also `fetch` and `verify/quote` when the page is a PDF (`details.reason` `pdf`, with `details.final_url`): only the full service reads PDFs. Never charged. | | 503 | `paused` | Paid routes are paused for maintenance. | | 503 | `over_capacity` | `verify/citations`, `verify/quotes` and `verify/document`: too many batches are waiting to be matched (`details.reason` `match_queue_full`). `fetch`, `verify/quote`, `verify/quotes` and `verify/citations`: the page is a PDF and too many PDFs are waiting to be read (`details.reason` `pdf_queue_full`, with `details.field`). Retry after `Retry-After`. Never charged, even when it comes after your payment was verified. | | 503 | `signing_unavailable` | Signing keys are not loaded. | | 503 | `payments_unavailable` | Payments are not set up on this server, (with L402 only) no invoice could be made, or a new payment for a payment id whose earlier call ended in `settle_failed` came while the chain could not be read. Where credit keys are on, also: no credit address could be issued (the signet explorer is down, the server's addresses are used up, or too many issued addresses are still unpaid: `details.reason` `credits_gap_full`). | | 503 | `sanctions_list_stale` | Edge server only: its built-in sanctions list is more than 8 days old, so it refuses payments until it is rebuilt (`details.list_date`, `details.full_service`). Never charged. | | 503 | `payment_screening_unavailable` | The payment check could not be completed (the chain read failed or timed out, or the sanctions list could not be read); retry after `Retry-After` and do not pay again. For a credit key that needs a new payment counted: `details.reason` `credits_screening_unavailable`. | ## L402 reasons Where L402 is on, a token that is refused carries one of these in `details.reason`. None of them spends the token or charges anything. | Reason | Code | Meaning | |---|---|---| | `l402_malformed` | `payment_invalid` | The `Authorization: L402 :` header or the macaroon cannot be read. | | `l402_bad_signature` | `payment_invalid` | The macaroon was not issued by this server, or was changed after it was issued. | | `l402_bad_version` | `payment_invalid` | The macaroon identifier has a version this server does not issue. | | `l402_unknown_caveat` | `payment_invalid` | The macaroon has a caveat this server does not understand. | | `l402_missing_caveat` | `payment_invalid` | The macaroon lacks its `route`, `price` or `expires` caveat. | | `l402_wrong_route` | `payment_invalid` | The token was issued for a different route. | | `l402_price_mismatch` | `payment_invalid` | The token's price is below this route's sats price. | | `l402_expired` | `payment_invalid` | The token's `expires` time has passed. | | `l402_preimage_mismatch` | `payment_invalid` | The preimage does not hash to the invoice's payment hash (the invoice was not paid). | | `l402_spent` | `payment_already_used` | The token already paid for a different call. | | `l402_invoice_cap` | `rate_limited` | No new Lightning invoice: too many unpaid ones from your address (see [Limits](/docs/limits)) (`details.scope` `ip`) or open on the server (`global`). Wait for `Retry-After`. When x402 can be offered, the answer is instead the usual 402 `payment_required` with the x402 offer, no L402 offer, this reason and `Retry-After`. | | `two_payment_headers` | `invalid_request` | Both an x402 payment header and an L402 `Authorization` header were sent. | ## Credit key reasons Where prepaid credit keys are on (`POST /v1/credits/order`, then `Authorization: Bearer qpc_…` on a paid route; Bitcoin signet, full service only), a refusal carries one of these in `details.reason`. None of them takes sats from the key. Before a payment to a key's address is credited, every input address of that payment is checked against the OFAC sanctions list; a payment with an input that has no address (a coinbase or bare script) cannot be checked and is held (`held_sats`, status `held`), not credited. | Reason | Code | Meaning | |---|---|---| | `credits_amount` | `invalid_request` | `POST /v1/credits/order`: `sats` is not a whole number from `details.min_sats` to `details.max_sats`. | | `credits_order_cap` | `rate_limited` | Too many credit orders from your address in the last hour; pay a key you have, or wait for `Retry-After` (`details.retry_after_ms`). | | `credits_gap_full` | `payments_unavailable` | `POST /v1/credits/order`: too many credit addresses in a row are still waiting for payment, so no new one is issued until one is paid or expires unpaid (see [Payments](payments.md)). Pay a key you already have, or try again later. | | `credits_no_key` | `invalid_request` | `GET /v1/credits` without `Authorization: Bearer `. | | `credits_malformed` | `payment_invalid` on a paid route, `not_found` on `GET /v1/credits` | The `qpc_` key is not 47 characters of base64url. | | `credits_unknown_key` | `payment_invalid` on a paid route, `not_found` on `GET /v1/credits` | This server never issued that key. | | `credits_insufficient` | `payment_required` | The key has fewer confirmed, screened sats available than the route's price (`details.price_sats`); `details` also carries the key's `address`, `status` and balances. Pay the address again to top up. | | `credits_payer_refused` | `payer_refused` | A payment to the key's address came from an address on the sanctions list: the key is blocked for good and that payment is not credited. | | `credits_screening_unavailable` | `payment_screening_unavailable` | The key has fewer sats than the price, and its address could not be read and screened now (the chain read failed or timed out, or the sanctions list cannot be read), so a new payment may not be counted yet. Do not pay again: retry after `Retry-After` (`details.retry_after_ms`). Sats already credited still work. | | `two_payment_headers` | `invalid_request` | A credit key was sent with an x402 payment header. | ## Results that are not errors A 404, 500, bot wall, timeout or dead link on the page you asked about is a result, not an error. It comes back with status 200, a `content_kind` such as `http_error` or `bot_wall`, and a receipt, and it is charged. See [How it works](/docs/how-it-works). --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/docs/verify-a-quote-an-llm-cites.md # How to verify a quote an LLM cites Last updated: 2026-10-10 To check a quote an LLM attributes to a web page, fetch that page yourself and search its text for the quote, allowing for small differences in punctuation and spacing. `POST /v1/verify/quote` does this in one call (US$0.01): it answers `exact`, `fuzzy` or `none`, says whether the page loaded as real content, and signs a receipt. ## Why a quick check is not enough LLMs often produce quotes that are close to the source but not on it: a word changed, two sentences joined, or a line from a different page. A plain `fetch` and string search misses three things: - **Small differences.** Curly quotes, dashes, line breaks and invisible characters make an exact search fail on a quote that is really there. We normalise both texts (Unicode NFKC, plain quotes and dashes, one space for any whitespace) and also accept close matches above a threshold you set. - **Pages that did not load.** A bot challenge, a JavaScript shell, a paywall or an error page has no quote on it either. A `none` on such a page means the page could not be read, not that the quote is absent. Every answer carries a `content_kind` verdict so you can tell the two apart. - **No record.** If someone later asks where the quote came from, you have nothing to show. The signed receipt records the URL, the time, the hash of the page text and the match. ## One quote ```sh curl -s https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/quote ``` That free sample runs the paid route on a demo page. A real call sends the URL and the quote: ```json { "url": "https://www.iana.org/help/example-domains", "quote": "These domains may be used as illustrative examples in documents" } ``` Read `page.content_kind` first, then `result.match`. Full details: [Verify a quote](/docs/verify-quote). Step by step with `curl`, from the free sample to checking the receipt: [How to check a quote an LLM cites, step by step with curl](/guides/verify-llm-quote). ## Pay per call with x402 No account or key is needed. A valid request with no payment gets `402 Payment Required` with the price in a `PAYMENT-REQUIRED` header; an x402 v2 client signs that offer and sends the request again. With `@x402/fetch` (`npm install @x402/fetch @x402/evm viem`) and a Base Sepolia wallet made only for testing, holding test USDC: ```js import { wrapFetchWithPayment, x402Client } from '@x402/fetch'; import { registerExactEvmScheme } from '@x402/evm/exact/client'; import { privateKeyToAccount } from 'viem/accounts'; const client = new x402Client(); registerExactEvmScheme(client, { signer: privateKeyToAccount(process.env.ATTESTPAGE_X402_KEY) }); const payingFetch = wrapFetchWithPayment(fetch, client); const res = await payingFetch('https://vehcdj664efetfrsolne5umanq.srv.us/v1/verify/quote', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ url: 'https://www.iana.org/help/example-domains', quote: 'These domains may be used as illustrative examples in documents' }), }); const answer = await res.json(); console.log(answer.page.content_kind, answer.result.match); ``` The wrapper signs the offer it gets. To check the price and network before signing, see [One x402 payment step by step](/docs/examples/fetch-x402). ## Every citation in an answer `POST /v1/verify/citations` (US$0.10 per batch) takes up to 10 quote and URL pairs, fetches each page once and returns the same verdict for each citation under one receipt. Run it on the sources at the end of an answer before the user sees it. Details: [Verify citations](/docs/verify-citations). ## The LLM paraphrased instead of quoting Send `"match": "passages"` and the claim as `quote`. You get the 3 passages of the page that share most of its words, to judge yourself; it is evidence to read, not a verdict. Try it free: [`GET /v1/sample/quote?match=passages`](https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/quote?match=passages). ## Questions ### Does a match mean the quote is correct? No. A match means the words are on the page our fetcher saw at that time. It says nothing about whether the page itself is right. ### What if the page blocks bots? The answer says so: `content_kind` is `bot_wall`, `js_shell` or `paywall`, and the advice field explains it. Treat the quote as unchecked, not as false. ### Can an agent call this without an account? Yes. Calls are paid per call with x402 (USDC on Base Sepolia, a test network), there is a small free trial, and the [MCP server](/docs/mcp) exposes the same check as a tool. ### How do I check the receipt later? With [POST /v1/receipt/verify](/docs/receipt-verify), or offline with our single-file checker: [Verify receipts offline](/docs/verify-offline). --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/guides/verify-llm-quote.md # How to check a quote an LLM cites, step by step with curl Last updated: 2026-10-10 To check that a quote an LLM cites really appears on the source page, send the page URL and the quote, word for word, to `POST /v1/verify/quote` (US$0.01 per call). AttestPage fetches the live page, searches its text for the quote and answers `exact`, `fuzzy` or `none`, with a verdict on the page 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. For why a plain string search falls short, see [How to verify a quote an LLM cites](/docs/verify-a-quote-an-llm-cites). ## What the check does - **Fetches the page.** It follows redirects and reads the page text as our fetcher receives it at that moment. - **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. - **Judges the page.** `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. - **Signs a receipt.** An Ed25519 receipt records the URL, the time, the page hashes and the match. Anyone can check it later against our published keys. ## Price US$0.01 per call, paid in USDC with x402 v2 on Base Sepolia (a test network). 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 page: ```sh curl -s https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/quote | jq '{match: .result.match, score: .result.score, kind: .page.content_kind, advice}' ``` ```json { "match": "fuzzy", "score": 0.963, "kind": "real", "advice": "The page loaded and looks like real content." } ``` ## 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/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 `10000` is US$0.01. ## 3. Check a quote Put the cited URL and the quote, exactly as the LLM gave it, in a file: ```sh cat > body.json <<'EOF' { "url": "https://www.iana.org/help/example-domains", "quote": "These domains may be used as illustrative examples in documents" } 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` 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/quotes or check/links covers at most 3 citations, quotes or URLs; a larger batch is not run as a trial but answered with the normal 402 offer, `details.reason` trial_too_large. On the trial, the same check runs from `curl`: ```sh curl -s -X POST https://vehcdj664efetfrsolne5umanq.srv.us/v1/verify/quote -H 'content-type: application/json' \ -H 'quoteproof-trial: 1' -d @body.json > answer.json jq '{match: .result.match, score: .result.score, context: .result.context, kind: .page.content_kind, tier}' answer.json ``` ## 4. Read the answer Read `page.content_kind` first, then `result.match`: | `content_kind` | `match` | What it means | |---|---|---| | `real` | `exact` | The quoted words are on the page. | | `real` | `fuzzy` | A close match. Compare `context` with the quote: a word may have been changed or two sentences joined. | | `real` | `none` | The quote is not on the page our fetcher received. Do not cite it to this source. | | `bot_wall`, `js_shell`, `paywall`, `http_error` | any | The page could not be read. The quote is unchecked, not false. | If the LLM paraphrased rather than quoted, send `"match": "passages"` with the claim as `quote`. You get the 3 passages that share most of its words, as evidence to read, not a verdict. Details: [Verify a quote](/docs/verify-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 ``` Or check it offline with our single-file checker: [Verify receipts offline](/docs/verify-offline). ## With MCP The same check is the MCP tool `attestpage_verify_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_quote", "arguments": { "url": "https://www.iana.org/help/example-domains", "quote": "These domains may be used as illustrative examples in documents" }, "_meta": { "attestpage/trial": false } } } EOF ``` ```json { "scheme": "exact", "network": "eip155:84532", "amount": "10000" } ``` 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.01 per quote with `POST /v1/verify/quote`. To check up to 10 quotes from one answer in one call, `POST /v1/verify/citations` costs US$0.10 per batch: [Verify citations](/docs/verify-citations). ### Does an exact match mean the quoted claim is right? No. It means those words were on the page our fetcher received at that time. Whether the page itself is right is a separate question. ### Is a call charged if the page will not load? Only answers below 400 are charged. A request that fails validation, a host name that does not resolve and a blocked URL are never charged. A page that answers with a 404 or a bot wall is charged, because that result is what the call checks; `content_kind` says so. Every case is in [Payments](/docs/payments) and [Errors](/docs/errors). --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/docs/signed-receipt-of-a-web-page.md # How to get a signed receipt of a web page Last updated: 2026-10-10 To get a signed receipt of what a web page showed at a given time, have an independent fetcher load it and sign the result. `POST /v1/fetch` (US$0.002) loads the URL and returns an Ed25519-signed receipt with the final URL, status, time, content hash and a verdict on what came back. Anyone can check it offline. ## What the receipt records The receipt is a compact JWS (`alg` EdDSA). Its payload includes: - the URL you asked for and the final URL after redirects, with the HTTP status; - `retrieved_at`, the time of the fetch; - `content_sha256`, the SHA-256 of the page text, so anyone holding the text can match it; - `content_kind`: `real`, `bot_wall`, `js_shell`, `paywall`, `http_error` and others; - the site's IP address, TLS certificate details and a short list of response headers, where our fetcher could read them. The full field list is in [How it works](/docs/how-it-works#receipts). ## Get one ```json { "url": "https://www.iana.org/help/example-domains", "return_text": true } ``` Send that to `POST /v1/fetch`. With `return_text` you also get up to 20,000 characters of clean text. Try the route free first: [`GET /v1/sample/fetch`](https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/fetch). Details: [Fetch a page](/docs/fetch). Step by step with `curl`, from the free sample to matching the page text: [How to get a signed receipt of a web page, step by step with curl](/guides/signed-page-receipt). ## Pay per call with x402 No account or key is needed. A valid request with no payment gets `402 Payment Required` with the price in a `PAYMENT-REQUIRED` header; an x402 v2 client signs that offer and sends the request again. With `@x402/fetch` (`npm install @x402/fetch @x402/evm viem`) and a Base Sepolia wallet made only for testing, holding test USDC: ```js import { wrapFetchWithPayment, x402Client } from '@x402/fetch'; import { registerExactEvmScheme } from '@x402/evm/exact/client'; import { privateKeyToAccount } from 'viem/accounts'; const client = new x402Client(); registerExactEvmScheme(client, { signer: privateKeyToAccount(process.env.ATTESTPAGE_X402_KEY) }); const payingFetch = wrapFetchWithPayment(fetch, client); const res = await payingFetch('https://vehcdj664efetfrsolne5umanq.srv.us/v1/fetch', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ url: 'https://www.iana.org/help/example-domains', return_text: true }), }); const answer = await res.json(); console.log(answer.page.content_kind, answer.page.content_sha256, answer.receipt); ``` The wrapper signs the offer it gets. To check the price and network before signing, see [One x402 payment step by step](/docs/examples/fetch-x402). ## Check one Any receipt can be checked against our published keys with [POST /v1/receipt/verify](/docs/receipt-verify) (free), or with no call to us at all: ```sh curl -O https://vehcdj664efetfrsolne5umanq.srv.us/dl/verify-receipt.mjs node verify-receipt.mjs --selftest ``` See [Verify receipts offline](/docs/verify-offline). The keys are at [/.well-known/jwks.json](https://vehcdj664efetfrsolne5umanq.srv.us/.well-known/jwks.json). ## Receipts for other things - A quote on a page: [Verify a quote](/docs/verify-quote) signs the match with the page details. - Many links at once: [Check links](/docs/check-links) signs one receipt over up to 10 URLs. - A file you hold, not a URL: [Attest a hash](/docs/attest) (US$0.002) signs a timestamp over a SHA-256 you send. We never see the file. ## Questions ### Is this the same as a web archive snapshot? No. An archive stores the page for others to view. A receipt is a small signed statement about one fetch that you keep yourself; we do not publish it, and pages fetched for you are not stored ([Privacy](/privacy)). ### Does the receipt show the page is accurate? No. It records what our fetcher saw at that time. Pages change and can show different content to different visitors. ### Why would an agent want one? To show later what a source said when it was used: when handing work to another agent, answering a dispute about a citation, or keeping an audit trail of the pages a task relied on. --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/guides/signed-page-receipt.md # How to get a signed receipt of a web page, step by step with curl Last updated: 2026-10-10 To get a signed, timestamped receipt of what a web page showed, send its URL to `POST /v1/fetch` (US$0.002 per call). AttestPage loads the page and returns an Ed25519-signed receipt with the final URL, HTTP status, fetch time, a hash of the page text and a verdict on what came back. This guide gets one receipt with `curl` and `jq` against `https://vehcdj664efetfrsolne5umanq.srv.us`, checks it, then shows the same call as an MCP tool. For what a receipt is and when an agent needs one, see [How to get a signed receipt of a web page](/docs/signed-receipt-of-a-web-page). ## What the receipt records The receipt is a compact JWS (`alg` EdDSA, `typ` `attestpage-evidence+jws`). Its payload has: - `kind` `fetch`, `iss` (the issuer, `https://still-rapids-9yt7.here.now`) and `iat` (the signing time, in Unix seconds); - `url`, the address you sent, and `final_url` after redirects, with `http_status`; - `retrieved_at`, the time of the fetch; - `content_sha256`, the SHA-256 of the full page text, and `raw_sha256`, the SHA-256 of the body as received; - `content_kind`: `real`, `bot_wall`, `js_shell`, `paywall`, `http_error` and others; - `server_ip`, `tls` and a short list of response `headers`, where our fetcher could read them; - `request_sha256`, the SHA-256 of your request body, so you can show which request it answers; - `tier` and `payment`, the payment that covered the call. ## Price US$0.002 per call, paid in USDC with x402 v2 on Base Sepolia (a test network). 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 a receipt for free The free sample runs the same route on a demo page and signs it with the sample key: ```sh curl -s https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/fetch > sample.json jq '{kind: .page.content_kind, status: .page.http_status, retrieved_at: .page.retrieved_at, content_sha256: .page.content_sha256, tier}' sample.json ``` ```json { "kind": "real", "status": 200, "retrieved_at": "2026-10-10T12:09:00.731Z", "content_sha256": "0e429607fdb16c005ef662414e758c53edb82e1cb24c3cbec239e4c1d6a7dfa0", "tier": "sample" } ``` A sample receipt is signed with a separate key and says `tier` `sample`, so it can never pass as evidence about a real page. ## 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/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}' ``` `amount` is in USDC base units (6 decimals), so `2000` is US$0.002. ## 3. Get the receipt Put the page URL in a file. `return_text` also returns up to 20,000 characters of clean text, which you need if you want to match the hash later: ```sh cat > body.json <<'EOF' { "url": "https://www.iana.org/help/example-domains", "return_text": true } 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` 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/quotes or check/links covers at most 3 citations, quotes or URLs; a larger batch is not run as a trial but answered with the normal 402 offer, `details.reason` trial_too_large. On the trial, the same call runs from `curl`: ```sh curl -s -X POST https://vehcdj664efetfrsolne5umanq.srv.us/v1/fetch -H 'content-type: application/json' \ -H 'quoteproof-trial: 1' -d @body.json > answer.json jq '{kind: .page.content_kind, status: .page.http_status, final_url: .page.final_url, tier}' answer.json ``` ## 4. Read what was fetched Read `page.content_kind` before relying on the receipt: | `content_kind` | What the receipt shows | |---|---| | `real` | The page loaded as content. The receipt records that content. | | `bot_wall`, `js_shell`, `paywall` | The site served a challenge, an empty JavaScript shell or a login wall. The receipt records that, not the article. | | `off_site` | The URL redirected to another site. `final_url` says where. | | `http_error`, `timeout` | The site answered with an error status, could not be reached or did not answer in time. | `advice` gives the same verdict in one sentence. Keep `answer.json` yourself: pages fetched for you are not stored ([Privacy](/privacy)). ## 5. Check the receipt The `receipt` field is the signed record. 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, tier, url: .payload.url, retrieved_at: .payload.retrieved_at}' ``` Or with no call to us at all, using the single-file checker pinned to our issuer: ```sh curl -O https://vehcdj664efetfrsolne5umanq.srv.us/dl/verify-receipt.mjs jq -r .receipt answer.json | node verify-receipt.mjs - --issuer https://still-rapids-9yt7.here.now ``` It prints `{valid, reason?, kid, tier, payload}` and exits 0 when the receipt is valid. Details: [Verify receipts offline](/docs/verify-offline). The keys are at [/.well-known/jwks.json](https://vehcdj664efetfrsolne5umanq.srv.us/.well-known/jwks.json). ## 6. Match the text to the receipt When `text_truncated` is false, the SHA-256 of `text` equals `content_sha256` in the receipt: ```sh jq -j .text answer.json | shasum -a 256 jq -r .page.content_sha256 answer.json ``` The two hashes match, so anyone holding that text and the receipt can see it is what our fetcher read at `retrieved_at`. ## With MCP The same receipt is the MCP tool `attestpage_fetch`, 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_fetch", "arguments": { "url": "https://www.iana.org/help/example-domains", "return_text": true }, "_meta": { "attestpage/trial": false } } } EOF ``` ```json { "scheme": "exact", "network": "eip155:84532", "amount": "2000" } ``` 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 receipt cost? US$0.002 per page with `POST /v1/fetch`. For up to 10 URLs under one receipt, `POST /v1/check/links` costs US$0.005 per batch: [Check links](/docs/check-links). ### Does the receipt show the page is accurate? No. It records what our fetcher received at that time. Pages change and can show different content to different visitors. ### Can I get a receipt for a file instead of a URL? Yes. [Attest a hash](/docs/attest) (US$0.002) signs a timestamp over a SHA-256 you send. We never see the file. --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/docs/check-links-before-citing.md # How to check links before citing them Last updated: 2026-10-10 Before citing a link, fetch it and confirm it returns real content, not a 404, a redirect to another site, a bot challenge or an empty page. `POST /v1/check/links` (US$0.005 per batch) checks up to 10 URLs at once and gives each one a status, redirect chain, final URL, `content_kind` verdict and hash, under one signed receipt. ## What can go wrong with a link - **Dead.** 404, 410, a 5xx error, or a host name that no longer resolves. - **Moved.** The link redirects, sometimes to a different site or to a home page that has nothing to do with the citation. Each result lists the redirects and the final URL; a link that ends on another site is `off_site`. - **Not readable.** The server answers 200 but sends a bot challenge, an empty JavaScript shell or a paywall. A status code alone calls these fine; `content_kind` does not. - **Invented.** LLMs sometimes produce URLs that look right but never existed. These usually fail as dead links or dead domains. ## Check a batch ```json { "urls": ["https://www.iana.org/help/example-domains", "https://www.iana.org/domains/reserved"] } ``` Keep a citation when `content_kind` is `real` and the final URL is still the page you meant. Try the route free on demo data: [`GET /v1/sample/links`](https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/links). Details: [Check links](/docs/check-links). Step by step with `curl`: [How to check links before citing them, step by step with curl](/guides/check-links-before-citing). ## Pay per call with x402 No account or key is needed. A valid request with no payment gets `402 Payment Required` with the price in a `PAYMENT-REQUIRED` header; an x402 v2 client signs that offer and sends the request again. With `@x402/fetch` (`npm install @x402/fetch @x402/evm viem`) and a Base Sepolia wallet made only for testing, holding test USDC: ```js import { wrapFetchWithPayment, x402Client } from '@x402/fetch'; import { registerExactEvmScheme } from '@x402/evm/exact/client'; import { privateKeyToAccount } from 'viem/accounts'; const client = new x402Client(); registerExactEvmScheme(client, { signer: privateKeyToAccount(process.env.ATTESTPAGE_X402_KEY) }); const payingFetch = wrapFetchWithPayment(fetch, client); const res = await payingFetch('https://vehcdj664efetfrsolne5umanq.srv.us/v1/check/links', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ urls: ['https://www.iana.org/help/example-domains', 'https://www.iana.org/domains/reserved'] }), }); const answer = await res.json(); for (const link of answer.results) console.log(link.content_kind, link.http_status, link.final_url); ``` The wrapper signs the offer it gets. To check the price and network before signing, see [One x402 payment step by step](/docs/examples/fetch-x402). ## Check the words too A live link can still be the wrong source. To check that the quoted words are on the page, use [Verify a quote](/docs/verify-quote), or [Verify citations](/docs/verify-citations) for up to 10 quote and URL pairs in one call. See [How to verify a quote an LLM cites](/docs/verify-a-quote-an-llm-cites). ## Questions ### Why not just send a HEAD request? Many servers answer HEAD differently from GET, block unknown clients, or return 200 for pages that only hold a challenge. We fetch the page as a reader would and look at what came back. ### Is a failed batch charged? If no URL in the batch resolves, the call gets 422 `host_not_found` and is not charged. Bad requests are refused before payment. See [Payments](/docs/payments) and [Errors](/docs/errors). ### Does it follow robots.txt? Yes. Our fetcher identifies itself and honours robots.txt and opt-outs; see [Our fetcher](/bot). ### Can I keep a record of the check? Yes. The receipt covers every URL in the batch and can be checked offline: [Verify receipts offline](/docs/verify-offline). --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/guides/check-links-before-citing.md # How to check links before citing them, step by step with curl Last updated: 2026-10-10 To check that the links you are about to cite are live, send up to 10 of them to `POST /v1/check/links` (US$0.005 per batch). AttestPage fetches each one and returns its status, redirects, final URL, title and a `content_kind` verdict, under one signed receipt. Then check that each page says what you claim with `POST /v1/verify/citations`. This guide runs both checks with `curl` and `jq` against `https://vehcdj664efetfrsolne5umanq.srv.us`, then shows the link check as an MCP tool call. For the kinds of broken link to look for, see [How to check links before citing them](/docs/check-links-before-citing). ## What the check does - **Fetches each link.** It follows up to 5 redirects and reads the page as our fetcher receives it at that moment. - **Judges each page.** `content_kind` is `real` when the page loaded as content, or `http_error`, `off_site`, `bot_wall`, `js_shell`, `paywall`, `timeout` and others when it did not. - **Reports what it found.** Each result has `http_status`, the `redirects` it followed, `final_url`, the page `title` and `content_sha256`, a hash of the page text. - **Signs a receipt.** One Ed25519 receipt covers every URL in the batch. Anyone can check it later against our published keys. ## Price US$0.005 per batch of 1 to 10 URLs, paid in USDC with x402 v2 on Base Sepolia (a test network). No account, key or sign-up. A request that fails validation gets 400 before payment and is never charged, and a batch where no host name resolves gets 422 and is not 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 two demo links, one live and one missing: ```sh curl -s https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/links | jq '.results[] | {url, status: .http_status, kind: .content_kind, title}' ``` ```json { "url": "https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/page", "status": 200, "kind": "real", "title": "Regional climate report 2025 (demo page)" } { "url": "https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/missing", "status": 404, "kind": "http_error", "title": null } ``` ## 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/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}' ``` `amount` is in USDC base units (6 decimals), so `5000` is US$0.005. ## 3. Check the links Put the links you plan to cite in a file, exactly as they will appear in your answer: ```sh cat > body.json <<'EOF' { "urls": ["https://www.iana.org/help/example-domains", "https://www.iana.org/domains/reserved"] } 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` 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/quotes or check/links covers at most 3 citations, quotes or URLs; a larger batch is not run as a trial but answered with the normal 402 offer, `details.reason` trial_too_large. On the trial, the same check runs from `curl`: ```sh curl -s -X POST https://vehcdj664efetfrsolne5umanq.srv.us/v1/check/links -H 'content-type: application/json' \ -H 'quoteproof-trial: 1' -d @body.json > answer.json jq '.results[] | {url, status: .http_status, kind: .content_kind, final_url, title}' answer.json ``` ## 4. Read the answer Read `content_kind` for each result, then compare `final_url` and `title` with what you meant to cite: | `content_kind` | What it means | What to do | |---|---|---| | `real` | The page loaded as content. | Keep it if `final_url` and `title` are the page you meant. | | `http_error` | A 4xx or 5xx status, or a host name that does not resolve (`http_status` null). | Drop the link or find where the page moved. | | `off_site` | The link redirected to another site. `final_url` says where; `off_site_error: true` means that site answered with an error. | Cite the final page only if it is the same source. | | `bot_wall`, `js_shell`, `paywall` | The site sent a challenge, an empty JavaScript shell or a login wall. | The link may work for a person, but its content is unchecked. | | `pdf` | A live PDF on the same site. It is not read, so `title` is null. | Check its text with [verify/quote](/docs/verify-quote). | | `timeout` | The site did not answer in time. | Try again later. | A link that redirects to a home page or a search page often still returns 200 and `real`. Check `redirects` and `title`, not the status alone. ## 5. Check that each page says what you claim A live link can still be the wrong source. Send each URL with the words you attribute to it to `POST /v1/verify/citations` (US$0.10 for up to 10 pairs): ```json { "citations": [ { "url": "https://www.iana.org/help/example-domains", "quote": "These domains may be used as illustrative examples in documents" } ] } ``` Each citation gets `exact`, `fuzzy` or `none`. If you paraphrased, add `"match": "passages"` beside `citations` to get the passages closest to your claim, as evidence to read. Details: [Verify citations](/docs/verify-citations). For one quote at a time, see [How to check a quote an LLM cites, step by step with curl](/guides/verify-llm-quote). ## 6. Keep the receipt The `receipt` field is a compact JWS covering every URL in the batch. 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). ## With MCP The same check is the MCP tool `attestpage_check_links`, 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_check_links", "arguments": { "urls": ["https://www.iana.org/help/example-domains", "https://www.iana.org/domains/reserved"] }, "_meta": { "attestpage/trial": false } } } EOF ``` ```json { "scheme": "exact", "network": "eip155:84532", "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 it cost to check a list of links? US$0.005 per batch of up to 10 URLs with `POST /v1/check/links`. Checking the quoted words too costs US$0.10 per batch of up to 10 citations with `POST /v1/verify/citations`. ### Is a batch charged if some links are dead? Yes, because reporting dead links is what the call does. If no host name in the batch resolves, the call gets 422 host_not_found and is not charged. Every case is in [Payments](/docs/payments) and [Errors](/docs/errors). ### Does a real result mean the link supports my claim? No. It means the page loaded as content when our fetcher read it. Whether it says what you claim is the separate check in step 5. ### Why not just send a HEAD request? Many servers answer HEAD differently from GET, block unknown clients, or return 200 for a challenge page. We fetch each page as a reader would and look at what came back. --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/docs/check-package-names-before-install.md # How to check a package name an LLM suggested before installing it Last updated: 2026-10-10 Before you install a package a model suggested, look the name up in its registry and stop if the registry does not list it. `POST /v1/check/packages` (US$0.005 per batch) checks up to 10 names on npm, PyPI or crates.io in one call and returns, for each, whether the registry lists it, its latest version, creation time and flags, under one signed receipt. ## Why invented names matter Models sometimes suggest package names that sound right but were never published. Running the install command then fails, or worse: someone can register that name later and publish their own code under it. Checking the name first catches the first case and shows you the flags worth a second look in the second. ## Check a batch ```json { "packages": [ { "ecosystem": "npm", "name": "left-pad" }, { "ecosystem": "pypi", "name": "requests" }, { "ecosystem": "crates", "name": "serde" } ] } ``` For each name: - `exists` is `false` when the registry answered 404: do not install it. - `signals` lists `created_recently` (under 30 days old), `deprecated`, `yanked` and `name_differs` (the registry spells the name differently). - `latest_version`, `license`, `homepage` and `repository` come from the registry, so you can compare them with what the model said. Try the route free on demo data: [`GET /v1/sample/packages`](https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/packages). Details: [Check packages](/docs/check-packages). ## Questions ### Does a listed package mean it is fine to install? No. The answer says whether the registry lists the name when we looked. A listed package can still be malicious, abandoned or not the one you meant. Read the signals and the publisher's details before you install. ### Which registries are covered? npm (including scoped names), PyPI and crates.io. Each name is checked against its registry's naming rules before payment, and a request with an invalid name gets 400 and is not charged. ### What if a registry is down? A failed lookup is reported per name with status lookup_failed and a reason. If every lookup in the batch fails, the call gets 502 and is not charged. See Payments and Errors in the docs. ### Can my coding agent call it from a sandbox? Yes, if the sandbox can reach our API. Agents can call it over HTTPS with x402 payment, or through the MCP server, which has one tool per route. --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/docs/tell-a-bot-wall-from-the-real-page.md # How to tell a bot wall from the real page when an agent fetches a URL Last updated: 2026-10-10 A 200 status does not mean the agent got the page. Check what came back before you summarise or quote it. `POST /v1/fetch` (US$0.002 per call) fetches one URL and returns a `content_kind` verdict (`real`, `bot_wall`, `js_shell`, `paywall`, `empty`, `http_error` and others), the clues behind it, the final URL, hashes and a signed receipt. ## What a fetch can return instead of the page - **Bot challenge.** A "checking your browser" page or a CAPTCHA. We report it as `bot_wall` and never try to get past it. - **JavaScript shell.** An almost empty HTML page whose content is drawn by scripts. Reported as `js_shell`. - **Paywall or login teaser.** The first paragraph, then a sign-in prompt. Reported as `paywall`. - **Redirect to another site.** A consent page or a home page on a different host. Reported as `off_site`. - **Error or empty page.** Reported as `http_error` or `empty`. Only `real` means the text is the page's content. On any other verdict, a summary or a missing quote says nothing about the page. ## Fetch a page ```json { "url": "https://www.iana.org/help/example-domains", "return_text": true } ``` With `return_text`, you also get up to 20,000 characters of clean text. The answer lists `injection_flags` when the text holds patterns often used for prompt injection. Try the route free on demo data: [`GET /v1/sample/fetch`](https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/fetch). Details: [Fetch a page](/docs/fetch) and [How it works](/docs/how-it-works). ## Questions ### How is the verdict decided? By automated rules over the status, headers and text. Rules can be wrong, so every answer lists the signals behind its verdict. ### Does it render JavaScript? No. A page that needs JavaScript is reported as js_shell rather than summarised as empty text, so the agent knows to try another source. ### Is a failed fetch charged? A result about the page is charged, whatever the verdict, including 404s, bot walls and timeouts: the verdict is the answer. A bad URL, a blocked address or a host name that does not resolve is refused before payment and not charged. See Payments and Errors in the docs. ### Can I show later what the page said? Yes. The receipt records the URL, final URL, status, verdict, text hash and time, signed with Ed25519, and anyone can check it offline. See How to get a signed receipt of a web page. --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/index.md # AttestPage Last updated: 2026-10-10 Signed evidence about web pages, for AI agents. Ask whether a quote appears on a page or check every citation in an answer at once, fetch a page and learn what actually came back, check a list of links, check that package names exist on npm, PyPI or crates.io, or timestamp a hash. Every answer carries an Ed25519-signed receipt that anyone can check offline against our published keys. A receipt records what our fetcher saw at a given time; it does not show that a statement on the page is correct. ## Endpoints | Route | Price per call | What you get | |---|---|---| | `POST /v1/verify/quote` | US$0.01 | your exact words checked against the page: exact, fuzzy or none match, offsets, page hashes, context, page verdict, receipt; or the passages best matching a paraphrased claim | | `POST /v1/verify/citations` | US$0.10 | up to 10 quote and URL pairs: the quote verdict per citation and the page verdict per URL, one receipt | | `POST /v1/verify/document` | US$0.10 per part | a whole answer or report: its quotes and links found and checked, 10 pairs a part, free preview first | | `POST /v1/fetch` | US$0.002 | content_kind verdict, status, final URL, title, hashes, injection flags, optional text, receipt | | `POST /v1/check/links` | US$0.005 | up to 10 URLs: status, redirects, content_kind, hash per URL, one receipt | | `POST /v1/check/packages` | US$0.005 | up to 10 npm, PyPI or crates.io names: exists in registry, latest version, metadata, one receipt (not a security verdict) | | `POST /v1/attest` | US$0.002 | signed timestamp over a SHA-256 you supply | | `POST /v1/receipt/verify` | free | checks a receipt against our keys | | `GET /v1/sample/{name}` | free | each paid route run on fixed demo data | Payment is x402 v2 in USDC on Base Sepolia, a test network. A request that fails validation gets 400 before any payment is asked for. See [Payments](/docs/payments) for when a call is charged. ## Start here 1. Call a free sample: `curl https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/quote` 2. Check its receipt: `curl -X POST https://vehcdj664efetfrsolne5umanq.srv.us/v1/receipt/verify -H 'content-type: application/json' -d '{"receipt":""}'` 3. Call a paid route without payment to see the 402 offer, then pay with any x402 v2 client. ## For agents - [llms.txt](https://vehcdj664efetfrsolne5umanq.srv.us/llms.txt) and [llms-full.txt](https://vehcdj664efetfrsolne5umanq.srv.us/llms-full.txt) - [OpenAPI 3.1](https://vehcdj664efetfrsolne5umanq.srv.us/openapi.json), [pricing.json](https://vehcdj664efetfrsolne5umanq.srv.us/pricing.json), [x402 discovery](https://vehcdj664efetfrsolne5umanq.srv.us/.well-known/x402) - Every page here is also markdown: add `.md` to the URL or send `Accept: text/markdown`. ## Read more - [Docs](/docs) - [How it works](/docs/how-it-works) - [Limits](/docs/limits) - [Our fetcher and how to opt out](/bot) --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/bot.md # Our fetcher Last updated: 2026-10-10 AttestPage fetches web pages when an agent asks it to check a quote or a list of citations, fetch a page or check links. This page describes that fetcher and how site owners can stop it. ## How to recognise it - User agent: `QuoteproofBot/1.0 (+https://vehcdj664efetfrsolne5umanq.srv.us/bot)` - robots.txt token: `QuoteproofBot` (matched without regard to case) - It fetches only the URL a caller sent, plus that site's `/robots.txt`. It does not crawl or follow links on its own. - For `check/packages` it also reads the public JSON APIs of registry.npmjs.org, pypi.org and crates.io for the names a caller sent, with the same user agent plus our contact address, and at most one crates.io request a second. ## What it does - GET requests only, without cookies, logins or any header passed through from the caller. - http and https on ports 80 and 443 only. It refuses private, loopback and other internal addresses, and checks again after every redirect. - Up to 5 redirects. It gives up after 5 seconds to connect and 10 seconds in all. - Reads at most 2 MB, and only HTML, plain text and JSON. - No JavaScript, no proxies, no IP rotation, and no attempt to get past CAPTCHAs or bot checks. A challenge page is reported to the caller as `bot_wall`. - At most 1 request a second and 30 a minute to any one host. - Page content is not stored. Receipts keep hashes of what was read, not the text. ## robots.txt The fetcher reads robots.txt before every fetch and keeps it for up to one hour. If there is a group for `QuoteproofBot`, only its rules apply; otherwise the `*` group applies. If your robots.txt answers with a server error (5xx), we treat the whole site as disallowed for an hour. To block it completely: ``` User-agent: QuoteproofBot Disallow: / ``` A disallowed page is refused to the caller with `403 robots_disallowed`, and the caller is not charged. ## Opt-out list We also keep a list of hosts that asked not to be fetched. Hosts on it are refused in the same way as a robots.txt disallow. ## Contact To ask about the fetcher or report a problem: no contact address is set on this server; the robots.txt opt-out works without one. Security reports: see [security.txt](https://vehcdj664efetfrsolne5umanq.srv.us/.well-known/security.txt). --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/terms.md # Terms of use Last updated: 2026-10-10 These terms cover use of AttestPage at https://vehcdj664efetfrsolne5umanq.srv.us, its API and its documents. By calling the API you accept them. ## The service AttestPage fetches web pages that you name and returns what it found, with a signed receipt. It also signs timestamped receipts over hashes you supply. Payments are on Base Sepolia, a test network. Features, prices and limits may change. Current prices are in [pricing.json](https://vehcdj664efetfrsolne5umanq.srv.us/pricing.json). ## What a receipt means A receipt is a signed record that our fetcher retrieved a URL at a stated time and saw the stated status, content hashes and verdict. It does not show that anything on the page is correct, that a page is free of harmful content, or that a person or organisation said what the page says. Verdicts such as `real`, `bot_wall` and `paywall` and the `injection_flags` list come from automated rules and can be wrong. An empty `injection_flags` list means none of our patterns matched, nothing more. An attest receipt shows that someone sent us a given SHA-256 at the stated time. It says nothing about what the hash stands for. ## Payment Paid routes cost the price shown in the 402 response. A request that fails validation is refused before payment. Payment is settled only when the route answers with a status below 400. Results about the target page, including errors, bot walls and timeouts, are answers and are charged. Where Lightning (L402) is on, you pay its invoice before the call: the token it gives you is used up only by an answer below 400, and it can be used until it expires (the time is in the token and in the [Payments](https://vehcdj664efetfrsolne5umanq.srv.us/docs/payments) page). A token that is never used is not refunded. Payments are not refunded except where this page or the docs say a call is not charged. ## Acceptable use Do not use the service to: - reach addresses you are not allowed to reach, or try to get around the fetcher's address checks; - send personal data, secrets or unlawful content in a note or quote; - overload a site, including by spreading requests to get round rate limits; - pay from an address on a sanctions list (such payments are refused). We may refuse or limit requests that break these terms. ## No warranty The service is provided as is, without warranty of any kind. To the extent the law allows, we are not liable for any loss from using it or relying on its results, and our total liability for any claim is limited to the amount you paid for the calls concerned. ## Changes We may change these terms. The date above shows the last change. --- Source: https://vehcdj664efetfrsolne5umanq.srv.us/privacy.md # Privacy Last updated: 2026-10-10 This page lists what AttestPage keeps about calls to https://vehcdj664efetfrsolne5umanq.srv.us and for how long. ## Access log For each request we log the time, method, path, status, duration, payment rail and its network, price, a coarse user-agent class (for example "ai_agent", "browser" or "crawler"), the name of a known AI agent where the user agent gives one (for example "gptbot"), the referrer's host name, a caller id, and a yes/no mark ("own") that says whether the request came from our own test and check scripts, so we can leave them out of our usage figures. The caller id is a keyed hash of your IP address under a random key held in memory only and replaced every UTC day, so it cannot be traced back to an address or linked across days. We do not log request bodies, query strings, target URLs, IP addresses or full user-agent strings. Log files are deleted after 14 days. ## Rate limits To apply per-address rate limits and trial limits, we hold a keyed hash of your IP address in memory only. The key is random and replaced every UTC day. It is never written to disk. ## Payments - For each settled payment we keep the time, route, network, amount, transaction hash and payment id. - A paid receipt contains the payment details, including the paying wallet address. You hold the receipt; see Attestations below for receipts we keep. - When a paying address is refused because it is on a sanctions list, we keep the time, route, network and that address. - To answer a retried payment with the same response, we keep the paid response in memory for one hour. - When a call's work is done but its payment fails to settle, we keep the time, route, payment id, signature nonce, paying address and, if reported, the failure reason and transaction hash, so the charge can be checked against the chain. If the payment had a payment id, the withheld response is also kept in memory for at least one hour, until the payment can no longer go through. - Where Lightning (L402) is on, we keep the payment hash of each used token and its expiry time until the token expires, so it cannot be used twice, and the paid response in memory until a restart, so the same request with that token gets it again. ## Attestations For `POST /v1/attest` we keep a sequence number, the time, a SHA-256 of your idempotency key and the paying address if you sent a key, the SHA-256 you sent, the SHA-256 of your note, and the signed receipt (which includes the paying address). We keep these so that a retry gets the same receipt. The note text itself is never stored. These records are kept with no set end date. ## Pages we fetch Pages fetched for you are not stored. We keep only what goes into the receipt you receive. That receipt records the IP address of the site we fetched (never yours), its TLS certificate details and a few of its response headers. robots.txt files are cached in memory for up to one hour. ## Sharing Payments go through an x402 facilitator and are recorded on a public blockchain, as all such payments are. When a verify/citations call cites a page with a DOI, that DOI (only the DOI) is sent to Crossref (api.crossref.org) to read its public metadata. We do not sell or share the data above with anyone else. ## Contact no contact address is set on this server; the robots.txt opt-out works without one.