# Pay per crawl

> Handle 402 Payment Required from a site that prices access, with crawler-price headers compatible with Cloudflare pay per crawl.

A site can put a price on some actions for some agents. The headers follow
Cloudflare's pay-per-crawl convention, so an agent that already handles it
works unchanged.

> [!NOTE]
> AgentGate signals prices only. It processes no payment: the site operator
> settles out of band from the decision log. Built-in settlement is
> [planned](/docs/crawl-settlement).

## The price

A priced request without an acceptable offer is answered:

```http
HTTP/1.1 402 Payment Required
crawler-price: USD 0.01
crawler-error: MissingCrawlerPrice
```

## Accepting the price

Retry with **one** of these headers:

| Header | Meaning |
| --- | --- |
| `crawler-exact-price: USD 0.01` | must equal the price (value and currency) |
| `crawler-max-price: USD 0.05` | the most you will pay; you are charged the listed price, not your maximum |

Your Web Bot Auth signature must cover the header you send, for example by
adding `"crawler-exact-price"` to the covered components. Unverified agents
cannot pay. On success the response carries the amount charged:

```http
HTTP/1.1 200 OK
crawler-charged: USD 0.01
AgentGate-Receipt: eyJhbGciOiJFZERTQSIs…
```

The decision is recorded with the label `agentgate:agent:payment:accepted`,
which the operator uses for settlement; the signed
[receipt](/docs/agent-receipts) is your proof of what was charged.

## Errors

| `crawler-error` | Status | Meaning |
| --- | --- | --- |
| `MissingCrawlerPrice` | 402 | no offer sent |
| `InvalidCrawlerExactPrice` | 402 | the exact offer differs from the price (value or currency) |
| `InvalidCrawlerMaxPrice` | 402 | the maximum is below the price |
| `InvalidCrawlerPriceValue` | 400 | not `CUR 0.00` |
| `ConflictingPriceHeaders` | 400 | both offer headers sent |
| `StrongAuthRequired` | 400 or 402 | the offer is not covered by a signature, or you are not verified |

Behind the nginx gateway, the price travels in the same headers and the
status is `402` (the check answers `403` with reason `payment_required` and
the example configuration turns it into `402`).
