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