CryptoMeter docs
v1.0.0
Get help
● Ship · Hooks and integrations

Developers
hooks, webhooks, REST.

Filters and actions, the payment webhook and its signature, the public files and REST routes, and every external service the plugin contacts.

01Developer hooks

Put customisations in a must-use plugin (how) or your own plugin, never in CryptoMeter's files.

HookArgumentsUse it to
cryptometer_botsarray $bots keyed by id: name, vendor, match, type (training, search_ai, agent, search), optional policy, rdns, signature_agentAdd or change bots
cryptometer_generic_clientsstring[]Change the generic HTTP client list
cryptometer_ip_sourcesarray id => {vendor, url, bots[]}Add or change official IP lists
cryptometer_ip_list_max_ageint seconds (default 3 days)How old a list may be to call a miss "Spoofed"
cryptometer_signature_agentsstring[] origins (default https://chatgpt.com)Trust more Web Bot Auth agents
cryptometer_rdns_per_minuteint, default 30Reverse-DNS lookup budget
cryptometer_referral_vendorsarray id => {label, hosts[], bot_vendor, bots[]|null}Add AI assistants for referral counting
cryptometer_beacon_per_minuteint, default 30Beacon limit per visitor
cryptometer_log_requestbool $log, array $clientSkip logging for some requests
cryptometer_spike_min_hitsint, default 50Minimum hourly requests for a spike alert
cryptometer_content_usagestring $value, array $configAdjust the Content-Usage value if the IETF draft changes
cryptometer_license_xmlstring $xml, array $configChange the RSL document
cryptometer_llms_txtstringChange /llms.txt
cryptometer_client_ip, cryptometer_trust_cloudflare, cryptometer_cloudflare_rangessee section 16Real visitor IP
cryptometer_pricefloat $usd, int $post_idFinal price (Monetize)
cryptometer_networks, cryptometer_facilitators, cryptometer_facilitator_request_args, cryptometer_max_failed_payments, cryptometer_licensesee the code commentsMonetize networks, facilitator presets, facilitator request args, failed-payment throttle (default 10 per IP per minute), licence line in paid content
cryptometer_payment_settled (action)$post_id, $settle, $client, $quoteReact to a settled payment

Examples

<?php
/**
 * Plugin Name: CryptoMeter customisations
 */

// 1. Recognise an extra AI crawler (appears in the AI training group).
add_filter( 'cryptometer_bots', function ( $bots ) {
	$bots['examplebot'] = array(
		'name'   => 'ExampleBot',
		'vendor' => 'Example Inc.',
		'match'  => 'ExampleBot', // case-insensitive user-agent substring
		'type'   => 'training',   // training | search_ai | agent | search
	);
	return $bots;
} );

// 2. Do not log requests to a health-check URL.
add_filter( 'cryptometer_log_request', function ( $log, $client ) {
	$uri = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : '';
	return false === strpos( $uri, '/healthz' ) ? $log : false;
}, 10, 2 );

// 3. Count visits from another AI assistant.
add_filter( 'cryptometer_referral_vendors', function ( $vendors ) {
	$vendors['example'] = array(
		'label'      => 'Example AI',
		'hosts'      => array( 'chat.example.ai' ),
		'bot_vendor' => 'Example Inc.', // compare with bots of this vendor
		'bots'       => null,           // or an explicit list of bot ids
	);
	return $vendors;
} );

// 4. Add a section to /llms.txt.
add_filter( 'cryptometer_llms_txt', function ( $txt ) {
	return $txt . "\n## Licensing contact\n\nlicensing@example.com\n";
} );

Hook names and arguments were checked against the code. Examples 2 and 3 were reviewed, not run. The referral beacon script lists the vendor hosts when the page is rendered, so a vendor added with the filter is included automatically.

02Webhooks and signatures

After every settled Monetize payment CryptoMeter sends a JSON POST to your webhook URL (Settings → Notifications). Only https://; private and internal addresses are refused (WordPress "safe" HTTP API); redirects are not followed; sent in the background with a short timeout and not retried (use the ledger or CSV as the source of truth). Headers: Content-Type: application/json, User-Agent: CryptoMeter/1.0.0, X-CryptoMeter-Event (payment.settled or test) and, with a secret, X-CryptoMeter-Signature: sha256=<hex>.

{
  "event": "payment.settled",
  "id": "423618ba-fa43-48b0-b59e-4d8acd8b362b",
  "created_at": "2026-09-23T09:59:28+00:00",
  "mode": "test",
  "site": "https://your-site.example/",
  "post": { "id": 4, "title": "Your post", "url": "https://your-site.example/your-post/" },
  "amount_usdc": "0.01",
  "network": "eip155:84532",
  "network_label": "Base Sepolia (testnet)",
  "transaction": "0x...",
  "explorer": "https://sepolia.basescan.org/tx/0x...",
  "payer": "0x...",
  "bot": "GPTBot",
  "bot_type": "training"
}

The test button sends "event": "test". The weekly report and spike alert are e-mail only; they are not sent to the webhook.

Verify the signature

HMAC-SHA256 of the raw body with your secret, in hex, prefixed with sha256=. Compare in constant time.

<?php
// webhook-receiver.php on YOUR server (not inside WordPress).
$secret   = getenv( 'CRYPTOMETER_WEBHOOK_SECRET' );
$body     = file_get_contents( 'php://input' );
$sent     = isset( $_SERVER['HTTP_X_CRYPTOMETER_SIGNATURE'] ) ? $_SERVER['HTTP_X_CRYPTOMETER_SIGNATURE'] : '';
$expected = 'sha256=' . hash_hmac( 'sha256', $body, $secret );
if ( ! hash_equals( $expected, $sent ) ) {
	http_response_code( 401 );
	exit( 'bad signature' );
}
$event = json_decode( $body, true );
http_response_code( 200 );
echo 'ok';
// receiver.mjs (Node.js, no dependencies)
import http from 'node:http';
import crypto from 'node:crypto';
const SECRET = process.env.CRYPTOMETER_WEBHOOK_SECRET;
http.createServer( ( req, res ) => {
	const chunks = [];
	req.on( 'data', ( c ) => chunks.push( c ) );
	req.on( 'end', () => {
		const raw = Buffer.concat( chunks );
		const expected = 'sha256=' + crypto.createHmac( 'sha256', SECRET ).update( raw ).digest( 'hex' );
		const sent = String( req.headers[ 'x-cryptometer-signature' ] || '' );
		const ok = sent.length === expected.length &&
			crypto.timingSafeEqual( Buffer.from( sent ), Buffer.from( expected ) );
		if ( ! ok ) { res.writeHead( 401 ).end( 'bad signature' ); return; }
		const event = JSON.parse( raw.toString( 'utf8' ) );
		console.log( event.event, event.amount_usdc, event.transaction );
		res.writeHead( 200 ).end( 'ok' );
	} );
} ).listen( 8787 );

Both receivers were tested against a signature produced by the plugin's code in the earlier verification round (2026-09-23); the signing code is unchanged in this version.

03Public files and REST API

Public files

AddressWhenContent
/robots.txtAlways (WordPress virtual robots.txt; site visible to search engines)Your robots.txt plus Disallow blocks for blocked bots, Content-Signal / Content-Usage lines and, with RSL, a License: line.
/llms.txtPublish /llms.txt onPlain text, cached 1 hour. Workaround under plain permalinks: /?cryptometer_llms=1.
/license.xmlRSL onapplication/rsl+xml, otherwise 404.
/.well-known/tdmrep.jsonTDMRep onJSON, otherwise 404.

Verified on a test install: /llms.txt 200; /license.xml and /.well-known/tdmrep.json 404 by default and 200 after switching them on.

REST API

Namespace /wp-json/cryptometer/v1 (with plain permalinks: /?rest_route=/cryptometer/v1/…).

RouteAccessPurpose
GET /catalogpublic (bots set to Block get 403)Machine-readable list of articles (page, per_page 1-100, search). payments.enabled is false and prices are 0.00 while Monetize is not set up.
GET /articles/{id}public, always metered (bots set to Block get 403)Monetize's paid article API: 402 → pay → JSON. 404 cryptometer_not_found, 403 cryptometer_blocked (post set to Blocked), 503 cryptometer_disabled (CryptoMeter off). Editors of the post get it free.
POST /beaconpublicReferral beacon; always answers 204.
/admin/*administrators (manage_options + REST nonce); anonymous calls get 401Power the admin screens (overview, activity, activity/live, referrals, bots, verification, verification/refresh, signals, signals/preview, rules, settings, monetize, privacy/erase, simulate, facilitator, webhook-test, live-test). Not meant for external use.

Core WordPress REST API and oEmbed: bots set to Block get HTTP 403 with your block message from every /wp-json/wp/v2/… and /wp-json/oembed/… route, the CryptoMeter catalog and the article API. For charged bots (Monetize), /wp-json/wp/v2/… of metered post types returns a teaser with content.protected: true and content.x402 pointing to the paid endpoint.

04External services and privacy

CryptoMeter contacts these services. No data about your visitors is sent. Requests identify themselves as CryptoMeter/<version> (WordPress; <your site URL>). Every request has a timeout, and a failure never affects your visitors (the last good copy is kept).

ServiceWhenWhat is sentSwitch
Official bot IP lists:
openai.com/gptbot.json
openai.com/searchbot.json
openai.com/chatgpt-user.json
www.perplexity.ai/perplexitybot.json
www.perplexity.ai/perplexity-user.json
claude.com/crawling/bots.json
developers.google.com/static/crawling/ipranges/common-crawlers.json
www.bing.com/toolbox/bingbot.json
Once a day by WP-Cron and shortly after activation/update (both only while the switch is on), and on "Refresh now"A plain GET (your site URL is in the User-Agent)Check official IP lists (turning it off also stops the scheduled download; "Refresh now" still fetches on demand)
Web Bot Auth key directory https://chatgpt.com/.well-known/http-message-signatures-directoryAt most once a day (daily refresh, or after a signed request when the cached keys are older than a day)A plain GETCheck signed requests (Web Bot Auth)
Reverse DNSWhen an IP claims to be a search engine or a Google AI bot; cached a day; max 30 new lookups a minuteThe IP, to your server's own DNS resolverVerify search engines by reverse DNS
x402 facilitator (Monetize only)When an agent pays, and when you press Check facilitatorThe agent's payment payloadMonetize off
Your webhook URL (only if set)After a settled payment, and the test buttonThe payment eventLeave empty

Terms: those of each vendor's or service's website. The admin app and this documentation load no external fonts or scripts.

What is stored

WhereWhat
{prefix}cryptometer_logOne row per bot request: time, bot, intent, verdict, method, decision, status, bytes, post, path (max 255 characters), IP network prefix (IPv4 /24, IPv6 /48), robots.txt violation flag, mode. Never humans. 90 days by default.
{prefix}cryptometer_referralsDay × assistant × post counters. No IP, no cookie.
{prefix}cryptometer_hitsDaily counters per post, bot and outcome. No IP.
{prefix}cryptometer_paymentsMonetize payments (payer wallet address, amount, network, transaction, status and so on).
Optionscryptometer_settings (secrets masked in forms, never exported), cryptometer_secret, cryptometer_errors, cached IP lists cryptometer_iplist_* and key directories cryptometer_sigdir_* (not autoloaded).
Transients cryptometer_*Rate-limit counters (hashed keys, about 2 minutes), reverse-DNS results per IP hash (1 day), cached robots.txt rules (1 hour).

An IP network prefix and a payer wallet address can count as personal data in some jurisdictions; mention them in your privacy policy if that applies to you (see the suggested text under Settings → Privacy).