Actors
Actor classification
Every event gets one of five actors. Classification happens server-side in Go and is deterministic.
Client-side scripts never see AI crawlers. User-agent lists alone are spoofable. Vector 5 classifies every ingest in apps/api/internal/classify using a curated UA list, assistant referrers / UTMs, and headless heuristics. ADR 0004.
The five actors
humanBrowser visitor. JavaScript ran.
human_via_aiPerson who arrived from an assistant.
ai_crawlerGPTBot, ClaudeBot, PerplexityBot, and peers.
ai_agentHeadless agent. No JavaScript.
botUptime, previews, Lighthouse, generic automation.
| Actor | Rule |
|---|---|
ai_crawler | UA matches a known AI crawler (GPTBot, OAI-SearchBot, ChatGPT-User, ClaudeBot, Claude-User, PerplexityBot, Google-Extended, Bytespider, CCBot, Amazonbot, Meta-ExternalAgent, …). Mapped to a source (chatgpt, claude, perplexity, gemini, …). |
human_via_ai | Referrer host is an assistant (chatgpt.com, perplexity.ai, claude.ai, gemini.google.com, copilot.microsoft.com, grok.com, chat.deepseek.com) or utm_medium is ai_search / ai-assistant, or utm_source names an assistant. |
ai_agent | Headless signature (headless, playwright, puppeteer, crawl4ai, agent…) and no JavaScript executed. |
bot | Generic automation: bot, crawler, spider, uptime monitors, link previewers, Lighthouse. |
human | Everything else. |
Evaluation order
- Known AI crawler UA →
ai_crawler. - Generic bot UA →
bot, orai_agentif it also looks headless and JS did not run. - Assistant referrer or AI UTM →
human_via_ai. - Headless UA and no JS →
ai_agent. - Otherwise →
human.
Browser events from v5.js pass jsExecuted = true, so they almost never become ai_agent. Middleware hits pass false.
Source and AI source
ai_source is the assistant or vendor (chatgpt, claude, perplexity, gemini, copilot, grok, deepseek). source is the report-friendly name — crawler name, UTM, or a mapped host (Google, Bing, Hacker News). The full host map lives next to the classifier.
Verified crawlers
The result includes a verified flag reserved for vendor IP / reverse-DNS checks when publishers document them. Today the curated UA list is the primary signal. Pull requests adding crawlers are welcome — the public list is GET /api/crawler-index.
Something off? Open an issue · Edit on GitHub

