Your access log shows GPTBot, ClaudeBot and a few names you don't recognise. The guides you find all say the same thing: list the User-Agents in robots.txt, then block the same strings in your server config.
That advice has a hole. A User-Agent is a string the client types, and typing "GPTBot" costs nothing. A blocklist of names stops honest bots, which would have obeyed robots.txt anyway, and lets through every scraper that borrows a name you allowed. The name is a claim and the IP is the proof, the same principle as bot detection by IP in general.
Below we sort the bots by job, show what robots.txt can and can't do, run four real crawler addresses through our API, and give you a three-tier policy with tested nginx and Express code. A spoofed GPTBot gets a 403. A real Googlebot never does.

Should you block AI crawlers? It depends on the bot's job
You should block AI crawlers whose job you object to and keep the ones that send you readers. AI bots do three different jobs, and blocking each one costs you something different.
- Training crawlers collect pages that may train future models. OpenAI says disallowing GPTBot "indicates a site's content should not be used in training generative AI foundation models." Blocking them costs nothing in search.
- AI search bots index pages so an assistant can cite them. OpenAI says sites opted out of OAI-SearchBot "will not be shown in ChatGPT search answers". Blocking them costs you citations and their clicks.
- User-triggered fetchers load a page because a person asked an assistant to read it. Blocking them breaks that person's request.
A paywalled publisher has a clear reason to refuse training. A brand that wants assistants to recommend it should keep the search bots. Most sites end up with a split: no to training, yes to search, a speed limit on fetchers. We cover which OpenAI bot to block and what each choice costs in a separate post. Agents that shop on a user's behalf are a different problem, covered in AI shopping agent fraud at checkout.
Which AI bots train and which cite you
This table sorts the AI bots you are most likely to see by job, with what each operator says about robots.txt. Every cell comes from the operator's own page, opened on 20 October 2026.
| Bot | Operator | Job | robots.txt, in the operator's words |
|---|---|---|---|
| GPTBot | OpenAI | Training | Disallow means content "should not be used in training" |
| OAI-SearchBot | OpenAI | AI search | Opted-out sites are not shown in ChatGPT search answers |
| ChatGPT-User | OpenAI | User-triggered fetch | robots.txt rules "may not apply" |
| ClaudeBot | Anthropic | Training | Honours robots.txt and Crawl-delay |
| Claude-SearchBot | Anthropic | AI search | Honours robots.txt and Crawl-delay |
| Claude-User | Anthropic | User-triggered fetch | Honours robots.txt and Crawl-delay |
| Google-Extended | Token for AI use, no crawler | A robots.txt token only, no User-Agent of its own | |
| Googlebot | Search, not an AI bot to block | Common crawlers "always obey robots.txt rules when crawling automatically" | |
| PerplexityBot | Perplexity | AI search | Not stated; Perplexity recommends allowing it |
| Perplexity-User | Perplexity | User-triggered fetch | "generally ignores robots.txt rules" |
| CCBot | Common Crawl | Open web archive that anyone can download | Checks robots.txt first; obeys Crawl-delay |
| Applebot-Extended | Apple | Token for AI training use, no crawler | Apple says it "does not crawl webpages" |
| Meta-ExternalAgent | Meta | Training and indexing | Not stated; Meta documents a Disallow rule for it |
| Bytespider | ByteDance | Not documented on a page we could open | Not documented on a page we could open |
Proof is a separate question from policy. A bot can only be verified if its operator publishes where it crawls from:
| Operator | Range file | Prefixes on 20 October 2026 | Reverse DNS documented | GeoIPHub verifies |
|---|---|---|---|---|
| OpenAI | gptbot.json, plus searchbot.json and chatgpt-user.json | 18, 39 and 234 | Not documented | Yes, all three |
| Anthropic | bots.json, one file for all three bots | 38 (file dated 7 October 2026) | Not documented | Yes, a match is named ClaudeBot |
| common-crawlers.json and four others | 317 in common crawlers | Yes: googlebot.com, google.com, googleusercontent.com | Yes | |
| Perplexity | perplexitybot.json and perplexity-user.json | 8 and 4 | Not documented | Yes |
| Common Crawl | ccbot.json | 5 | Yes, crawl.commoncrawl.org, IPv4 only | Yes |
| Apple | applebot.json | 24 | Yes, applebot.apple.com | Yes, for Applebot |
| Meta | Not published | Not published | Not documented | ASN match only, verified: false |
| ByteDance | We found none | None found | Not documented | No |
Before you copy a list from another site, check its names. Some guides that rank for this topic still name anthropic-ai and Claude-Web, which Anthropic's current page doesn't list, and miss Claude-SearchBot and Claude-User. A stale list blocks strings nobody sends.
What robots.txt can and can't do
robots.txt can tell well-behaved bots what you want, and it can't stop anything. RFC 9309, the standard that defines it, says: "These rules are not a form of access authorization." Whether ignoring it has legal consequences is a question for a lawyer.
It is still the first tier, because most named bots read it. This file refuses training, welcomes AI search and leaves Google Search alone:
# Training: no
User-agent: GPTBot
User-agent: ClaudeBot
User-agent: CCBot
User-agent: Google-Extended
User-agent: Applebot-Extended
User-agent: Meta-ExternalAgent
User-agent: Bytespider
Disallow: /
# AI search: yes
User-agent: OAI-SearchBot
User-agent: PerplexityBot
Allow: /
# Anthropic honours Crawl-delay (seconds between requests)
User-agent: Claude-SearchBot
Allow: /
Crawl-delay: 2
Four details decide whether that file does what you think:
- Google-Extended and Applebot-Extended are tokens, not crawlers. Google's common crawlers page, updated 15 October 2026, says "Google-Extended doesn't have a separate HTTP request user agent string" and that it "does not impact a site's inclusion in Google Search". No request ever arrives from Google-Extended, so no IP rule can block it. Apple's Applebot page says Applebot-Extended "does not crawl webpages" either.
- Blocking Googlebot to stop AI use costs you Search. So does a
User-agent: *group withDisallow: /. - User-triggered fetchers may ignore the file. OpenAI says of ChatGPT-User that "robots.txt rules may not apply". Perplexity says Perplexity-User "generally ignores robots.txt rules".
- Changes are not instant. OpenAI says "it can take ~24 hours from a site's robots.txt update for our systems to adjust".
Newer files such as llms.txt and ai.txt are proposals. Like robots.txt, they ask and don't enforce.
The opposite mistake is putting search crawlers on an "AI bot" blocklist. Contentstack's guide to blocking AI crawlers shows an edge function whose bot list includes googlebot and bingbot next to gptbot and claudebot, and answers all of them with "Forbidden: AI crawlers are not allowed." Copied as written, that rule takes a site out of Google and Bing search. Anyone pretending to be Googlebot is a fake Googlebot, a different problem with a different fix.
A real crawler or a spoof: how verification works
You tell a real crawler from a spoof by checking the connecting IP against what the named operator publishes. Which of three methods works depends on the operator.
- Range files. OpenAI, Anthropic, Perplexity, Common Crawl, Apple and Google publish JSON lists of their prefixes. For OpenAI and Anthropic this is the only method, because their addresses have no reverse DNS.
- Forward-confirmed reverse DNS. Google's verification guide has you resolve the IP to a name, check the domain, then resolve the name back and confirm it returns the same IP. Apple and Common Crawl document the same method.
- Signed requests (Web Bot Auth). A bot signs each request with a published key. It is still a draft, covered in the edge section below.
Our guide on how to verify an AI crawler by IP covers all three in detail. We looked up four crawler addresses at 04:37 UTC on 20 October 2026, without a key. This is the ClaudeBot address, trimmed, nesting kept and values unedited:
{
"ip": "216.73.216.10",
"asn": {
"asn": 16509,
"org": "AMAZON-02 - Amazon.com, Inc."
},
"threat": {
"is_crawler": true,
"crawler": {
"verified": true,
"name": "ClaudeBot",
"operator": "Anthropic",
"category": "ai",
"verification_method": "ip_range_list"
}
},
"scoring": {
"fraud_score": 0,
"recommended_action": "allow",
"detection_methods": ["datacenter_ip", "no_ptr_datacenter", "verified_crawler"]
}
}
The other three, from the same run:
| IP | crawler.name / operator / category | Verified, method | Score, action | detection_methods | ASN |
|---|---|---|---|---|---|
| 132.196.86.1 | GPTBot / OpenAI / ai | true, ip_range_list | 0, allow | datacenter_ip, no_ptr_datacenter, verified_crawler | 8075 MICROSOFT-CORP-MSN-AS-BLOCK |
| 66.249.66.1 | Googlebot / Google / search_engine | true, ip_range_list | 0, allow | datacenter_ip, verified_crawler | 15169 GOOGLE |
| 107.178.192.1 | GoogleUserTriggeredFetcher / Google / fetcher | true, ip_range_list | 0, allow | datacenter_ip, rdns_hosting, verified_crawler | 396982 GOOGLE-CLOUD-PLATFORM |
The GPTBot address is the first host of 132.196.86.0/24, the first prefix in OpenAI's gptbot.json. Then we asked DNS for names, at 04:45 UTC:
$ dig -x 216.73.216.10 +short # ClaudeBot: NXDOMAIN, no name
$ dig -x 132.196.86.1 +short # GPTBot: NXDOMAIN, no name
$ dig -x 66.249.66.1 +short
crawl-66-249-66-1.googlebot.com.
$ dig crawl-66-249-66-1.googlebot.com +short
66.249.66.1
That is the finding most guides miss. ClaudeBot and GPTBot have no PTR record, so reverse DNS cannot verify them. The range file is the only check. no_ptr_datacenter fires on both, yet the score stays at 0, because verified_crawler is an exonerating signal. For these operators a missing PTR is normal. Googlebot passes both ways: its name resolves back to the same address.
Two limits apply. The category field says "ai" for training and non-training AI bots alike, so policy code should key on crawler.name. And the lookup sees only an IP, never the User-Agent, so the API never flags a spoof. Comparing the claimed name with the proven name happens in your code. You can run the same check by hand in our IP lookup tool.
A three-tier policy: ask, verify, throttle
A policy that holds up has three tiers: ask with robots.txt, verify every bot claim against its IP, and rate-limit whatever is left. Block by job, enforce by IP, and never put Googlebot on an AI blocklist.
Tier 1: ask. robots.txt carries your per-bot decision, plus Crawl-delay for operators that honour it.
Tier 2: verify. When a User-Agent names a bot, look up the connecting IP and compare names. If the IP doesn't belong to that bot, the request gets a 403. If it does, apply your tier 1 decision, so a verified GPTBot that ignores your Disallow is still refused.
Tier 3: throttle. Everything else gets a rate limit: requests that claim no bot, bots that can't be verified, and fetchers acting for a person.
| Case | Example | Action |
|---|---|---|
| Verified bot you allow | Googlebot from 66.249.66.1 | Serve normally, no rate limit |
| Verified bot you refused, still arriving | GPTBot from 132.196.86.1 with GPTBot disallowed | 403 |
| Unverified claim: UA names a bot, IP doesn't match | GPTBot UA from 216.73.216.10, an Anthropic address | 403, or a challenge if you prefer |
| Bot that can't be verified | Bytespider, Meta-ExternalAgent | Rate-limit; robots.txt is your only lever |
| No bot claim | Browsers, and scrapers that pose as browsers | Rate-limit per IP |
The third row teaches the most. 216.73.216.10 is a verified crawler, just not the one the request names, so a check that only asks "is this a verified crawler?" would let it in. Compare names. Likewise, 107.178.192.1 is a verified Google fetcher, but a request from it claiming Googlebot is not Googlebot.
Log every tier 2 failure. An address outside the published ranges usually means a spoof, but it can be a prefix the operator just added. Our range files are re-fetched daily and the lookup database rebuilt daily, so a new prefix can take up to about two days to verify.
Enforce it in nginx and Express, with a spoofed-GPTBot test
Enforcement is one small Express service that does the lookup, plus an nginx config that asks it about every request claiming to be a bot. Stock nginx can't call an HTTP API inline, so the auth_request module sends a subrequest to the verifier and nginx caches the answer. Without nginx, the same file protects an Express app on its own.
The verifier matches the User-Agent against current bot tokens, looks up the IP with GET https://api.geoiphub.com/v1/lookup/{ip} and the x-api-key header, caches the result for an hour, and compares threat.crawler.name with the claimed name. It needs a name map because the API writes some names without the hyphen: OAISearchBot, ChatGPTUser, PerplexityUser. See the crawler fields in the API docs.
// bot-gate.js: tier 2 (verify the IP behind a bot claim) and tier 3 (rate-limit the rest).
// Tested with Node 24.20.0, Express 5.3.0 and express-rate-limit 8.7.1.
const express = require('express');
const { rateLimit } = require('express-rate-limit');
const { BlockList, isIP } = require('node:net');
const API_KEY = process.env.GEOIPHUB_KEY; // production: always set a key
const CACHE_MS = 60 * 60 * 1000; // one hour per IP
const cache = new Map(); // ip: { at, crawler }
// Your policy: User-Agent tokens you refuse even when verified. Everything verified and not listed passes.
const BLOCKED = new Set((process.env.BLOCKED_BOTS || 'GPTBot,ClaudeBot,CCBot').split(','));
// Maps each User-Agent token to the crawler.name our API returns for that operator's range file.
// Anthropic publishes one file for all three of its bots, so a match comes back as ClaudeBot.
const EXPECTED = {
'GPTBot': 'GPTBot',
'OAI-SearchBot': 'OAISearchBot',
'ChatGPT-User': 'ChatGPTUser',
'ClaudeBot': 'ClaudeBot',
'Claude-User': 'ClaudeBot',
'Claude-SearchBot': 'ClaudeBot',
'PerplexityBot': 'PerplexityBot',
'Perplexity-User': 'PerplexityUser',
'CCBot': 'CCBot',
'Googlebot': 'Googlebot',
'bingbot': 'Bingbot',
'Applebot': 'Applebot',
};
// Longest tokens first, so "Claude-SearchBot" is not read as a shorter token.
const TOKENS = Object.keys(EXPECTED).sort((a, b) => b.length - a.length);
// A real crawler never connects from a private or loopback address.
const privateNets = new BlockList();
for (const [net, bits] of [['10.0.0.0', 8], ['172.16.0.0', 12], ['192.168.0.0', 16],
['127.0.0.0', 8], ['100.64.0.0', 10]]) privateNets.addSubnet(net, bits, 'ipv4');
privateNets.addSubnet('fc00::', 7, 'ipv6');
privateNets.addAddress('::1', 'ipv6');
function claimedBot(ua = '') {
const lower = ua.toLowerCase();
return TOKENS.find((t) => lower.includes(t.toLowerCase())) || null;
}
async function lookupCrawler(ip) {
const hit = cache.get(ip);
if (hit && Date.now() - hit.at < CACHE_MS) return hit.crawler;
const res = await fetch(`https://api.geoiphub.com/v1/lookup/${ip}`, {
headers: API_KEY ? { 'x-api-key': API_KEY } : {}, // keyless only for a local test
signal: AbortSignal.timeout(1500),
});
if (!res.ok) throw new Error(`lookup ${res.status}`);
const data = await res.json();
cache.set(ip, { at: Date.now(), crawler: data.threat.crawler });
return data.threat.crawler;
}
async function verifyBot(req, res, next) {
const claim = claimedBot(req.get('user-agent'));
if (!claim) return next(); // no bot claim: tier 3 handles it
const ip = req.ip.replace(/^::ffff:/, '');
const family = isIP(ip) === 6 ? 'ipv6' : 'ipv4';
if (privateNets.check(ip, family)) {
return res.status(403).send(`unverified ${claim} claim\n`);
}
let crawler;
try {
crawler = await lookupCrawler(ip);
} catch (err) {
// Fail mode: never fail open to "verified". Mark it and let the rate limiter decide.
req.botUnverified = true;
return next();
}
// The API never flags a spoof: it sees only the IP, not the User-Agent.
// The spoof check is this comparison, in your code.
const verified = crawler.verified === true && crawler.name === EXPECTED[claim];
if (!verified) return res.status(403).send(`unverified ${claim} claim\n`);
if (BLOCKED.has(claim)) return res.status(403).send(`${claim} is blocked by policy\n`);
req.verifiedBot = crawler.name;
return next();
}
// Tier 3: everyone without a verified bot name, keyed per IPv4 address or per IPv6 /64.
// nginx auth_request understands only 2xx, 401 and 403, so the verifier endpoint answers 403.
const limiter = rateLimit({
windowMs: 60 * 1000,
limit: 120,
ipv6Subnet: 64,
skip: (req) => Boolean(req.verifiedBot),
handler: (req, res) => res.set('Cache-Control', 'no-store')
.status(req.path === '/_verify_bot' ? 403 : 429).send('rate limited\n'),
});
const app = express();
app.set('trust proxy', 'loopback'); // only nginx on this host may set X-Forwarded-For
// Endpoint for nginx auth_request: 204 lets the request through, 403 stops it.
// After a failed lookup, no-store keeps nginx from caching the 204, so the limiter sees every request.
app.get('/_verify_bot', verifyBot, limiter, (req, res) => {
if (req.botUnverified) res.set('Cache-Control', 'no-store');
res.sendStatus(204);
});
// Standalone use without nginx: protect every route.
app.use(verifyBot, limiter);
app.get('/', (req, res) => res.send('ok\n'));
app.listen(3000, '127.0.0.1');
Three choices are deliberate. If the lookup fails or takes over 1.5 seconds, the request is never treated as verified; it falls through to the rate limiter. Behind nginx, that answer is sent with no-store, so nginx doesn't cache it and the verifier counts every request against the same limit. The policy list holds User-Agent tokens, so refusing ClaudeBot doesn't refuse Claude-SearchBot, even though both come back from the lookup as ClaudeBot. trust proxy is loopback only, because otherwise a client could send its own X-Forwarded-For and choose the address you check. And only bot claims are looked up, so volume follows bot traffic, not page views. The free tier covers 1,500 lookups a day with a key, every field included.
The nginx side maps the User-Agent to a claimed bot, skips the subrequest when there is no claim, and caches the verifier's 204 or 403 per address and bot for an hour, except answers marked no-store:
# Goes in the http {} block. Tested with nginx 1.30.6 built with --with-http_auth_request_module.
# Which bot does the User-Agent claim? Empty means no claim.
map $http_user_agent $claimed_bot {
default "";
"~*(GPTBot|OAI-SearchBot|ChatGPT-User|ClaudeBot|Claude-User|Claude-SearchBot|PerplexityBot|Perplexity-User|CCBot|Googlebot|bingbot|Applebot)" $1;
}
# Tier 3 key: rate-limit requests with no bot claim. Claims go to tier 2 instead.
map $claimed_bot $limit_key {
"" $binary_remote_addr;
default "";
}
limit_req_zone $limit_key zone=perip:10m rate=10r/s;
# Cache the verifier's answer per address and claimed bot for one hour.
proxy_cache_path /var/cache/nginx/botcheck keys_zone=botcheck:1m max_size=10m inactive=1h;
server {
listen 8080;
location / {
limit_req zone=perip burst=20 nodelay;
limit_req_status 429;
auth_request /_verify_bot;
proxy_pass http://127.0.0.1:8000; # your app
}
location = /_verify_bot {
internal;
if ($claimed_bot = "") { return 204; } # no claim: no lookup
proxy_pass http://127.0.0.1:3000; # bot-gate.js
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Forwarded-For $remote_addr;
proxy_cache botcheck;
proxy_cache_key $remote_addr$claimed_bot;
proxy_cache_valid 204 403 1h;
}
}
nginx -t passed on this config, and we ran it in front of the Express file on 20 October 2026, at 05:05 UTC. First the spoofed GPTBot test, through nginx, from the same machine:
$ curl -s -o /dev/null -w "%{http_code}\n" \
-A "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; GPTBot/1.4; +https://openai.com/gptbot" \
http://localhost:8080/
403
$ curl -s -o /dev/null -w "%{http_code}\n" \
-A "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" \
http://localhost:8080/
200
That GPTBot string is the current example on OpenAI's bots page. From your own machine the request comes from 127.0.0.1, which no crawler uses, so the verifier refuses it before any lookup. To watch the lookup, call the Express file directly with the address nginx would forward:
$ GPT='Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; GPTBot/1.4; +https://openai.com/gptbot'
$ GB='Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)'
$ curl -s -A "$GPT" -H "X-Forwarded-For: 216.73.216.10" http://127.0.0.1:3000/
unverified GPTBot claim
$ curl -s -A "$GPT" -H "X-Forwarded-For: 132.196.86.1" http://127.0.0.1:3000/
GPTBot is blocked by policy
$ curl -s -A "$GB" -H "X-Forwarded-For: 107.178.192.1" http://127.0.0.1:3000/
unverified Googlebot claim
$ curl -s -A "$GB" -H "X-Forwarded-For: 66.249.66.1" http://127.0.0.1:3000/
ok
Follow the first line end to end. The User-Agent claims GPTBot. The verifier looks up 216.73.216.10, and the API answers with a verified crawler named ClaudeBot. ClaudeBot is not GPTBot, so the request gets a 403. The real GPTBot address, 132.196.86.1, passes verification and is refused only because this policy refuses training; with BLOCKED_BOTS=CCBot it got a 200. A Claude-SearchBot claim from 216.73.216.10 got a 200, because this policy refuses the ClaudeBot token, not Anthropic's search bot. A test-only copy of the nginx config that forwarded the same four addresses returned 403, 403, 403 and 200. With the API made unreachable on purpose, 130 bingbot claims from one address through nginx got 120 responses with status 200, then 403s: an outage throttles bot claims instead of waving them through.
If you test without a key, keep it short. Anonymous lookups are limited per minute, and the 30th call inside one 60-second window is already a 429 that locks your IP out for two hours.
Rate-limit what you can't verify instead of blocking it
Rate limiting is the right tool for traffic you can't prove either way: unverifiable bots, user-triggered fetchers and anything that claims no bot. A limit stops a scraper copying your site in an afternoon without locking out the people behind a fetcher.
Both snippets already do this. nginx's limit_req keys on the client address for requests with no bot claim, at 10 a second with a burst of 20. In our test, 40 back-to-back requests returned 25 successes and 15 responses with status 429. Tune the numbers, and exclude static assets if one page load fetches dozens. The Express limiter keys IPv6 clients on their /64, because one subscriber usually controls a whole /64; see rate limiting IPv6 by /64 prefix for when to widen to /56 or /48.
Crawl-delay is the polite version for bots that honour it. Anthropic's crawler help page says "we support the non-standard Crawl-delay extension to robots.txt", and Common Crawl's FAQ says "We obey the Crawl-delay parameter". Others don't. Google's robots.txt specification says fields "such as crawl-delay aren't supported", and Apple says "Applebot does not follow crawl-delay". OpenAI's and Perplexity's pages don't mention it.
Tier 2 only catches requests that name a bot. To block AI scrapers that never claim to be bots, add behaviour to tier 3: request rates, paths, and the network the request comes from. Our post on how to detect web scraping from your logs covers those signals.
Edge options: Cloudflare's AI blocking and pay per crawl
Behind Cloudflare, you can block AI crawlers with a setting instead of code, or charge them per request. Neither removes the need to know which bots you are dealing with.
Cloudflare announced on 1 July 2025, in a post last modified 15 July 2026, that it was "changing the default to block AI crawlers unless they pay creators for their content." If you run your own rules too, check what Cloudflare already blocks on your zone so the two layers agree.
With pay per crawl, a crawler requesting a priced page gets "a 402 Payment Required response, accompanied by a crawler-price header", and can retry with payment intent. Crawlers sign requests under the Web Bot Auth proposals so nobody can pay as someone else. Cloudflare says pay per crawl "is currently in private beta".
Web Bot Auth is not a standard yet. On 20 October 2026 the IETF webbotauth working group had one working-group document, draft-ietf-webbotauth-httpsig-protocol-00 from 1 September 2026, and no RFC; its charter's April 2026 milestone for a standards-track specification has passed. An individual draft, draft-illyes-webbotauth-jafar, proposes a common JSON format for crawler range files. Until those land, IP ranges and reverse DNS are what you can check.
Measure what the policy changed
Measure the policy by logging the verification result on every bot request, then comparing before and after.
- Share by verified bot. Log
crawler.nameandcrawler.verifiednext to each request that claimed a bot. The share of failed claims is your spoofing rate. The share by name shows which operators actually crawl you. - AI referrals. Count visits referred by chatgpt.com, perplexity.ai, claude.ai, copilot.microsoft.com and gemini.google.com. Treat that count as a floor, since some assistant traffic arrives with no referrer.
- Crawl volume. Compare requests per day from each bot before and after a change. Search Console's Crawl stats report covers Google's crawlers only, so the other bots have to come from your own logs.
If citations drop after you block an AI search bot, you have your answer on that bot.
When to revisit your AI crawler policy
Revisit your AI crawler policy whenever an operator adds a bot, changes a range file or retires a token, and at least once a quarter. OpenAI's bots page now lists OAI-AdsBot, with its own adsbot.json. Our crawler feeds don't include that file and the code above doesn't name it, so an OAI-AdsBot claim falls to tier 3 until you decide.
Be clear about what this setup can't do:
- It only proves bots that publish ranges. Bytespider and Meta-ExternalAgent can't be verified, and a spoof of either looks exactly like the real thing.
- New prefixes lag. A prefix added today can take up to about two days to verify, so a real bot can briefly fail tier 2.
- IP blocks can backfire. Anthropic warns that blocking its IPs "may not work correctly or persistently guarantee an opt-out, as doing so impedes our ability to read your robots.txt file." Keep the Disallow in robots.txt even when tier 2 enforces it.
- The lookup can't see the User-Agent. The spoof check is the name comparison in your code.
- Tier 2 misses scrapers that never claim a name. Those are tier 3's job.
Start with your own logs. Pick one address that claimed GPTBot or ClaudeBot this week, run it through a lookup, and compare crawler.name with the name it claimed.
Start Scoring Every IP in Real Time
GeoIPHub gives fraud, security, and engineering teams a single API for IP geolocation, VPN & proxy detection, threat intelligence, and an explainable 0 to 100 risk score.
Get Your Free API Key
Sign up in minutes, no credit card required. Upgrade only when you need more volume.
Frequently Asked Questions
Should I block AI crawlers?
It depends on the bot's job. Block training crawlers such as GPTBot and ClaudeBot if you object to your content training models. Allow AI search bots such as OAI-SearchBot if you want to be cited. Rate-limit user-triggered fetchers rather than relying on robots.txt.
Does robots.txt stop AI crawlers?
Only the ones that choose to obey it. RFC 9309 says its rules "are not a form of access authorization". A scraper using a GPTBot User-Agent ignores it, so enforcement has to check the IP where the request arrives.
Does blocking AI crawlers hurt SEO?
Blocking the Google-Extended token does not affect Google Search; Google says it "does not impact" a site's inclusion in Search. Blocking Googlebot to stop AI use removes you from Search. Blocking AI search bots such as OAI-SearchBot keeps you out of that assistant's search answers.
Can I block Google-Extended by IP?
No. Google-Extended is a robots.txt token with no User-Agent of its own. Google's existing crawlers do the fetching, so the only control is a robots.txt rule for the token. Applebot-Extended works the same way: Apple says it does not crawl webpages.
How do I know a request claiming to be GPTBot is real?
Check that the connecting IP is in OpenAI's published gptbot.json. Reverse DNS does not help: the GPTBot and ClaudeBot addresses we checked have no PTR record. A lookup returns threat.crawler.verified and the bot name, and your code compares that name to the User-Agent.
Can every AI crawler be verified by IP?
No. We found no range file from ByteDance for Bytespider, and Meta publishes none for Meta-ExternalAgent, so neither can be verified by IP. Rate-limit them instead of trusting the User-Agent.