> */ public static function catalog() { return array( // ---- OpenAI ---- array( 'slug' => 'chatgpt-user', 'name' => 'ChatGPT-User', 'company' => 'OpenAI', 'platform' => 'chatgpt', 'function' => 'live-fetch', 'regex' => '~ChatGPT-User~i' ), array( 'slug' => 'oai-searchbot', 'name' => 'OAI-SearchBot', 'company' => 'OpenAI', 'platform' => 'chatgpt', 'function' => 'citation-index', 'regex' => '~OAI-SearchBot~i' ), array( 'slug' => 'gptbot', 'name' => 'GPTBot', 'company' => 'OpenAI', 'platform' => 'chatgpt', 'function' => 'training', 'regex' => '~GPTBot~i' ), // ---- Anthropic ---- array( 'slug' => 'claude-user', 'name' => 'Claude-User', 'company' => 'Anthropic', 'platform' => 'claude', 'function' => 'live-fetch', 'regex' => '~Claude-User~i' ), array( 'slug' => 'claude-searchbot', 'name' => 'Claude-SearchBot', 'company' => 'Anthropic', 'platform' => 'claude', 'function' => 'citation-index', 'regex' => '~Claude-SearchBot~i' ), array( 'slug' => 'claudebot', 'name' => 'ClaudeBot', 'company' => 'Anthropic', 'platform' => 'claude', 'function' => 'training', 'regex' => '~ClaudeBot~i' ), array( 'slug' => 'anthropic-ai', 'name' => 'anthropic-ai', 'company' => 'Anthropic', 'platform' => 'claude', 'function' => 'training', 'regex' => '~anthropic-ai~i' ), // ---- Perplexity ---- array( 'slug' => 'perplexity-user', 'name' => 'Perplexity-User', 'company' => 'Perplexity', 'platform' => 'perplexity', 'function' => 'live-fetch', 'regex' => '~Perplexity-User~i' ), array( 'slug' => 'perplexitybot', 'name' => 'PerplexityBot', 'company' => 'Perplexity', 'platform' => 'perplexity', 'function' => 'citation-index', 'regex' => '~PerplexityBot~i' ), // ---- Google / Gemini ---- array( 'slug' => 'google-extended', 'name' => 'Google-Extended', 'company' => 'Google', 'platform' => 'google-ai', 'function' => 'training', 'regex' => '~Google-Extended~i' ), array( 'slug' => 'googleother', 'name' => 'GoogleOther', 'company' => 'Google', 'platform' => 'google-ai', 'function' => 'training', 'regex' => '~GoogleOther~i' ), array( 'slug' => 'googlebot', 'name' => 'Googlebot', 'company' => 'Google', 'platform' => 'google-ai', 'function' => 'search-engine', 'regex' => '~Googlebot~i' ), // ---- Microsoft / Copilot ---- array( 'slug' => 'bingbot', 'name' => 'Bingbot', 'company' => 'Microsoft', 'platform' => 'copilot', 'function' => 'search-engine', 'regex' => '~bingbot~i' ), // ---- Apple ---- array( 'slug' => 'applebot-extended', 'name' => 'Applebot-Extended', 'company' => 'Apple', 'platform' => 'other', 'function' => 'training', 'regex' => '~Applebot-Extended~i' ), array( 'slug' => 'applebot', 'name' => 'Applebot', 'company' => 'Apple', 'platform' => 'other', 'function' => 'search-engine', 'regex' => '~Applebot~i' ), // ---- Meta ---- array( 'slug' => 'meta-externalagent', 'name' => 'Meta-ExternalAgent', 'company' => 'Meta', 'platform' => 'meta-ai', 'function' => 'training', 'regex' => '~meta-externalagent~i' ), array( 'slug' => 'facebookbot', 'name' => 'FacebookExternalHit', 'company' => 'Meta', 'platform' => 'meta-ai', 'function' => 'live-fetch', 'regex' => '~facebookexternalhit~i' ), // ---- ByteDance ---- array( 'slug' => 'bytespider', 'name' => 'Bytespider', 'company' => 'ByteDance', 'platform' => 'training', 'function' => 'training', 'regex' => '~Bytespider|TikTokSpider~i' ), // ---- Amazon ---- array( 'slug' => 'amzn-user', 'name' => 'Amzn-User', 'company' => 'Amazon', 'platform' => 'other', 'function' => 'live-fetch', 'regex' => '~Amzn-User~i' ), array( 'slug' => 'amzn-searchbot', 'name' => 'Amzn-SearchBot', 'company' => 'Amazon', 'platform' => 'other', 'function' => 'citation-index', 'regex' => '~Amzn-SearchBot~i' ), array( 'slug' => 'amazonbot', 'name' => 'Amazonbot', 'company' => 'Amazon', 'platform' => 'other', 'function' => 'training', 'regex' => '~Amazonbot~i' ), // ---- Datasets / training crawlers ---- array( 'slug' => 'ccbot', 'name' => 'CCBot', 'company' => 'Common Crawl', 'platform' => 'training', 'function' => 'training', 'regex' => '~CCBot~i' ), array( 'slug' => 'ai2bot', 'name' => 'AI2Bot', 'company' => 'Allen Institute for AI', 'platform' => 'training', 'function' => 'training', 'regex' => '~AI2Bot~i' ), array( 'slug' => 'cohere-ai', 'name' => 'Cohere', 'company' => 'Cohere', 'platform' => 'training', 'function' => 'training', 'regex' => '~cohere-ai|cohere-training-data-crawler~i' ), array( 'slug' => 'laion', 'name' => 'LAION', 'company' => 'LAION', 'platform' => 'training', 'function' => 'training', 'regex' => '~LAIONDownloader|laion-huggingface-processor~i' ), array( 'slug' => 'omgili', 'name' => 'Omgili', 'company' => 'Webz.io', 'platform' => 'training', 'function' => 'training', 'regex' => '~omgili~i' ), array( 'slug' => 'timpibot', 'name' => 'TimpiBot', 'company' => 'Timpi', 'platform' => 'training', 'function' => 'training', 'regex' => '~Timpibot~i' ), // ---- Other AI search ---- array( 'slug' => 'youbot', 'name' => 'YouBot', 'company' => 'You.com', 'platform' => 'other-ai-search', 'function' => 'citation-index', 'regex' => '~YouBot~i' ), array( 'slug' => 'duckassistbot', 'name' => 'DuckAssistBot', 'company' => 'DuckDuckGo', 'platform' => 'other-ai-search', 'function' => 'live-fetch', 'regex' => '~DuckAssistBot~i' ), array( 'slug' => 'phindbot', 'name' => 'PhindBot', 'company' => 'Phind', 'platform' => 'other-ai-search', 'function' => 'citation-index', 'regex' => '~PhindBot~i' ), array( 'slug' => 'kagibot', 'name' => 'KagiBot', 'company' => 'Kagi', 'platform' => 'other-ai-search', 'function' => 'citation-index', 'regex' => '~Kagibot~i' ), // ---- Huawei ---- array( 'slug' => 'pangubot', 'name' => 'PanguBot', 'company' => 'Huawei', 'platform' => 'other', 'function' => 'training', 'regex' => '~PanguBot~i' ), array( 'slug' => 'petalbot', 'name' => 'PetalBot', 'company' => 'Huawei', 'platform' => 'other', 'function' => 'search-engine', 'regex' => '~PetalBot~i' ), // ---- Indexing for AI APIs ---- array( 'slug' => 'shapbot', 'name' => 'ShapBot', 'company' => 'Parallel.ai', 'platform' => 'other', 'function' => 'citation-index', 'regex' => '~ShapBot~i' ), // ---- Agentic browsers ---- array( 'slug' => 'tavilybot', 'name' => 'TavilyBot', 'company' => 'Tavily', 'platform' => 'agentic', 'function' => 'agentic', 'regex' => '~TavilyBot~i' ), array( 'slug' => 'devin', 'name' => 'Devin', 'company' => 'Cognition Labs', 'platform' => 'agentic', 'function' => 'agentic', 'regex' => '~Devin~i' ), ); } /** * Human labels + accent colors for platform groups (the violet Aiso palette). * * @return array> */ public static function platforms() { return array( 'chatgpt' => array( 'label' => 'ChatGPT (OpenAI)', 'color' => '#10a37f' ), 'claude' => array( 'label' => 'Claude (Anthropic)', 'color' => '#d97706' ), 'perplexity' => array( 'label' => 'Perplexity', 'color' => '#6d5ae8' ), 'google-ai' => array( 'label' => 'Google / Gemini', 'color' => '#4285f4' ), 'copilot' => array( 'label' => 'Microsoft Copilot', 'color' => '#0078d4' ), 'meta-ai' => array( 'label' => 'Meta AI', 'color' => '#0866ff' ), 'other-ai-search' => array( 'label' => 'Other AI Search', 'color' => '#8b7afc' ), 'training' => array( 'label' => 'Training Datasets', 'color' => '#64748b' ), 'agentic' => array( 'label' => 'Agentic Browsers', 'color' => '#db2777' ), 'other' => array( 'label' => 'Other / Misc', 'color' => '#94a3b8' ), ); } /** * Human labels for function tags. "Chat impression" is the closest equivalent * to an impression inside an AI answer, which is the headline metric. * * @return array> */ public static function functions() { return array( 'live-fetch' => array( 'label' => 'Chat impression', 'color' => '#16a34a', 'desc' => 'On-demand fetch while an AI chatbot reads your URL during a live conversation — the closest thing to an impression inside the chat.' ), 'citation-index' => array( 'label' => 'Citation index', 'color' => '#6d5ae8', 'desc' => 'Search-style indexer that decides which pages get cited in AI answers.' ), 'training' => array( 'label' => 'Training', 'color' => '#64748b', 'desc' => 'Collects pages used to train an LLM.' ), 'search-engine' => array( 'label' => 'Search engine', 'color' => '#0078d4', 'desc' => 'Traditional search crawler that also feeds the vendor’s AI features.' ), 'agentic' => array( 'label' => 'Agent / browser', 'color' => '#db2777', 'desc' => 'Headless agent browsing the live web on behalf of a user or another AI.' ), ); } /** * Detect the AI crawler behind a User-Agent string. * * @param string $user_agent Raw User-Agent. * @return array|null Matched catalog entry (with is_impression), or null. */ public static function detect( $user_agent ) { if ( ! is_string( $user_agent ) || '' === $user_agent ) { return null; } foreach ( self::catalog() as $bot ) { if ( preg_match( $bot['regex'], $user_agent ) ) { $bot['is_impression'] = ( 'live-fetch' === $bot['function'] ); return $bot; } } return null; } /** * Published IP ranges (CIDR) per company, from the Aiso bot directory. Used as a * lightweight spoof check: a real GPTBot request originates from OpenAI's ranges. * Companies that publish no static list (e.g. Google verifies by reverse-DNS) are * intentionally absent, in which case verification returns null ("unverifiable"). * * @return array */ public static function ip_ranges() { return array( 'OpenAI' => array( '20.171.206.0/24', '20.171.207.0/24', '40.84.180.0/24', '40.84.181.0/24', '52.230.152.0/24', '52.233.106.0/24', '20.106.183.128/28', '13.65.138.112/28', '157.55.132.80/28', '157.56.88.96/28', '40.84.186.208/28', '52.167.144.176/28', '23.98.179.16/28', '172.183.222.128/28', '52.190.190.16/28' ), 'Microsoft' => array( '157.55.0.0/16', '157.56.0.0/16', '13.64.0.0/11', '40.74.0.0/15', '52.145.0.0/16', '20.80.0.0/12' ), 'Perplexity' => array( '54.176.188.0/25', '34.224.0.0/12', '3.208.0.0/12', '52.0.0.0/10' ), 'Amazon' => array( '100.24.190.202/32', '100.25.127.167/32', '100.25.153.201/32', '107.20.124.65/32', '13.216.102.104/32', '13.219.170.211/32' ), 'Meta' => array( '31.13.24.0/21', '31.13.64.0/18', '57.144.0.0/14', '66.220.144.0/20', '69.63.176.0/20', '69.171.224.0/19', '129.134.0.0/17', '157.240.0.0/17', '163.70.128.0/17' ), 'Common Crawl' => array( '18.97.9.168/29', '18.97.14.80/29', '98.85.178.216/32' ), 'Parallel.ai' => array( '34.60.130.210/32', '34.136.80.156/32', '104.198.36.41/32', '104.154.193.69/32', '34.135.143.163/32', '34.135.191.216/32', '34.42.5.16/32', '34.70.203.11/32' ), 'Huawei' => array( '114.119.128.0/18' ), ); } /** * Verify a hit's source IP against the crawler company's published ranges. * * @param string $ip IPv4 address. * @param string $company Crawler company. * @return bool|null true = in published range, false = outside (possible spoof), null = unverifiable. */ public static function verify_ip( $ip, $company ) { $ranges = self::ip_ranges(); if ( empty( $ranges[ $company ] ) ) { return null; // No published list -> cannot cheaply verify. } if ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) { return null; // Only IPv4 ranges are published; skip IPv6. } foreach ( $ranges[ $company ] as $cidr ) { if ( self::ip_in_cidr( $ip, $cidr ) ) { return true; } } return false; } /** * Plain IPv4 CIDR membership test. * * @param string $ip IPv4 address. * @param string $cidr CIDR block, e.g. 20.171.206.0/24. * @return bool */ private static function ip_in_cidr( $ip, $cidr ) { if ( false === strpos( $cidr, '/' ) ) { return $ip === $cidr; } list( $subnet, $bits ) = explode( '/', $cidr, 2 ); $bits = (int) $bits; $ip_long = ip2long( $ip ); $subnet_long = ip2long( $subnet ); if ( false === $ip_long || false === $subnet_long ) { return false; } if ( $bits <= 0 ) { return true; } $mask = -1 << ( 32 - $bits ); return ( $ip_long & $mask ) === ( $subnet_long & $mask ); } }