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

  • human

    Browser visitor. JavaScript ran.

  • human_via_ai

    Person who arrived from an assistant.

  • ai_crawler

    GPTBot, ClaudeBot, PerplexityBot, and peers.

  • ai_agent

    Headless agent. No JavaScript.

  • bot

    Uptime, previews, Lighthouse, generic automation.

ActorRule
ai_crawlerUA 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_aiReferrer 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_agentHeadless signature (headless, playwright, puppeteer, crawl4ai, agent…) and no JavaScript executed.
botGeneric automation: bot, crawler, spider, uptime monitors, link previewers, Lighthouse.
humanEverything else.

Evaluation order

  1. Known AI crawler UA → ai_crawler.
  2. Generic bot UA → bot, or ai_agent if it also looks headless and JS did not run.
  3. Assistant referrer or AI UTM → human_via_ai.
  4. Headless UA and no JS → ai_agent.
  5. 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