AI crawler tracking
Updated October 18, 2026
AI crawlers do not run JavaScript, so the Statsdot script never sees them. To know which of them read your site, your server reports their requests to Statsdot. You then see, in the report of the site, each crawler with its number of requests and the pages crawlers read most.
Only requests whose user agent matches a crawler of the AI crawler directory are stored, with the crawler, the path without query string and the status code. Requests from people are ignored even if your server sends them, and no IP address is stored.
1. Turn it on
In the settings of the site, under AI crawler tracking, choose Turn on and create a key. Copy the key (it starts with sdb_). The key can only report crawler requests; rotate it in the same place if it leaks.
2. Connect your site
WordPress
In Settings > Statsdot of the plugin, paste the key in AI crawler tracking and save. That is all.
Laravel
Add this middleware to the web group. It reports after the response has been sent, so crawlers are not slowed down:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Symfony\Component\HttpFoundation\Response;
class ReportAiCrawlers
{
private const TOKENS = ['gptbot', 'oai-searchbot', 'chatgpt-user', 'claudebot', 'claude-user', 'claude-searchbot', 'perplexitybot', 'perplexity-user', 'google-extended', 'googlebot', 'bingbot', 'applebot', 'meta-externalagent', 'bytespider', 'ccbot', 'amazonbot', 'duckassistbot', 'mistralai-user'];
public function handle(Request $request, Closure $next): Response
{
return $next($request);
}
public function terminate(Request $request, Response $response): void
{
$agent = strtolower((string) $request->userAgent());
foreach (self::TOKENS as $token) {
if (str_contains($agent, $token)) {
Http::timeout(2)->withHeaders(['X-Statsdot-Bot-Key' => config('services.statsdot.bot_key')])
->post('https://statsdot.com/api/bots', [
'user_agent' => $request->userAgent(),
'url' => $request->fullUrl(),
'status' => $response->getStatusCode(),
]);
return;
}
}
}
}
Put the key in .env as STATSDOT_BOT_KEY and in config/services.php as 'statsdot' => ['bot_key' => env('STATSDOT_BOT_KEY')]. Statsdot recognises more crawlers than the short list above; extend it with the tokens of the directory you care about.
Plain PHP
At the end of your front controller (for example index.php), after the page has been sent:
$agent = $_SERVER['HTTP_USER_AGENT'] ?? '';
if (preg_match('/gptbot|oai-searchbot|chatgpt-user|claudebot|claude-user|claude-searchbot|perplexitybot|perplexity-user|googlebot|bingbot|applebot|meta-externalagent|bytespider|ccbot|amazonbot/i', $agent)) {
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
}
$context = stream_context_create(['http' => [
'method' => 'POST',
'timeout' => 2,
'header' => "Content-Type: application/json\r\nX-Statsdot-Bot-Key: sdb_your_key\r\n",
'content' => json_encode([
'user_agent' => $agent,
'url' => 'https://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'],
'status' => http_response_code(),
]),
]]);
@file_get_contents('https://statsdot.com/api/bots', false, $context);
}
Node.js and Express
const CRAWLERS = /gptbot|oai-searchbot|chatgpt-user|claudebot|claude-user|claude-searchbot|perplexitybot|perplexity-user|googlebot|bingbot|applebot|meta-externalagent|bytespider|ccbot|amazonbot/i;
app.use((req, res, next) => {
const agent = req.get('user-agent') || '';
if (CRAWLERS.test(agent)) {
res.on('finish', () => {
fetch('https://statsdot.com/api/bots', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Statsdot-Bot-Key': process.env.STATSDOT_BOT_KEY },
body: JSON.stringify({ user_agent: agent, url: `${req.protocol}://${req.get('host')}${req.originalUrl}`, status: res.statusCode }),
}).catch(() => {});
});
}
next();
});
Next.js
In middleware.ts, report without delaying the response:
import { NextResponse, type NextRequest, type NextFetchEvent } from 'next/server';
const CRAWLERS = /gptbot|oai-searchbot|chatgpt-user|claudebot|claude-user|claude-searchbot|perplexitybot|perplexity-user|googlebot|bingbot|applebot|meta-externalagent|bytespider|ccbot|amazonbot/i;
export function middleware(request: NextRequest, event: NextFetchEvent) {
const agent = request.headers.get('user-agent') ?? '';
if (CRAWLERS.test(agent)) {
event.waitUntil(fetch('https://statsdot.com/api/bots', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Statsdot-Bot-Key': process.env.STATSDOT_BOT_KEY! },
body: JSON.stringify({ user_agent: agent, url: request.url }),
}).catch(() => {}));
}
return NextResponse.next();
}
The endpoint
POST https://statsdot.com/api/bots with the header X-Statsdot-Bot-Key and a JSON body {"user_agent": "...", "url": "...", "status": 200}, or {"events": [...]} with up to 100 requests. It answers 202 with the number recorded. Up to 600 requests a minute per key.
Limits
Each account records up to 100,000 crawler requests a month across its sites; beyond that they are not recorded until the next month. Crawler requests do not count towards your plan's events. They are kept as long as your visits (24 months).