Skip to content

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).