ai-crawler-index 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e6a2d0be0c73f728584d587a1dbff0ed65acd7e8aa5174539896d521106a2d1e
4
+ data.tar.gz: d2d10ed8c72ab01f6c7183c5961b3bfe34868816e6c9fc00aa7cb1a53a1496e2
5
+ SHA512:
6
+ metadata.gz: 4f0fcfe0511e4a2e8e7b40fa0eda74ba1fdce21b31c4213a6f52c4bd6a4c49a739afaca6b91fcca9d3b71890529b6a2fcb4c36629792b946072a4c9b50dc8151
7
+ data.tar.gz: e4d86259e669717e36ba023ed958cd1a9edf751562dc948bb521cae70df0c15bfbce7a0df34f8e46e6d37402b826f2ae87adb3dd8ec1bbae5d979913f08c15f1
data/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pathwren
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ The crawler table shipped in data.json is dedicated to the public domain under
26
+ CC0-1.0. Robots tokens, user-agent strings and documentation URLs are taken
27
+ from each operator's own published documentation; the categories and the prose
28
+ are the index's own.
data/README.md ADDED
@@ -0,0 +1,135 @@
1
+ # ai-crawler-index
2
+
3
+ Offline classifier for AI-crawler and bot user-agents, in Ruby. Give it a
4
+ `User-Agent` string, get back what it is:
5
+
6
+ ```ruby
7
+ require "ai_crawler_index"
8
+
9
+ AiCrawlerIndex.ai_crawler?("Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; GPTBot/1.2; +https://openai.com/gptbot")
10
+ # => true
11
+
12
+ AiCrawlerIndex.identify("Mozilla/5.0 (compatible; ChatGPT-User/1.0; +https://openai.com/bot)")
13
+ # => {"slug"=>"chatgpt-user",
14
+ # "name"=>"ChatGPT-User",
15
+ # "operator"=>"OpenAI",
16
+ # "category"=>"user-fetch",
17
+ # "verification_method"=>"published-ranges", ...}
18
+ ```
19
+
20
+ **Zero dependencies, stdlib only, no network.** The table of 56 crawlers
21
+ is bundled as one 45.4 KB JSON file and every method is a pure function of
22
+ the string you pass in, so it runs inside a Rack request, a Sidekiq job, a
23
+ Lambda, or on a machine with no route to the internet at all. One HTTP call per
24
+ request to classify a user-agent is not an acceptable design; this exists so you
25
+ do not have to make one.
26
+
27
+ ## Install
28
+
29
+ ```sh
30
+ gem install ai-crawler-index
31
+ ```
32
+
33
+ or in a `Gemfile`:
34
+
35
+ ```ruby
36
+ gem "ai-crawler-index"
37
+ ```
38
+
39
+ Ruby >= 2.6. No dependencies.
40
+
41
+ ## API
42
+
43
+ | Method | Returns |
44
+ | --- | --- |
45
+ | `AiCrawlerIndex.ai_crawler?(ua)` | `true` for AI training, AI search, user-triggered fetch and dataset crawlers |
46
+ | `AiCrawlerIndex.identify(ua)` | the full record, or `nil` — `{name, operator, category, verification_method, ...}` |
47
+ | `AiCrawlerIndex.crawler?(ua)` | `true` for **any** known automated client, AI or not (search engines, SEO, archives, tools) |
48
+ | `AiCrawlerIndex.category_of(ua)` / `.operator_of(ua)` | `"ai-training"` / `"OpenAI"`, or `nil` |
49
+ | `AiCrawlerIndex.match_all(ua)` | every matching record, most specific first |
50
+ | `AiCrawlerIndex.list(category: nil)` | the whole table, or one category of it |
51
+ | `AiCrawlerIndex.get(slug)` | one record by slug, e.g. `get("gptbot")` |
52
+ | `AiCrawlerIndex.robots_txt(stance)` | a `robots.txt` body for `"block-ai-training"`, `"block-all-ai"` or `"block-none"` |
53
+ | `PATTERNS`, `CATEGORIES`, `META` | compiled alternations, category descriptions, and what this snapshot is |
54
+ | `AiCrawlerIndex.refresh` | **optional, the only network path** — fetches the current table and returns a new object |
55
+
56
+ In Rack, without middleware and without a request to anywhere:
57
+
58
+ ```ruby
59
+ ua = env["HTTP_USER_AGENT"]
60
+ if AiCrawlerIndex.category_of(ua) == "ai-training"
61
+ return [403, { "content-type" => "text/plain" }, ["no"]]
62
+ end
63
+ ```
64
+
65
+ There is a command line too, installed as `ai-crawler-index`:
66
+
67
+ ```sh
68
+ ai-crawler-index "Mozilla/5.0 (compatible; GPTBot/1.2)" # JSON verdict, exit 0 if known
69
+ ai-crawler-index --robots block-ai-training # a robots.txt on stdout
70
+ ai-crawler-index --list ai-search # one category of the table
71
+ ```
72
+
73
+ Matching is case-insensitive substring, most specific token first, so
74
+ `Googlebot-Image` beats `Googlebot` and `Claude-SearchBot` beats `ClaudeBot`.
75
+ Unknown, empty and non-string input returns `nil` / `false` and never raises.
76
+
77
+ ## A user-agent is a claim, not evidence
78
+
79
+ This gem tells you what a client *says* it is. Whether the claim is true is a
80
+ question about its IP address, and this gem deliberately does not pretend to
81
+ answer it offline: IP ranges rotate, and a stale range list baked into a package
82
+ is worse than no check at all. For operators that publish ranges, each record
83
+ carries an `ip_ranges` URL — check the address before you act on the name.
84
+
85
+ ### The categories
86
+
87
+ | Category | What it means | What blocking it costs you |
88
+ | --- | --- | --- |
89
+ | `ai-training` | bulk collection for training a model | your pages are excluded from future training sets; nothing a user sees today changes |
90
+ | `ai-search` | builds the index an assistant answers and cites from | this is the class that sends you traffic; blocking it is the expensive mistake |
91
+ | `user-fetch` | one page, right now, because a person asked for it | a visible error for a real reader |
92
+ | `dataset` | crawls into a published or resold corpus | highest leverage per block, longest delay before any effect |
93
+ | `search`, `seo`, `archive`, `tool`, `preview` | classic crawlers | ordinary search and tooling consequences |
94
+
95
+ `verification_method` tells you how far a claim can be trusted:
96
+ `published-ranges` (the operator publishes the IP ranges it crawls from),
97
+ `reverse-dns`, or `none`.
98
+
99
+ ## The data
100
+
101
+ Generated 2026-09-01T21:09:27+00:00 from the [AI Crawler Index](https://www.pathwren.workers.dev/c/rubygems-registry/) —
102
+ 56 crawlers from 30 operators, each reviewed against its
103
+ operator's own published documentation. Robots tokens, user-agent strings and
104
+ documentation URLs come from those operator pages (cited per record); the
105
+ categories and the prose are the index's own.
106
+
107
+ - Source of truth: `https://www.pathwren.workers.dev/c/rubygems-registry/data/agents.json` — regenerated every six hours.
108
+ - This bundle is a **snapshot of that file taken at 2026-09-01T21:09:27+00:00**, not a
109
+ live feed. Crawlers appear and change names; a gem published last month cannot
110
+ know about a bot announced last week.
111
+ - Data licence: **CC0-1.0**. Code licence: MIT.
112
+ - Version scheme: the patch number moves when the table changes, the minor
113
+ number when a crawler is added or removed, the major number only for an API
114
+ change.
115
+
116
+ If a bot is missing, wrong or misfiled, corrections are welcome and get applied
117
+ to the index — it is a public reference and it is meant to be argued with.
118
+
119
+ ### Staying current without upgrading
120
+
121
+ ```ruby
122
+ live = AiCrawlerIndex.refresh # one HTTPS GET, explicit, never automatic
123
+ live.identify(ua)
124
+ ```
125
+
126
+ `refresh` is the only method that touches the network, it is never called for
127
+ you, and it returns a *new* object rather than mutating the bundled table.
128
+ Everything else works with the network unplugged.
129
+
130
+ ## What this is
131
+
132
+ [Pathwren](https://www.pathwren.workers.dev/c/rubygems-registry/) is an independent, non-commercial project. It is run by
133
+ automation and says so wherever it introduces itself; it is not affiliated with
134
+ any of the operators listed, and it sells nothing. The index behind this gem is
135
+ static files, CC0, no signup: JSON, CSV, robots.txt and regex at https://www.pathwren.workers.dev/c/rubygems-registry/.
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # ai-crawler-index — command line.
5
+ #
6
+ # ai-crawler-index "Mozilla/5.0 (compatible; GPTBot/1.2)" # JSON verdict, exit 0 if known
7
+ # ai-crawler-index --robots block-ai-training # a robots.txt on stdout
8
+ # ai-crawler-index --list ai-search # the table, or one category of it
9
+ # ai-crawler-index --version
10
+
11
+ require "json"
12
+ require "ai_crawler_index"
13
+
14
+ def usage
15
+ warn <<~TEXT
16
+ ai-crawler-index #{AiCrawlerIndex::VERSION} — offline AI-crawler user-agent classifier
17
+
18
+ ai-crawler-index "<user-agent>" classify one user-agent (JSON; exit 1 if unknown)
19
+ ai-crawler-index --robots <stance> robots.txt for block-ai-training | block-all-ai | block-none
20
+ ai-crawler-index --list [category] the whole table, or one category, as JSON
21
+ ai-crawler-index --meta what this snapshot is
22
+ ai-crawler-index --version
23
+
24
+ #{AiCrawlerIndex::META['source']}
25
+ TEXT
26
+ end
27
+
28
+ arg = ARGV[0]
29
+
30
+ case arg
31
+ when nil, "-h", "--help"
32
+ usage
33
+ exit 2
34
+ when "--version", "-V"
35
+ puts AiCrawlerIndex::VERSION
36
+ when "--meta"
37
+ puts JSON.pretty_generate(AiCrawlerIndex::META)
38
+ when "--robots"
39
+ begin
40
+ puts AiCrawlerIndex.robots_txt(ARGV[1] || "block-ai-training")
41
+ rescue ArgumentError => e
42
+ warn e.message
43
+ exit 2
44
+ end
45
+ when "--list"
46
+ rows = AiCrawlerIndex.list(category: ARGV[1])
47
+ if rows.empty?
48
+ warn "no crawlers in category #{ARGV[1].inspect}; known: #{AiCrawlerIndex::CATEGORIES.keys.join(', ')}"
49
+ exit 1
50
+ end
51
+ puts JSON.pretty_generate(rows)
52
+ else
53
+ hit = AiCrawlerIndex.identify(arg)
54
+ if hit.nil?
55
+ puts JSON.generate({ "matched" => false, "ai_crawler" => false, "ua" => arg })
56
+ exit 1
57
+ end
58
+ puts JSON.pretty_generate(hit.merge("ai_crawler" => AiCrawlerIndex.ai_crawler?(arg)))
59
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The gem is named ai-crawler-index; some people will type the gem name and some
4
+ # the module path. Both work, and they are the same object.
5
+ require_relative "ai_crawler_index"
@@ -0,0 +1 @@
1
+ {"version":"1.1.0","generated_at":"2026-09-01T21:09:27+00:00","source":"AI Crawler Index — https://www.pathwren.workers.dev — independent, non-commercial; data CC0-1.0","data_url":"https://www.pathwren.workers.dev/c/rubygems-registry/data/agents.json","window":"Table reviewed 2026-09-01; every record checked against its operator's own published documentation. IP-range mirrors behind the index refresh every six hours; this bundle is a snapshot, not a live feed.","license":{"code":"MIT","data":"CC0-1.0"},"categories":{"ai-training":{"label":"AI training crawlers","description":"Collect pages in bulk so that a model can be trained or fine-tuned on them. Blocking these removes you from future training sets and changes nothing a user sees today."},"ai-search":{"label":"AI search crawlers","description":"Build the retrieval index an assistant answers and cites from. These are the crawlers that send you traffic; blocking them is the expensive mistake in this space."},"user-fetch":{"label":"User-triggered fetchers","description":"Fetch one page because a person asked for it, right then. One human intent, one request. Blocking them produces a visible error for a real reader."},"dataset":{"label":"Corpus and dataset builders","description":"Crawl the web into a published or resold dataset that other people train on. Highest leverage per block, longest delay before any effect."},"search":{"label":"Search engines","description":"Classic index-and-rank crawlers. Several also feed their operator's generative answers, which is why the AI opt-out for Google and Apple is a token rather than a block."},"seo":{"label":"SEO and backlink crawlers","description":"Commercial link-graph tooling. No user-facing effect either way, and usually a large share of your bot bandwidth."},"archive":{"label":"Archivers","description":"Preservation crawlers. Their output is public and permanent, which makes them a separate decision from the AI one."},"preview":{"label":"Link preview fetchers","description":"Read your Open Graph tags when someone shares a link. Blocking these is almost always an accident."},"tool":{"label":"Tools and frameworks","description":"Not operators: crawling software anyone can run. The party behind the request is unknown, so treat them as a rate-limit question rather than a consent question."}},"ai_categories":["ai-training","ai-search","user-fetch","dataset"],"regex":{"all":"(AhrefsBot|AI2Bot|Ai2Bot\\-Dolma|Amazonbot|anthropic\\-ai|Applebot|archive\\.org_bot|Baiduspider|bingbot|Bytespider|CCBot|ChatGPT\\-User|Claude\\-SearchBot|Claude\\-User|Claude\\-Web|ClaudeBot|cohere\\-ai|cohere\\-training\\-data\\-crawler|Diffbot|DuckAssistBot|DuckDuckBot|FacebookBot|facebookexternalhit|FirecrawlAgent|Google\\-CloudVertexBot|Google\\-InspectionTool|Googlebot|Googlebot\\-Image|Googlebot\\-News|GoogleOther|GPTBot|ia_archiver|ImagesiftBot|img2dataset|meta\\-externalagent|meta\\-externalfetcher|MistralAI\\-User|OAI\\-SearchBot|omgili|omgilibot|Perplexity\\-User|PerplexityBot|PetalBot|Scrapy|SemrushBot|SemrushBot\\-OCOB|SeznamBot|Storebot\\-Google|TikTokSpider|Timpibot|Webzio\\-Extended|YandexBot|Yeti|YouBot)","ai_only":"(AI2Bot|Ai2Bot\\-Dolma|Amazonbot|anthropic\\-ai|Bytespider|CCBot|ChatGPT\\-User|Claude\\-SearchBot|Claude\\-User|Claude\\-Web|ClaudeBot|cohere\\-ai|cohere\\-training\\-data\\-crawler|Diffbot|DuckAssistBot|FacebookBot|Google\\-CloudVertexBot|GoogleOther|GPTBot|ImagesiftBot|img2dataset|meta\\-externalagent|meta\\-externalfetcher|MistralAI\\-User|OAI\\-SearchBot|omgili|omgilibot|Perplexity\\-User|PerplexityBot|SemrushBot\\-OCOB|TikTokSpider|Webzio\\-Extended|YouBot)","by_category":{"ai-training":"(anthropic\\-ai|Bytespider|ClaudeBot|cohere\\-training\\-data\\-crawler|FacebookBot|GoogleOther|GPTBot|meta\\-externalagent|SemrushBot\\-OCOB|TikTokSpider|Webzio\\-Extended)","ai-search":"(Amazonbot|Claude\\-SearchBot|Claude\\-Web|DuckAssistBot|Google\\-CloudVertexBot|OAI\\-SearchBot|PerplexityBot|YouBot)","user-fetch":"(ChatGPT\\-User|Claude\\-User|cohere\\-ai|meta\\-externalfetcher|MistralAI\\-User|Perplexity\\-User)","search":"(Applebot|Baiduspider|bingbot|DuckDuckBot|Googlebot|Googlebot\\-Image|Googlebot\\-News|PetalBot|SeznamBot|Storebot\\-Google|Timpibot|YandexBot|Yeti)","tool":"(FirecrawlAgent|Google\\-InspectionTool|Scrapy)","dataset":"(AI2Bot|Ai2Bot\\-Dolma|CCBot|Diffbot|ImagesiftBot|img2dataset|omgili|omgilibot)","preview":"(facebookexternalhit)","seo":"(AhrefsBot|SemrushBot)","archive":"(archive\\.org_bot|ia_archiver)"}},"crawlers":[{"slug":"ahrefsbot","name":"AhrefsBot","operator":"Ahrefs","category":"seo","category_label":"SEO and backlink crawlers","ua":"AhrefsBot","robots_token":"AhrefsBot","verification_method":"reverse-dns","verification_label":"reverse DNS","respects_robots_txt":"documented","docs":"https://ahrefs.com/robot","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/ahrefsbot.html","what_it_is":"Ahrefs' backlink crawler, and one of the largest non-search crawlers on the web by request volume.","cost_of_blocking":"No user-facing effect. Ahrefs honours Crawl-delay, so rate-limiting is usually better than blocking."},{"slug":"ai2bot","name":"AI2Bot","operator":"Allen Institute for AI","category":"dataset","category_label":"Corpus and dataset builders","ua":"AI2Bot","robots_token":"AI2Bot","verification_method":"none","verification_label":"no published verification method","respects_robots_txt":"documented","docs":"https://allenai.org/crawler","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/ai2bot.html","what_it_is":"The Allen Institute's crawler, gathering pages for open research corpora such as Dolma that underpin fully open models like OLMo.","cost_of_blocking":"Excluded from open research datasets. Worth a deliberate decision: this is the category where 'blocking AI' also blocks the open, auditable end of it."},{"slug":"ai2bot-dolma","name":"Ai2Bot-Dolma","operator":"Allen Institute for AI","category":"dataset","category_label":"Corpus and dataset builders","ua":"Ai2Bot-Dolma","robots_token":"Ai2Bot-Dolma","verification_method":"none","verification_label":"no published verification method","respects_robots_txt":"documented","docs":"https://allenai.org/crawler","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/ai2bot-dolma.html","what_it_is":"The variant of AI2's crawler named for the Dolma corpus specifically.","cost_of_blocking":"Same as AI2Bot: exclusion from an open, published training corpus."},{"slug":"amazonbot","name":"Amazonbot","operator":"Amazon","category":"ai-search","category_label":"AI search crawlers","ua":"Amazonbot","robots_token":"Amazonbot","verification_method":"reverse-dns","verification_label":"reverse DNS","respects_robots_txt":"documented","docs":"https://developer.amazon.com/amazonbot","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/amazonbot.html","what_it_is":"Amazon's crawler, feeding Alexa's ability to answer questions from the web and Amazon's own search and assistant products.","cost_of_blocking":"Alexa and Amazon's assistants stop answering from your pages. Verify with reverse DNS to crawl.amazonbot.amazon before trusting the user-agent."},{"slug":"anthropic-ai","name":"anthropic-ai","operator":"Anthropic","category":"ai-training","category_label":"AI training crawlers","ua":"anthropic-ai","robots_token":"anthropic-ai","verification_method":"none","verification_label":"no published verification method","respects_robots_txt":"n-a","docs":"https://support.anthropic.com/en/articles/8896518","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/anthropic-ai.html","what_it_is":"A legacy robots.txt token from before Anthropic consolidated on ClaudeBot. It is still widely present in robots.txt files and costs nothing to keep, but it is a control token rather than a bot you will see in logs.","cost_of_blocking":"None. Nothing crawls under this name today; keeping the rule is harmless insurance."},{"slug":"applebot","name":"Applebot","operator":"Apple","category":"search","category_label":"Search engines","ua":"Applebot","robots_token":"Applebot","verification_method":"published-ranges","verification_label":"published IP ranges","respects_robots_txt":"documented","docs":"https://support.apple.com/en-us/119829","ip_ranges":"https://www.pathwren.workers.dev/c/rubygems-registry/ip-ranges/apple-applebot.json","url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/applebot.html","what_it_is":"Powers Siri, Spotlight and Safari suggestions. Blocking it is a search decision, not an AI decision — the AI decision has its own token.","cost_of_blocking":"You disappear from Siri, Spotlight and Safari search suggestions across Apple's install base."},{"slug":"applebot-extended","name":"Applebot-Extended","operator":"Apple","category":"ai-training","category_label":"AI training crawlers","ua":"(control token only — no crawler)","robots_token":"Applebot-Extended","verification_method":"none","verification_label":"no published verification method","respects_robots_txt":"n-a","docs":"https://support.apple.com/en-us/119829","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/applebot-extended.html","what_it_is":"Apple's counterpart to Google-Extended: a robots.txt token that withdraws consent for Apple Intelligence and Apple foundation-model training, without touching Applebot's search crawl.","cost_of_blocking":"Excluded from Apple Intelligence training. Siri, Spotlight and Safari suggestions are unaffected."},{"slug":"archive-org-bot","name":"archive.org_bot","operator":"Internet Archive","category":"archive","category_label":"Archivers","ua":"archive.org_bot","robots_token":"archive.org_bot","verification_method":"none","verification_label":"no published verification method","respects_robots_txt":"documented","docs":"https://archive.org/details/archive.org_bot","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/archive-org-bot.html","what_it_is":"The Wayback Machine's crawler. Preservation rather than AI, but it lands in the same 'is this bot welcome' decision and its output is a public corpus.","cost_of_blocking":"Your site stops being preserved. When it dies, it is gone. Consider this one separately from the AI question."},{"slug":"baiduspider","name":"Baiduspider","operator":"Baidu","category":"search","category_label":"Search engines","ua":"Baiduspider","robots_token":"Baiduspider","verification_method":"reverse-dns","verification_label":"reverse DNS","respects_robots_txt":"documented","docs":"https://help.baidu.com/question?prod_id=99&class=0&id=3001","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/baiduspider.html","what_it_is":"Baidu's search crawler, and the ingest path for Baidu's Ernie-backed answers.","cost_of_blocking":"Removal from Baidu Search, which matters only if you want Chinese-language traffic."},{"slug":"bingbot","name":"bingbot","operator":"Microsoft","category":"search","category_label":"Search engines","ua":"bingbot","robots_token":"bingbot","verification_method":"published-ranges","verification_label":"published IP ranges","respects_robots_txt":"documented","docs":"https://www.bing.com/webmasters/help/which-crawlers-does-bing-use-8c184ec0","ip_ranges":"https://www.pathwren.workers.dev/c/rubygems-registry/ip-ranges/bing-bingbot.json","url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/bingbot.html","what_it_is":"Bing's only crawler, and therefore also the crawler behind Microsoft Copilot's grounding. Microsoft's documented way to keep search indexing while refusing generative reuse is the nocache / noarchive robots meta directive, not a separate user-agent.","cost_of_blocking":"Very high and very wide: Bing, Copilot, DuckDuckGo and several assistants that resell Bing's index all lose you at once. Use nocache/noarchive rather than blocking."},{"slug":"bytespider","name":"Bytespider","operator":"ByteDance","category":"ai-training","category_label":"AI training crawlers","ua":"Bytespider","robots_token":"Bytespider","verification_method":"none","verification_label":"no published verification method","respects_robots_txt":"disputed","docs":"https://www.bytespider.net/","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/bytespider.html","what_it_is":"ByteDance's crawler, associated with training data collection for Doubao and related models. Repeatedly reported by CDNs and site operators as the highest-volume AI crawler on the web and as inconsistent about robots.txt.","cost_of_blocking":"Little to lose. If you want it gone, expect to block by user-agent at the edge rather than to ask politely in robots.txt."},{"slug":"ccbot","name":"CCBot","operator":"Common Crawl","category":"dataset","category_label":"Corpus and dataset builders","ua":"CCBot","robots_token":"CCBot","verification_method":"none","verification_label":"no published verification method","respects_robots_txt":"documented","docs":"https://commoncrawl.org/faq","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/ccbot.html","what_it_is":"Common Crawl's corpus builder. It trains nothing itself, but its archive is an input to most open and many closed LLM training sets, which makes it the highest-leverage single entry on this list.","cost_of_blocking":"Future Common Crawl snapshots exclude you, so downstream training sets lose you too — but only going forward. Existing snapshots are permanent and blocking today does not retract them."},{"slug":"chatgpt-user","name":"ChatGPT-User","operator":"OpenAI","category":"user-fetch","category_label":"User-triggered fetchers","ua":"ChatGPT-User","robots_token":"ChatGPT-User","verification_method":"published-ranges","verification_label":"published IP ranges","respects_robots_txt":"documented","docs":"https://platform.openai.com/docs/bots","ip_ranges":"https://www.pathwren.workers.dev/c/rubygems-registry/ip-ranges/openai-chatgpt-user.json","url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/chatgpt-user.html","what_it_is":"Fetches a single page at the moment a user or a ChatGPT agent asks for it — a pasted link, a browsing step, an Operator task. One human intent, one request. OpenAI states these fetches are not used for training.","cost_of_blocking":"ChatGPT cannot open your pages when a user explicitly asks it to. The user sees a fetch failure. This is usually the last bot anyone means to block."},{"slug":"claude-searchbot","name":"Claude-SearchBot","operator":"Anthropic","category":"ai-search","category_label":"AI search crawlers","ua":"Claude-SearchBot","robots_token":"Claude-SearchBot","verification_method":"none","verification_label":"no published verification method","respects_robots_txt":"documented","docs":"https://support.anthropic.com/en/articles/8896518","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/claude-searchbot.html","what_it_is":"Indexes pages so Claude's web search can find and cite them. Separate token from the training crawler, so search visibility and training consent are independent decisions.","cost_of_blocking":"You stop appearing in Claude's search results and citations."},{"slug":"claude-user","name":"Claude-User","operator":"Anthropic","category":"user-fetch","category_label":"User-triggered fetchers","ua":"Claude-User","robots_token":"Claude-User","verification_method":"none","verification_label":"no published verification method","respects_robots_txt":"documented","docs":"https://support.anthropic.com/en/articles/8896518","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/claude-user.html","what_it_is":"Fetches a page because a Claude user asked Claude to read it, at that moment.","cost_of_blocking":"Claude reports a fetch failure to a user who asked for your page by name."},{"slug":"claude-web","name":"Claude-Web","operator":"Anthropic","category":"ai-search","category_label":"AI search crawlers","ua":"Claude-Web","robots_token":"Claude-Web","verification_method":"none","verification_label":"no published verification method","respects_robots_txt":"n-a","docs":"https://support.anthropic.com/en/articles/8896518","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/claude-web.html","what_it_is":"An earlier Anthropic token for user-facing web access, superseded by Claude-User and Claude-SearchBot. Kept here because it appears in most published robots.txt templates.","cost_of_blocking":"None in practice. Retain the rule; expect no traffic."},{"slug":"claudebot","name":"ClaudeBot","operator":"Anthropic","category":"ai-training","category_label":"AI training crawlers","ua":"ClaudeBot","robots_token":"ClaudeBot","verification_method":"none","verification_label":"no published verification method","respects_robots_txt":"documented","docs":"https://support.anthropic.com/en/articles/8896518","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/claudebot.html","what_it_is":"Anthropic's bulk crawler, gathering pages that may be used to train Claude models.","cost_of_blocking":"Content excluded from training data for future Claude models. No effect on Claude's ability to fetch a link a user gives it."},{"slug":"cohere-ai","name":"cohere-ai","operator":"Cohere","category":"user-fetch","category_label":"User-triggered fetchers","ua":"cohere-ai","robots_token":"cohere-ai","verification_method":"none","verification_label":"no published verification method","respects_robots_txt":"documented","docs":"https://cohere.com/","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/cohere-ai.html","what_it_is":"Cohere's fetcher, used when its assistant products need a page.","cost_of_blocking":"Cohere-powered assistants cannot read your pages on request."},{"slug":"cohere-training-data-crawler","name":"cohere-training-data-crawler","operator":"Cohere","category":"ai-training","category_label":"AI training crawlers","ua":"cohere-training-data-crawler","robots_token":"cohere-training-data-crawler","verification_method":"none","verification_label":"no published verification method","respects_robots_txt":"documented","docs":"https://cohere.com/","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/cohere-training-data-crawler.html","what_it_is":"Cohere's separately-named bulk crawler for model training data, split out so consent for training and consent for retrieval can differ.","cost_of_blocking":"Excluded from Cohere model training."},{"slug":"diffbot","name":"Diffbot","operator":"Diffbot","category":"dataset","category_label":"Corpus and dataset builders","ua":"Diffbot","robots_token":"Diffbot","verification_method":"none","verification_label":"no published verification method","respects_robots_txt":"documented","docs":"https://docs.diffbot.com/","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/diffbot.html","what_it_is":"Extracts structured records from pages to build a commercial knowledge graph that is resold and used for retrieval and training.","cost_of_blocking":"Your facts stop entering a widely-licensed knowledge graph. Whether that is a loss depends on whether you want to be a machine-readable entity."},{"slug":"duckassistbot","name":"DuckAssistBot","operator":"DuckDuckGo","category":"ai-search","category_label":"AI search crawlers","ua":"DuckAssistBot","robots_token":"DuckAssistBot","verification_method":"none","verification_label":"no published verification method","respects_robots_txt":"documented","docs":"https://duckduckgo.com/duckduckgo-help-pages/results/duckassistbot/","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/duckassistbot.html","what_it_is":"Fetches pages so DuckAssist can generate and cite answers inside DuckDuckGo.","cost_of_blocking":"No DuckAssist answers or citations from your site. Ordinary DuckDuckGo results are unaffected."},{"slug":"duckduckbot","name":"DuckDuckBot","operator":"DuckDuckGo","category":"search","category_label":"Search engines","ua":"DuckDuckBot","robots_token":"DuckDuckBot","verification_method":"published-ranges","verification_label":"published IP ranges","respects_robots_txt":"documented","docs":"https://duckduckgo.com/duckduckgo-help-pages/results/duckduckbot/","ip_ranges":"https://www.pathwren.workers.dev/c/rubygems-registry/ip-ranges/duckduckgo-duckduckbot.json","url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/duckduckbot.html","what_it_is":"DuckDuckGo's own crawler. Note that the bulk of DuckDuckGo's web results come from Bing, so blocking bingbot removes you from DuckDuckGo whether or not you allow this one.","cost_of_blocking":"Limited on its own; the real DuckDuckGo lever is bingbot."},{"slug":"facebookbot","name":"FacebookBot","operator":"Meta","category":"ai-training","category_label":"AI training crawlers","ua":"FacebookBot","robots_token":"FacebookBot","verification_method":"none","verification_label":"no published verification method","respects_robots_txt":"documented","docs":"https://developers.facebook.com/docs/sharing/webmasters/web-crawlers","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/facebookbot.html","what_it_is":"Meta's older speech- and language-corpus crawler, largely superseded by meta-externalagent but still listed as a valid robots token.","cost_of_blocking":"Negligible today. Keep the rule; expect little traffic."},{"slug":"facebookexternalhit","name":"facebookexternalhit","operator":"Meta","category":"preview","category_label":"Link preview fetchers","ua":"facebookexternalhit","robots_token":"facebookexternalhit","verification_method":"none","verification_label":"no published verification method","respects_robots_txt":"documented","docs":"https://developers.facebook.com/docs/sharing/webmasters/web-crawlers","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/facebookexternalhit.html","what_it_is":"The link unfurler: it reads your Open Graph tags when somebody shares your URL on a Meta property.","cost_of_blocking":"Severe and usually accidental. Your links share as bare grey boxes with no title, image or description across Facebook, Instagram, Messenger and WhatsApp. Almost nobody means to block this."},{"slug":"firecrawlagent","name":"FirecrawlAgent","operator":"Firecrawl","category":"tool","category_label":"Tools and frameworks","ua":"FirecrawlAgent","robots_token":"FirecrawlAgent","verification_method":"none","verification_label":"no published verification method","respects_robots_txt":"documented","docs":"https://docs.firecrawl.dev/","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/firecrawlagent.html","what_it_is":"A hosted scrape-to-markdown service that LLM applications call to read pages. The requester is whoever is building on it, not Firecrawl itself, so volume and intent vary wildly.","cost_of_blocking":"Applications built on Firecrawl cannot read your pages. This is increasingly how agents fetch the web, so it is a bigger block than its name suggests."},{"slug":"google-cloudvertexbot","name":"Google-CloudVertexBot","operator":"Google","category":"ai-search","category_label":"AI search crawlers","ua":"Google-CloudVertexBot","robots_token":"Google-CloudVertexBot","verification_method":"published-ranges","verification_label":"published IP ranges","respects_robots_txt":"documented","docs":"https://developers.google.com/search/docs/crawling-indexing/overview-google-crawlers","ip_ranges":"https://www.pathwren.workers.dev/c/rubygems-registry/ip-ranges/google-special.json","url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/google-cloudvertexbot.html","what_it_is":"Crawls a site on behalf of a Vertex AI Agent Builder customer who is building an agent over that site. It only visits sites the customer has asked it to.","cost_of_blocking":"Third parties can no longer build Vertex AI agents that read your site. Irrelevant to Google Search."},{"slug":"google-extended","name":"Google-Extended","operator":"Google","category":"ai-training","category_label":"AI training crawlers","ua":"(control token only — no crawler)","robots_token":"Google-Extended","verification_method":"none","verification_label":"no published verification method","respects_robots_txt":"n-a","docs":"https://developers.google.com/search/docs/crawling-indexing/overview-google-crawlers","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/google-extended.html","what_it_is":"Not a crawler. A robots.txt token that tells Google whether pages Googlebot already fetched may be used to train and ground Gemini. You will never see it in an access log; disallowing it changes what Google does with content it fetched under a different name.","cost_of_blocking":"You are excluded from Gemini grounding and Gemini training. Google Search ranking and indexing are explicitly unaffected. This is the cleanest 'no training, keep my search traffic' lever that exists."},{"slug":"google-inspectiontool","name":"Google-InspectionTool","operator":"Google","category":"tool","category_label":"Tools and frameworks","ua":"Google-InspectionTool","robots_token":"Google-InspectionTool","verification_method":"published-ranges","verification_label":"published IP ranges","respects_robots_txt":"documented","docs":"https://developers.google.com/search/docs/crawling-indexing/overview-google-crawlers","ip_ranges":"https://www.pathwren.workers.dev/c/rubygems-registry/ip-ranges/google-special.json","url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/google-inspectiontool.html","what_it_is":"The fetcher behind Search Console's URL Inspection and the Rich Results Test. It runs when a site owner clicks a button.","cost_of_blocking":"Your own Search Console live tests stop working. Blocking this only hurts you."},{"slug":"googlebot","name":"Googlebot","operator":"Google","category":"search","category_label":"Search engines","ua":"Googlebot","robots_token":"Googlebot","verification_method":"published-ranges","verification_label":"published IP ranges","respects_robots_txt":"documented","docs":"https://developers.google.com/search/docs/crawling-indexing/overview-google-crawlers","ip_ranges":"https://www.pathwren.workers.dev/c/rubygems-registry/ip-ranges/google-googlebot.json","url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/googlebot.html","what_it_is":"The classic search crawler. It is also the crawler behind AI Overviews: Google does not run a separate bot for them, which is why the only AI opt-out is the Google-Extended token and not a Googlebot block.","cost_of_blocking":"Total. You leave Google Search. Never block this to avoid AI use; use Google-Extended instead."},{"slug":"googlebot-image","name":"Googlebot-Image","operator":"Google","category":"search","category_label":"Search engines","ua":"Googlebot-Image","robots_token":"Googlebot-Image","verification_method":"published-ranges","verification_label":"published IP ranges","respects_robots_txt":"documented","docs":"https://developers.google.com/search/docs/crawling-indexing/overview-google-crawlers","ip_ranges":"https://www.pathwren.workers.dev/c/rubygems-registry/ip-ranges/google-googlebot.json","url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/googlebot-image.html","what_it_is":"Image indexing for Google Images. A separate token so you can leave images out of search without leaving search.","cost_of_blocking":"Your images stop appearing in Google Images."},{"slug":"googlebot-news","name":"Googlebot-News","operator":"Google","category":"search","category_label":"Search engines","ua":"Googlebot-News","robots_token":"Googlebot-News","verification_method":"published-ranges","verification_label":"published IP ranges","respects_robots_txt":"documented","docs":"https://developers.google.com/search/docs/crawling-indexing/overview-google-crawlers","ip_ranges":"https://www.pathwren.workers.dev/c/rubygems-registry/ip-ranges/google-googlebot.json","url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/googlebot-news.html","what_it_is":"A robots.txt token controlling inclusion in Google News. It does not have its own user-agent string; the fetch arrives as Googlebot.","cost_of_blocking":"Removal from Google News, with normal Search unaffected."},{"slug":"googleother","name":"GoogleOther","operator":"Google","category":"ai-training","category_label":"AI training crawlers","ua":"GoogleOther","robots_token":"GoogleOther","verification_method":"published-ranges","verification_label":"published IP ranges","respects_robots_txt":"documented","docs":"https://developers.google.com/search/docs/crawling-indexing/overview-google-crawlers","ip_ranges":"https://www.pathwren.workers.dev/c/rubygems-registry/ip-ranges/google-special.json","url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/googleother.html","what_it_is":"A generic fetcher used by Google product teams for one-off crawls and research, including data collection that does not belong to Search.","cost_of_blocking":"No effect on Search indexing. Blocks internal Google research and product fetches."},{"slug":"gptbot","name":"GPTBot","operator":"OpenAI","category":"ai-training","category_label":"AI training crawlers","ua":"GPTBot","robots_token":"GPTBot","verification_method":"published-ranges","verification_label":"published IP ranges","respects_robots_txt":"documented","docs":"https://platform.openai.com/docs/bots","ip_ranges":"https://www.pathwren.workers.dev/c/rubygems-registry/ip-ranges/openai-gptbot.json","url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/gptbot.html","what_it_is":"OpenAI's bulk crawler. Pages it fetches may be used to train future OpenAI foundation models. It is not the bot that puts you in ChatGPT's search results, and blocking it does not remove you from them.","cost_of_blocking":"Your content is excluded from training data for future OpenAI models. No effect on ChatGPT search visibility, on citations, or on links a user pastes into ChatGPT."},{"slug":"ia-archiver","name":"ia_archiver","operator":"Internet Archive","category":"archive","category_label":"Archivers","ua":"ia_archiver","robots_token":"ia_archiver","verification_method":"none","verification_label":"no published verification method","respects_robots_txt":"documented","docs":"https://archive.org/details/archive.org_bot","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/ia-archiver.html","what_it_is":"The legacy Alexa/Internet Archive token, still present in most robots.txt files and still occasionally honoured.","cost_of_blocking":"Negligible today; retain for tidiness."},{"slug":"imagesiftbot","name":"ImagesiftBot","operator":"Hive AI","category":"dataset","category_label":"Corpus and dataset builders","ua":"ImagesiftBot","robots_token":"ImagesiftBot","verification_method":"none","verification_label":"no published verification method","respects_robots_txt":"documented","docs":"https://imagesift.com/about","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/imagesiftbot.html","what_it_is":"Crawls images for Hive AI's reverse-image and dataset products. Image-heavy sites see this one long before they see the text crawlers.","cost_of_blocking":"Your images stop entering an image dataset and reverse-image index."},{"slug":"img2dataset","name":"img2dataset","operator":"LAION / img2dataset","category":"dataset","category_label":"Corpus and dataset builders","ua":"img2dataset","robots_token":"img2dataset","verification_method":"none","verification_label":"no published verification method","respects_robots_txt":"documented","docs":"https://github.com/rom1504/img2dataset","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/img2dataset.html","what_it_is":"The tool used to turn image-URL lists such as LAION's into downloaded training sets. It is run by whoever is building a dataset, not by a single operator.","cost_of_blocking":"Your images are skipped when someone materialises an image-text dataset that references them."},{"slug":"meta-externalagent","name":"meta-externalagent","operator":"Meta","category":"ai-training","category_label":"AI training crawlers","ua":"meta-externalagent","robots_token":"meta-externalagent","verification_method":"none","verification_label":"no published verification method","respects_robots_txt":"documented","docs":"https://developers.facebook.com/docs/sharing/webmasters/web-crawlers","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/meta-externalagent.html","what_it_is":"Meta's AI crawler, gathering training data for Llama and Meta AI. It replaced the older FacebookBot name for this purpose.","cost_of_blocking":"Excluded from Meta AI training. Link previews on Facebook, Instagram and WhatsApp are unaffected — those are a different bot."},{"slug":"meta-externalfetcher","name":"meta-externalfetcher","operator":"Meta","category":"user-fetch","category_label":"User-triggered fetchers","ua":"meta-externalfetcher","robots_token":"meta-externalfetcher","verification_method":"none","verification_label":"no published verification method","respects_robots_txt":"documented","docs":"https://developers.facebook.com/docs/sharing/webmasters/web-crawlers","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/meta-externalfetcher.html","what_it_is":"Fetches a page when a Meta AI user asks about a specific link.","cost_of_blocking":"Meta AI cannot read pages users hand it."},{"slug":"mistralai-user","name":"MistralAI-User","operator":"Mistral AI","category":"user-fetch","category_label":"User-triggered fetchers","ua":"MistralAI-User","robots_token":"MistralAI-User","verification_method":"none","verification_label":"no published verification method","respects_robots_txt":"documented","docs":"https://docs.mistral.ai/","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/mistralai-user.html","what_it_is":"Fetches a page when a Le Chat user asks Mistral's assistant to read it.","cost_of_blocking":"Le Chat cannot open links your readers give it."},{"slug":"oai-searchbot","name":"OAI-SearchBot","operator":"OpenAI","category":"ai-search","category_label":"AI search crawlers","ua":"OAI-SearchBot","robots_token":"OAI-SearchBot","verification_method":"published-ranges","verification_label":"published IP ranges","respects_robots_txt":"documented","docs":"https://platform.openai.com/docs/bots","ip_ranges":"https://www.pathwren.workers.dev/c/rubygems-registry/ip-ranges/openai-searchbot.json","url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/oai-searchbot.html","what_it_is":"Builds the index ChatGPT search answers from. Content it collects is used for retrieval and citation, not for model training.","cost_of_blocking":"High. Blocking this removes you from ChatGPT search results and from the source links ChatGPT shows. This is the single most expensive block on this list for anyone who wants to be cited by an assistant."},{"slug":"omgili","name":"omgili","operator":"Webz.io","category":"dataset","category_label":"Corpus and dataset builders","ua":"omgili","robots_token":"omgili","verification_method":"none","verification_label":"no published verification method","respects_robots_txt":"documented","docs":"https://webz.io/blog/machine-learning/","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/omgili.html","what_it_is":"The older robots token for the same Webz.io collection, still honoured and still worth listing.","cost_of_blocking":"Same as omgilibot."},{"slug":"omgilibot","name":"omgilibot","operator":"Webz.io","category":"dataset","category_label":"Corpus and dataset builders","ua":"omgilibot","robots_token":"omgilibot","verification_method":"none","verification_label":"no published verification method","respects_robots_txt":"documented","docs":"https://webz.io/blog/machine-learning/","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/omgilibot.html","what_it_is":"Webz.io's crawler, collecting web and forum text sold as datasets, including to model builders.","cost_of_blocking":"Exclusion from a commercial dataset resold to third parties."},{"slug":"perplexity-user","name":"Perplexity-User","operator":"Perplexity","category":"user-fetch","category_label":"User-triggered fetchers","ua":"Perplexity-User","robots_token":"Perplexity-User","verification_method":"published-ranges","verification_label":"published IP ranges","respects_robots_txt":"by-design-no","docs":"https://docs.perplexity.ai/guides/bots","ip_ranges":"https://www.pathwren.workers.dev/c/rubygems-registry/ip-ranges/perplexity-user.json","url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/perplexity-user.html","what_it_is":"Fetches a page because a Perplexity user asked for it. Perplexity documents that this fetch is user-initiated and is therefore not governed by robots.txt — a robots rule will not stop it, by stated policy.","cost_of_blocking":"Not controllable via robots.txt. If you must stop it, verify by the published IP ranges and block at the edge — and accept that users who ask for your page get an error."},{"slug":"perplexitybot","name":"PerplexityBot","operator":"Perplexity","category":"ai-search","category_label":"AI search crawlers","ua":"PerplexityBot","robots_token":"PerplexityBot","verification_method":"published-ranges","verification_label":"published IP ranges","respects_robots_txt":"documented","docs":"https://docs.perplexity.ai/guides/bots","ip_ranges":"https://www.pathwren.workers.dev/c/rubygems-registry/ip-ranges/perplexity-bot.json","url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/perplexitybot.html","what_it_is":"Builds Perplexity's search index. Perplexity is citation-heavy by product design, so inclusion here converts to referral traffic more directly than most AI surfaces.","cost_of_blocking":"You stop being indexed and cited by Perplexity, and lose the referral clicks its citations produce."},{"slug":"petalbot","name":"PetalBot","operator":"Huawei","category":"search","category_label":"Search engines","ua":"PetalBot","robots_token":"PetalBot","verification_method":"reverse-dns","verification_label":"reverse DNS","respects_robots_txt":"documented","docs":"https://aspiegel.com/petalbot","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/petalbot.html","what_it_is":"Huawei's crawler for Petal Search, shipped as the default search on Huawei devices.","cost_of_blocking":"Removal from Petal Search. Frequently blocked for volume rather than for policy."},{"slug":"scrapy","name":"Scrapy","operator":"Scrapy project","category":"tool","category_label":"Tools and frameworks","ua":"Scrapy","robots_token":"Scrapy","verification_method":"none","verification_label":"no published verification method","respects_robots_txt":"documented","docs":"https://scrapy.org/","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/scrapy.html","what_it_is":"Not an operator: the default user-agent of the most common Python crawling framework. Anyone can be behind it. Modern Scrapy obeys robots.txt by default, which is why the default UA is still worth a rule.","cost_of_blocking":"You block a very large tail of unattributed one-off crawlers, and also every well-behaved researcher who did not change the default."},{"slug":"semrushbot","name":"SemrushBot","operator":"Semrush","category":"seo","category_label":"SEO and backlink crawlers","ua":"SemrushBot","robots_token":"SemrushBot","verification_method":"reverse-dns","verification_label":"reverse DNS","respects_robots_txt":"documented","docs":"https://www.semrush.com/bot/","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/semrushbot.html","what_it_is":"Semrush's backlink and keyword crawler. It is not an AI crawler, but it is usually in the top three by volume on any site, and it is the cheapest block on this list.","cost_of_blocking":"Your competitors' Semrush reports get thinner, and so do yours. No user-facing effect."},{"slug":"semrushbot-ocob","name":"SemrushBot-OCOB","operator":"Semrush","category":"ai-training","category_label":"AI training crawlers","ua":"SemrushBot-OCOB","robots_token":"SemrushBot-OCOB","verification_method":"reverse-dns","verification_label":"reverse DNS","respects_robots_txt":"documented","docs":"https://www.semrush.com/bot/","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/semrushbot-ocob.html","what_it_is":"Semrush's separately-tokenised crawler for its AI content tooling, split out so SEO crawling and AI reuse can be answered differently.","cost_of_blocking":"Exclusion from Semrush's AI corpus, with its SEO crawl unaffected."},{"slug":"seznambot","name":"SeznamBot","operator":"Seznam","category":"search","category_label":"Search engines","ua":"SeznamBot","robots_token":"SeznamBot","verification_method":"reverse-dns","verification_label":"reverse DNS","respects_robots_txt":"documented","docs":"https://napoveda.seznam.cz/en/seznamzbozi/subject-matter-crawler/","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/seznambot.html","what_it_is":"Seznam's crawler — the dominant search engine in the Czech Republic and one of the few national engines with its own index.","cost_of_blocking":"Removal from Seznam. Also removes you from its IndexNow endpoint's usefulness."},{"slug":"storebot-google","name":"Storebot-Google","operator":"Google","category":"search","category_label":"Search engines","ua":"Storebot-Google","robots_token":"Storebot-Google","verification_method":"published-ranges","verification_label":"published IP ranges","respects_robots_txt":"documented","docs":"https://developers.google.com/search/docs/crawling-indexing/overview-google-crawlers","ip_ranges":"https://www.pathwren.workers.dev/c/rubygems-registry/ip-ranges/google-special.json","url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/storebot-google.html","what_it_is":"Checks shopping and checkout flows for Google's shopping surfaces.","cost_of_blocking":"Product listings may lose shopping-specific enrichment. Irrelevant to non-commerce sites."},{"slug":"tiktokspider","name":"TikTokSpider","operator":"ByteDance","category":"ai-training","category_label":"AI training crawlers","ua":"TikTokSpider","robots_token":"TikTokSpider","verification_method":"none","verification_label":"no published verification method","respects_robots_txt":"disputed","docs":"https://www.bytespider.net/","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/tiktokspider.html","what_it_is":"A second ByteDance crawler identifying with TikTok, collecting page content for the same family of models.","cost_of_blocking":"Little to lose unless TikTok search referral matters to you."},{"slug":"timpibot","name":"Timpibot","operator":"Timpi","category":"search","category_label":"Search engines","ua":"Timpibot","robots_token":"Timpibot","verification_method":"none","verification_label":"no published verification method","respects_robots_txt":"documented","docs":"https://timpi.io/","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/timpibot.html","what_it_is":"A distributed crawler building an independent search index outside the Google/Bing duopoly.","cost_of_blocking":"Absence from a small independent index."},{"slug":"webzio-extended","name":"Webzio-Extended","operator":"Webz.io","category":"ai-training","category_label":"AI training crawlers","ua":"Webzio-Extended","robots_token":"Webzio-Extended","verification_method":"none","verification_label":"no published verification method","respects_robots_txt":"documented","docs":"https://webz.io/blog/machine-learning/","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/webzio-extended.html","what_it_is":"Webz.io's opt-out token specifically for AI training reuse, in the pattern Google and Apple established.","cost_of_blocking":"Your content is excluded from the AI-training tier of Webz.io's product while ordinary collection continues."},{"slug":"yandexbot","name":"YandexBot","operator":"Yandex","category":"search","category_label":"Search engines","ua":"YandexBot","robots_token":"YandexBot","verification_method":"reverse-dns","verification_label":"reverse DNS","respects_robots_txt":"documented","docs":"https://yandex.com/support/webmaster/robot-workings/check-yandex-robots.html","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/yandexbot.html","what_it_is":"Yandex's search crawler, which also feeds Alice and Yandex's generative answers.","cost_of_blocking":"Removal from Yandex Search. Verify with reverse DNS to a yandex.ru, yandex.net or yandex.com host — YandexBot is among the most-spoofed user-agents there is."},{"slug":"yeti","name":"Yeti","operator":"Naver","category":"search","category_label":"Search engines","ua":"Yeti","robots_token":"Yeti","verification_method":"reverse-dns","verification_label":"reverse DNS","respects_robots_txt":"documented","docs":"https://searchadvisor.naver.com/guide/seo-basic-crawl","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/yeti.html","what_it_is":"Naver's crawler. Naver is South Korea's largest search portal and runs its own index and its own generative answers.","cost_of_blocking":"Removal from Naver, which is most of Korean search."},{"slug":"youbot","name":"YouBot","operator":"You.com","category":"ai-search","category_label":"AI search crawlers","ua":"YouBot","robots_token":"YouBot","verification_method":"none","verification_label":"no published verification method","respects_robots_txt":"documented","docs":"https://about.you.com/youbot/","ip_ranges":null,"url":"https://www.pathwren.workers.dev/c/rubygems-registry/crawler/youbot.html","what_it_is":"You.com's crawler, feeding its AI search product and its search API.","cost_of_blocking":"Removal from You.com's index and from answers built on its API."}]}
@@ -0,0 +1,248 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ai-crawler-index — offline AI-crawler / bot user-agent classifier.
4
+ #
5
+ # Zero dependencies (stdlib JSON only), no network at require time. Every method
6
+ # is a pure function of the string you hand it, so it runs inside a Rack request,
7
+ # a Sidekiq worker, a Lambda or a machine with no route to the internet.
8
+ #
9
+ # require "ai_crawler_index"
10
+ #
11
+ # AiCrawlerIndex.ai_crawler?("Mozilla/5.0 (compatible; GPTBot/1.2; +https://openai.com/gptbot)")
12
+ # # => true
13
+ #
14
+ # AiCrawlerIndex.identify("Mozilla/5.0 (compatible; ChatGPT-User/1.0; +https://openai.com/bot)")
15
+ # # => {"slug"=>"chatgpt-user", "name"=>"ChatGPT-User", "operator"=>"OpenAI",
16
+ # # "category"=>"user-fetch", "verification_method"=>"published-ranges", ...}
17
+ #
18
+ # The table is generated from the AI Crawler Index, an independent,
19
+ # non-commercial public reference. Data: CC0-1.0. Code: MIT.
20
+
21
+ require "json"
22
+
23
+ module AiCrawlerIndex
24
+ DATA_PATH = File.join(__dir__, "ai_crawler_index", "data.json")
25
+
26
+ DATA = JSON.parse(File.read(DATA_PATH, encoding: "UTF-8")).freeze
27
+
28
+ VERSION = DATA["version"].freeze
29
+
30
+ # Every record in the index. Treat as read-only; #list hands out a copy.
31
+ CRAWLERS = DATA["crawlers"].freeze
32
+ # Category slug => {"label" => ..., "description" => ...}
33
+ CATEGORIES = DATA["categories"].freeze
34
+ # The category slugs that count as "AI" for .ai_crawler?
35
+ AI_CATEGORIES = DATA["ai_categories"].freeze
36
+
37
+ # Longest user-agent token first, so "Googlebot-Image" wins over "Googlebot"
38
+ # and "Claude-SearchBot" over "ClaudeBot". Ties broken by name, deterministically.
39
+ INDEX = CRAWLERS
40
+ .map { |r| [r["ua"].downcase, r] }
41
+ .sort_by { |needle, _| [-needle.length, needle] }
42
+ .freeze
43
+
44
+ # Compiled case-insensitive alternations, if you would rather match yourself.
45
+ PATTERNS = {
46
+ "all" => Regexp.new(DATA["regex"]["all"], Regexp::IGNORECASE),
47
+ "ai" => Regexp.new(DATA["regex"]["ai_only"], Regexp::IGNORECASE),
48
+ "by_category" => DATA["regex"]["by_category"]
49
+ .each_with_object({}) { |(k, v), h| h[k] = Regexp.new(v, Regexp::IGNORECASE) }
50
+ .freeze
51
+ }.freeze
52
+
53
+ # What this copy of the table is, and when it was measured.
54
+ META = {
55
+ "version" => DATA["version"],
56
+ "generated_at" => DATA["generated_at"],
57
+ "count" => CRAWLERS.length,
58
+ "source" => DATA["source"],
59
+ "data_url" => DATA["data_url"],
60
+ "data_license" => "CC0-1.0",
61
+ "window" => DATA["window"]
62
+ }.freeze
63
+
64
+ class << self
65
+ # The single best matching record for +ua+, or nil.
66
+ # Non-string and empty input returns nil and never raises.
67
+ def identify(ua)
68
+ hay = normalise(ua)
69
+ return nil if hay.empty?
70
+
71
+ INDEX.each { |needle, record| return record if hay.include?(needle) }
72
+ nil
73
+ end
74
+
75
+ # Every record whose user-agent token appears in +ua+, most specific first.
76
+ def match_all(ua)
77
+ hay = normalise(ua)
78
+ return [] if hay.empty?
79
+
80
+ INDEX.select { |needle, _| hay.include?(needle) }.map { |_, record| record }
81
+ end
82
+
83
+ # True when +ua+ is an AI crawler: training, AI search, user-triggered fetch or dataset.
84
+ def ai_crawler?(ua)
85
+ hit = identify(ua)
86
+ !hit.nil? && AI_CATEGORIES.include?(hit["category"])
87
+ end
88
+
89
+ # True when +ua+ is any known automated client in the index — AI or not.
90
+ def crawler?(ua)
91
+ !identify(ua).nil?
92
+ end
93
+
94
+ # "ai-training" / "ai-search" / "user-fetch" / "dataset" / "search" / "seo" /
95
+ # "archive" / "tool" / "preview" — or nil.
96
+ def category_of(ua)
97
+ hit = identify(ua)
98
+ hit && hit["category"]
99
+ end
100
+
101
+ # The operator's name ("OpenAI", "Anthropic", ...), or nil.
102
+ def operator_of(ua)
103
+ hit = identify(ua)
104
+ hit && hit["operator"]
105
+ end
106
+
107
+ # The whole table as a fresh array, or one category of it.
108
+ def list(category: nil)
109
+ return CRAWLERS.dup if category.nil?
110
+
111
+ CRAWLERS.select { |r| r["category"] == category }
112
+ end
113
+
114
+ # One record by its index slug, e.g. get("gptbot").
115
+ def get(slug)
116
+ CRAWLERS.find { |r| r["slug"] == slug.to_s }
117
+ end
118
+
119
+ # A robots.txt body for a stance. Nothing is written anywhere.
120
+ # "block-ai-training" blocks training and dataset crawlers
121
+ # "block-all-ai" blocks every AI class including user-triggered fetches
122
+ # "block-none" returns a permissive file
123
+ def robots_txt(stance = "block-ai-training")
124
+ want =
125
+ case stance.to_s
126
+ when "block-all-ai" then ->(r) { AI_CATEGORIES.include?(r["category"]) }
127
+ when "block-ai-training" then ->(r) { %w[ai-training dataset].include?(r["category"]) }
128
+ when "block-none" then ->(_r) { false }
129
+ else
130
+ raise ArgumentError,
131
+ "robots_txt: unknown stance #{stance.inspect} " \
132
+ "(block-ai-training | block-all-ai | block-none)"
133
+ end
134
+
135
+ tokens = CRAWLERS.select { |r| want.call(r) && r["robots_token"] }
136
+ .map { |r| r["robots_token"] }.uniq.sort
137
+ lines = ["# generated by ai-crawler-index #{VERSION} (stance: #{stance})",
138
+ "# source: #{DATA['source']}"]
139
+ tokens.each { |t| lines.concat(["", "User-agent: #{t}", "Disallow: /"]) }
140
+ lines.concat(["", "User-agent: *", "Disallow:", ""])
141
+ lines.join("\n")
142
+ end
143
+
144
+ # THE ONLY NETWORK PATH, and it is never called for you.
145
+ # Fetches the current table and returns a new Table with the same API;
146
+ # the bundled constants are not mutated.
147
+ def refresh(url = META["data_url"], timeout: 10)
148
+ require "net/http"
149
+ require "uri"
150
+ uri = URI.parse(url)
151
+ http = Net::HTTP.new(uri.host, uri.port)
152
+ http.use_ssl = (uri.scheme == "https")
153
+ http.open_timeout = timeout
154
+ http.read_timeout = timeout
155
+ res = http.request(Net::HTTP::Get.new(uri.request_uri,
156
+ "User-Agent" => "ai-crawler-index-rb/#{VERSION}",
157
+ "Accept" => "application/json"))
158
+ raise "refresh: HTTP #{res.code} from #{url}" unless res.is_a?(Net::HTTPSuccess)
159
+
160
+ Table.new(from_index_json(JSON.parse(res.body)))
161
+ end
162
+
163
+ # The public index document -> the record shape this gem exposes.
164
+ # The public document spells the token "user_agent_substring"; this gem
165
+ # spells it "ua", and that difference is how a live document is told apart
166
+ # from an already-gem-shaped one.
167
+ def from_index_json(doc)
168
+ rows = doc["crawlers"] || doc["agents"]
169
+ raise ArgumentError, "refresh: unexpected document — expected {\"crawlers\": [...]}" unless rows.is_a?(Array)
170
+ return doc if rows.first && rows.first.key?("ua")
171
+
172
+ {
173
+ "version" => doc["version"] || VERSION,
174
+ "generated_at" => doc["generated_at"],
175
+ "source" => META["source"],
176
+ "categories" => doc["categories"] || CATEGORIES,
177
+ "ai_categories" => AI_CATEGORIES,
178
+ "crawlers" => rows.map do |a|
179
+ {
180
+ "slug" => a["slug"], "name" => a["name"], "operator" => a["operator"],
181
+ "category" => a["category"], "category_label" => a["category_label"],
182
+ "ua" => a["user_agent_substring"], "robots_token" => a["robots_token"],
183
+ "verification_method" => a["verification_method"],
184
+ "verification_label" => a["verification_label"],
185
+ "respects_robots_txt" => a["respects_robots_txt"],
186
+ "docs" => a["operator_docs"], "ip_ranges" => a["ip_ranges_endpoint"],
187
+ "url" => a["html_url"], "what_it_is" => a["what_it_is"],
188
+ "cost_of_blocking" => a["cost_of_blocking"]
189
+ }
190
+ end
191
+ }
192
+ end
193
+
194
+ private
195
+
196
+ def normalise(ua)
197
+ ua.is_a?(String) ? ua.downcase : ""
198
+ end
199
+ end
200
+
201
+ # The same API over a table fetched at runtime. Returned by .refresh.
202
+ class Table
203
+ attr_reader :data, :crawlers, :ai_categories, :meta
204
+
205
+ def initialize(data)
206
+ @data = data
207
+ @crawlers = data["crawlers"]
208
+ @ai_categories = data["ai_categories"] || AI_CATEGORIES
209
+ @index = @crawlers.map { |r| [r["ua"].to_s.downcase, r] }
210
+ .sort_by { |needle, _| [-needle.length, needle] }
211
+ @meta = { "version" => data["version"], "generated_at" => data["generated_at"],
212
+ "count" => @crawlers.length, "source" => data["source"] }
213
+ end
214
+
215
+ def identify(ua)
216
+ hay = ua.is_a?(String) ? ua.downcase : ""
217
+ return nil if hay.empty?
218
+
219
+ @index.each { |needle, record| return record if !needle.empty? && hay.include?(needle) }
220
+ nil
221
+ end
222
+
223
+ def ai_crawler?(ua)
224
+ hit = identify(ua)
225
+ !hit.nil? && @ai_categories.include?(hit["category"])
226
+ end
227
+
228
+ def crawler?(ua)
229
+ !identify(ua).nil?
230
+ end
231
+
232
+ def category_of(ua)
233
+ hit = identify(ua)
234
+ hit && hit["category"]
235
+ end
236
+
237
+ def operator_of(ua)
238
+ hit = identify(ua)
239
+ hit && hit["operator"]
240
+ end
241
+
242
+ def list(category: nil)
243
+ return @crawlers.dup if category.nil?
244
+
245
+ @crawlers.select { |r| r["category"] == category }
246
+ end
247
+ end
248
+ end
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ai-crawler-index
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Pathwren
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: 'Give it a User-Agent string, get back what it is: 56 AI crawlers, search
13
+ engines, SEO bots, archivers and dataset builders from 30 operators, bundled as
14
+ one 45.4 KB JSON table. ai_crawler?(ua), identify(ua) -> {name, operator, category,
15
+ verification_method}, robots_txt(stance). Zero dependencies, no network at require
16
+ time, pure functions of the string you pass in. Data CC0-1.0 from the independent
17
+ AI Crawler Index; code MIT.'
18
+ executables:
19
+ - ai-crawler-index
20
+ extensions: []
21
+ extra_rdoc_files: []
22
+ files:
23
+ - LICENSE
24
+ - README.md
25
+ - exe/ai-crawler-index
26
+ - lib/ai-crawler-index.rb
27
+ - lib/ai_crawler_index.rb
28
+ - lib/ai_crawler_index/data.json
29
+ homepage: https://www.pathwren.workers.dev/c/rubygems-registry/
30
+ licenses:
31
+ - MIT
32
+ metadata:
33
+ homepage_uri: https://www.pathwren.workers.dev/c/rubygems-registry/
34
+ documentation_uri: https://www.pathwren.workers.dev/c/rubygems-registry/
35
+ changelog_uri: https://www.pathwren.workers.dev/c/rubygems-registry/changelog.html
36
+ data_source_uri: https://www.pathwren.workers.dev/c/rubygems-registry/data/agents.json
37
+ data_license: CC0-1.0
38
+ generated_at: '2026-09-01T21:09:27+00:00'
39
+ crawlers: '56'
40
+ rdoc_options: []
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 2.6.0
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ requirements: []
54
+ rubygems_version: 3.6.7
55
+ specification_version: 4
56
+ summary: 'Offline AI-crawler user-agent classifier: ai_crawler?(ua) and identify(ua)'
57
+ test_files: []