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.
| Hook | Arguments | Use it to |
|---|---|---|
cryptometer_bots | array $bots keyed by id: name, vendor, match, type (training, search_ai, agent, search), optional policy, rdns, signature_agent | Add or change bots |
cryptometer_generic_clients | string[] | Change the generic HTTP client list |
cryptometer_ip_sources | array id => {vendor, url, bots[]} | Add or change official IP lists |
cryptometer_ip_list_max_age | int seconds (default 3 days) | How old a list may be to call a miss "Spoofed" |
cryptometer_signature_agents | string[] origins (default https://chatgpt.com) | Trust more Web Bot Auth agents |
cryptometer_rdns_per_minute | int, default 30 | Reverse-DNS lookup budget |
cryptometer_referral_vendors | array id => {label, hosts[], bot_vendor, bots[]|null} | Add AI assistants for referral counting |
cryptometer_beacon_per_minute | int, default 30 | Beacon limit per visitor |
cryptometer_log_request | bool $log, array $client | Skip logging for some requests |
cryptometer_spike_min_hits | int, default 50 | Minimum hourly requests for a spike alert |
cryptometer_content_usage | string $value, array $config | Adjust the Content-Usage value if the IETF draft changes |
cryptometer_license_xml | string $xml, array $config | Change the RSL document |
cryptometer_llms_txt | string | Change /llms.txt |
cryptometer_client_ip, cryptometer_trust_cloudflare, cryptometer_cloudflare_ranges | see section 16 | Real visitor IP |
cryptometer_price | float $usd, int $post_id | Final price (Monetize) |
cryptometer_networks, cryptometer_facilitators, cryptometer_facilitator_request_args, cryptometer_max_failed_payments, cryptometer_license | see the code comments | Monetize 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, $quote | React 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
| Address | When | Content |
|---|---|---|
/robots.txt | Always (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.txt | Publish /llms.txt on | Plain text, cached 1 hour. Workaround under plain permalinks: /?cryptometer_llms=1. |
/license.xml | RSL on | application/rsl+xml, otherwise 404. |
/.well-known/tdmrep.json | TDMRep on | JSON, 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/…).
| Route | Access | Purpose |
|---|---|---|
GET /catalog | public (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 /beacon | public | Referral beacon; always answers 204. |
/admin/* | administrators (manage_options + REST nonce); anonymous calls get 401 | Power 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).
| Service | When | What is sent | Switch |
|---|---|---|---|
Official bot IP lists:openai.com/gptbot.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-directory | At most once a day (daily refresh, or after a signed request when the cached keys are older than a day) | A plain GET | Check signed requests (Web Bot Auth) |
| Reverse DNS | When an IP claims to be a search engine or a Google AI bot; cached a day; max 30 new lookups a minute | The IP, to your server's own DNS resolver | Verify search engines by reverse DNS |
| x402 facilitator (Monetize only) | When an agent pays, and when you press Check facilitator | The agent's payment payload | Monetize off |
| Your webhook URL (only if set) | After a settled payment, and the test button | The payment event | Leave 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
| Where | What |
|---|---|
{prefix}cryptometer_log | One 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_referrals | Day × assistant × post counters. No IP, no cookie. |
{prefix}cryptometer_hits | Daily counters per post, bot and outcome. No IP. |
{prefix}cryptometer_payments | Monetize payments (payer wallet address, amount, network, transaction, status and so on). |
| Options | cryptometer_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).