How it works
requests, verdicts, caches.
The decision order for every request, how verdicts are reached, and the two setup topics that matter most in production: page caches and the real visitor IP behind a proxy.
01How a request is handled
For front-end requests (pages, posts, archives, feeds, search) that reach WordPress, in this order:
- CryptoMeter switched off, admin, AJAX, previews, or a logged-in user → untouched, not logged.
- robots.txt,
/license.xml,/.well-known/tdmrep.json→ always served. - Classification by user agent (or Web Bot Auth signature) and verification. Not a known bot and not a generic client → human: untouched, not logged.
- Policy Block (including spoofed bots while "Block spoofed bots" is on) → 403, site-wide.
- Over the bot's per-bot rate limit → 429.
- Search engine → passes. AI bot on a post (of a metered type) set to Blocked in the editor → 403. Policy Allow → reads normally.
- Policy Charge (Monetize): access rules → price → no wallet yet (let through, counted as unpaid demand) → access pass → per-IP limit for unpaid requests → 402 or payment processing.
| Visitor | Response | X-CryptoMeter |
|---|---|---|
| Human, logged-in user, generic client (default) | 200, normal page | none |
| Search engine (verified, or check undecided) | 200, normal page | none |
| AI bot set to Allow (on a post of a metered type) | 200, normal page | free |
| Blocked bot, spoofed bot, or post set to Blocked | 403 with your block message | block |
| Over a rate limit | 429, Retry-After: 60 | limited |
| Bot set to Charge (Monetize set up) | 402 + x402 challenge | challenge |
| Agent that paid or has a valid access pass | 200 + content | paid |
Verified on a test install with the default preset: GPTBot 403 (block), Bytespider 403, ChatGPT-User 200 (free), normal browser 200, curl's default user agent 200. A spoofed search-engine user agent never gets the search-engine pass; with "Block spoofed bots" off it is treated like a generic HTTP client.
02Verification explained
| Verdict | When |
|---|---|
| Verified | The IP is in the vendor's official list, or forward-confirmed reverse DNS matches the vendor's domain, or the request carries a valid Web Bot Auth signature from a trusted agent. |
| Spoofed | The bot has a verification method and the check definitely failed: the IP is in none of the (fresh) lists and reverse DNS does not confirm it, or reverse DNS gives a definite mismatch, or a signature is invalid for a bot without an IP list. |
| Unverified | The bot publishes no way to verify it (no list, no DNS, no signature), or you switched the methods off. |
| Unknown | The check could not decide: private or proxy IP, list not fetched yet or older than 3 days, DNS failure, or the DNS lookup budget (30 new lookups per minute) is used up. |
- Official IP lists are downloaded once a day (and with Refresh now), with a 10-second timeout and a 1 MB limit each, and stored with fetch time, count and status. On failure the last good copy is kept. A list older than 3 days is never used to call a request "Spoofed".
- Reverse DNS: the host name of the IP must end with the vendor's domain (for example
.googlebot.com,.search.msn.com,.applebot.apple.com) and resolve back to the same IP. Results are cached per IP for a day. - Web Bot Auth (RFC 9421 HTTP Message Signatures, Ed25519): headers
Signature,Signature-InputandSignature-Agent. Keys come fromhttps://chatgpt.com/.well-known/http-message-signatures-directory, cached a day. Only allow-listed agents are fetched (filtercryptometer_signature_agents). Needs PHP sodium. - No verification download happens during a visitor's request; lists and keys are fetched in the background.
- Behind a proxy or CDN, verification needs the real visitor IP: see Cloudflare and proxies. Local test requests (127.0.0.1) always show "Unknown".
Verified on a test install: Refresh now fetched all eight lists (18, 39, 229, 8, 4, 26, 317 and 28 ranges on 2026-09-24) and one key from chatgpt.com. Verified/spoofed verdicts for real crawler IPs are covered by the self-test with in-memory lists, not by live crawler traffic.
03Page caches
A full-page cache can answer a request before WordPress and CryptoMeter run. The bot is then not verified, not logged and not blocked. This is the most common setup problem, so please check it.
What CryptoMeter already does
- Every response it changes (403, 429, 402, allowed AI bot on a metered post, paid content) sets
DONOTCACHEPAGEand sendsCache-Control: no-store, private, max-age=0andVary: User-Agent, Accept, PAYMENT-SIGNATURE, X-PAYMENT. - It detects WP Rocket, LiteSpeed Cache, W3 Total Cache, WP Super Cache, WP Fastest Cache, SiteGround Speed Optimizer, Breeze, Cache Enabler, Hummingbird and
WP_CACHE, and warns on the Overview, in System status and on Server & cache rules. - The referral beacon is sent by the visitor's browser, so referrals are counted even on cached pages.
This stops CryptoMeter's responses from being cached, but it cannot stop a cache from serving a page that was cached earlier for a human.
What you configure
- Open Bots & rules → Server & cache rules and use the snippet for your cache: WP Rocket (Never Cache User Agents list), LiteSpeed (no-cache rule), Nginx (FastCGI cache bypass). For other caches (Varnish, host-managed caches, Cloudflare APO), exclude or bypass requests whose User-Agent contains the fragments from the WP Rocket tab, or vary the cache on User-Agent. Ask your host if unsure.
- With Monetize, also never answer requests carrying
PAYMENT-SIGNATURE,X-PAYMENTorX-CryptoMeter-Accessfrom cache. - Clear all caches, run the loopback test again, and check from outside with the curl command from the Quick start (expect 403 and
x-cryptometer: blockfor GPTBot with the default preset).
Search engines pass anyway, so a cache serving them does no harm, except that a fake "Googlebot" served from cache is not detected.
04Cloudflare and proxies
CryptoMeter needs the visitor's real IP for verification, IP allowlists, rate limits and the IP prefix in the log.
Cloudflare: automatic
When a request comes from a Cloudflare edge address (ranges from cloudflare.com/ips, built in as of 2026-09-23) and carries CF-Connecting-IP, that header is used as the visitor IP. Requests from other addresses cannot fake it. The Cloudflare row in System status confirms it. If another proxy sits in front of Cloudflare, or Cloudflare publishes new ranges, use a must-use plugin:
- Create
wp-content/mu-plugins/if it does not exist. - Create a file, for example
cryptometer-custom.php, starting with<?php, and add the snippet. Must-use plugins load automatically.
<?php
// Always trust CF-Connecting-IP (only if ALL traffic reaches your server through Cloudflare).
add_filter( 'cryptometer_trust_cloudflare', '__return_true' );
// Add a range Cloudflare publishes later.
add_filter( 'cryptometer_cloudflare_ranges', function ( $ranges ) {
$ranges[] = '198.51.100.0/24'; // example
return $ranges;
} );
Other proxies and load balancers
<?php
// Behind your own load balancer or reverse proxy (not Cloudflare).
add_filter( 'cryptometer_client_ip', function ( $ip ) {
$trusted_proxies = array( '10.0.0.5' ); // the address(es) of YOUR proxy
$remote = isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : '';
if ( in_array( $remote, $trusted_proxies, true ) && ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
$parts = explode( ',', $_SERVER['HTTP_X_FORWARDED_FOR'] );
$first = trim( $parts[0] );
if ( filter_var( $first, FILTER_VALIDATE_IP ) ) {
return $first;
}
}
return $ip;
} );
Never trust X-Forwarded-For from everyone: any client can send it and pretend to be an allowlisted IP or a verified bot.