Tell agents, AI crawlers, search crawlers and bots apart from people.
Mintlify hosts documentation for thousands of companies, and those sites serve hundreds of millions of requests a month. For years almost all of that came from developers reading docs in a browser. Now a growing share comes from agents: Claude Code pulling a page into a coding session, ChatGPT answering a question with your docs, Cursor checking an API reference before it writes a call.
As that traffic grew, we kept needing to answer questions our tools couldn't. How much of our traffic is agents, and which ones? Is that request from GPTBot really OpenAI, or someone borrowing the name? Is a coding agent reading the docs, or is an AI company crawling them for training data, or a script mirroring llms.txt every hour? The bot lists we started with matched user agent substrings, put everything automated into one bucket, and believed whatever a request claimed to be.
So we built our own. We studied our traffic, tested the existing open-source bot lists against it, and kept track of which signals actually separate one kind of request from another. agent-traffic is what came out of that: a tested dataset of the user agents, headers and official verification sources of real agents and crawlers, plus a small classifier that applies it. We're open-sourcing it so you don't have to learn all of this from scratch.
Install the skill in Claude Code, Cursor, Codex or any agent the skills CLI supports:
npx skills add mintlify/agent-traffic
The skill teaches your agent how classification works and how to use the package and dataset, from request handlers to stored logs. Then ask it for whatever you need. You can also paste the skill's URL straight into any agent.
import { classify } from '@mintlify/agent-traffic'
classify({ userAgent: 'Claude-User (claude-code/2.1.308; +https://support.anthropic.com/)' })
// { category: 'agent', kind: 'coding', name: 'Claude Code', operator: 'Anthropic', version: '2.1.308', ... }
classify({ userAgent: 'axios/1.13.2' })
// { category: 'bot', kind: 'http-client', name: 'axios', ... }- A curated, tested dataset of the user agents, headers and official verification sources of real agents and crawlers
- Verifies crawlers against their operators' published IP ranges and catches impersonators
- Every result explains itself
- Zero dependencies, and runs anywhere
fetchdoes: Node, Bun, Deno, Cloudflare Workers, Vercel Edge
npm i @mintlify/agent-traffic
In a Cloudflare Worker, pass the request headers and the real client IP:
import { classify, loadIpRanges } from '@mintlify/agent-traffic'
let ipRanges
export default {
async fetch(request) {
ipRanges ??= loadIpRanges()
const result = classify(
{ headers: request.headers, ip: request.headers.get('cf-connecting-ip') },
{ ipRanges: await ipRanges, headersComplete: true }
)
if (result.category === 'agent') {
return serveMarkdown(request)
}
return fetch(request)
}
}The ranges load on the first request because Workers can't fetch at startup, and caching the promise keeps concurrent requests from each downloading them. headersComplete turns on the fake-browser checks, which is safe when browser headers reach the Worker untouched over HTTPS; leave it off if plain HTTP or a proxy in front can strip them.
Someone spoofing a crawler gets caught:
classify({ userAgent: 'Mozilla/5.0 (compatible; GPTBot/1.4; +https://openai.com/gptbot)', ip: '73.12.4.9' }, { ipRanges })
// { category: 'bot', kind: 'impersonator', verified: false, confidence: 'high',
// reasons: ["claims to be GPTBot but 73.12.4.9 is outside OpenAI's published ranges"] }Agents that hide behind a browser user agent still ask for markdown:
classify({ headers: { 'user-agent': 'Mozilla/5.0 (Macintosh; ...) Chrome/149.0.0.0 Safari/537.36', accept: 'text/markdown, text/html;q=0.9' } })
// { category: 'agent', kind: 'unidentified', confidence: 'medium',
// reasons: ['Accept header prefers markdown over HTML, which browsers never do'] }Just need a quick check? Each category is also available as a single regular expression:
import { patterns } from '@mintlify/agent-traffic'
patterns.agent.test(userAgent)
patterns['ai-crawler'].test(userAgent)Your agent can do this for you with the skill. It translates the patterns for your database; ClickHouse, DuckDB and Postgres are verified.
To do it yourself, classify each distinct user agent once and join the results:
import { classify } from '@mintlify/agent-traffic'
const rows = distinctUserAgents.map((ua) => ({ user_agent: ua, ...classify({ userAgent: ua }) }))
// load rows into a table, then: SELECT ... FROM logs JOIN user_agent_classes USING (user_agent)Logs usually keep only the user agent. That still identifies known agents, crawlers and HTTP clients, but agents that borrow a browser user agent will look like people unless you also log the Accept header.
category |
Meaning | kind |
|---|---|---|
agent |
An AI product fetching a page right now, for a person | assistant, coding, browser, unidentified |
ai-crawler |
An AI company collecting pages ahead of time | training, ai-search, ingest |
crawler |
A search engine | search |
bot |
Any other automation | seo, preview, ads, monitor, scanner, archiver, feed, http-client, headless, fake-browser, impersonator, unidentified |
human |
A person in a browser | |
unknown |
Nothing to classify (no user agent or headers) |
request:
| Field | Type | |
|---|---|---|
userAgent |
string |
Read from headers when omitted |
headers |
Headers | Record<string, string | string[]> |
Any casing |
ip |
string |
The real client IP. Ports and IPv4-mapped IPv6 addresses are handled |
options:
| Field | Default | |
|---|---|---|
ipRanges |
none | From loadIpRanges(). Turns on IP verification |
headersComplete |
false |
Set when browser headers reach you untouched over HTTPS. Turns on fake-browser checks |
maxRangeAgeDays |
180 |
An operator's range file older than this can verify a request, but never flag it as an impersonator |
Returns:
{
category: 'agent' | 'ai-crawler' | 'crawler' | 'bot' | 'human' | 'unknown'
kind?: string
id?: string
name?: string
operator?: string
version?: string
verified: boolean | null
confidence: 'high' | 'medium' | 'low'
reasons: string[]
}verified: truemeans the IP is in the operator's published ranges.verified: falsemeans it's provably not who it claims to be.verified: nullmeans it wasn't checked, or can't be. Coding agents like Claude Code run on the user's own machine, so they're never IP-checked.
Downloads the IP range files published by OpenAI, Anthropic, Google, Perplexity, Apple, Microsoft, Common Crawl and DuckDuckGo. Call it once at startup, refresh it about daily, and pass the result to classify. Files that fail to load are skipped, so requests from those operators come back unverified rather than flagged.
The dataset, also importable as plain JSON from @mintlify/agent-traffic/data/*.json for use from other languages or SQL:
agents.jsonlists known agents and crawlers. Each entry has user agent patterns, Accept andSignature-Agentfingerprints, verification sources, docs, real example user agents, and the date it was last verified.http-clients.jsonlists generic HTTP libraries and headless browsers.headers.jsonsays what each request header tells you, and who controls it.
patterns is one case-insensitive RegExp per category: agent, ai-crawler, crawler and bot. userAgents holds the same patterns as arrays of strings, for building your own matchers. They match user agents only. Use classify to get verification, Accept and header checks.
true when an Accept header ranks text/markdown above text/html.
Checks run in order, and the first one that answers wins:
- Known agent or crawler, verified by IP when possible
- Asks for markdown
- Generic HTTP client or headless browser
- Browser, unless its headers contradict it
- Otherwise a bot
GUIDELINES.md covers the full rules, why they work this way, and how to apply them without this package.
- Some products share an identity. Codex has been reported to fetch as
ChatGPT-User, and Cursor through Exa asExaUser. Results name only what can be proven. - Some agents are invisible at the HTTP level. Agentic browsers that run inside a person's own Chrome (Atlas, Comet, Claude in Chrome) look exactly like that person.
Agents change their fetchers often. If you see one that's missing or wrong, open a PR against data/agents.json with a real example user agent and a source. See CONTRIBUTING.md.
MIT © Mintlify