AI Visibility (AEO)

Answer engines can only cite pages their crawlers have read. Outside-in tools sample what the models say about a brand; your site can do better, because the crawlers hit your server. BusinessDash records those fetches first-party, per page, per crawler: "ChatGPT's crawler read your pricing page 40 times this month; Perplexity has never fetched it."

There are two kinds of AI visitors, and they arrive differently:

KindExamplesRuns JS?How it's captured
Live assistantsChatGPT-User, Claude-User, Perplexity-UserYesThe normal <BDAnalytics /> tracker — nothing to add
Training & search crawlersGPTBot, ClaudeBot, PerplexityBot, CCBot, Bytespider, …NoYour server reports them (below)

Report crawler fetches from your server

Training and search crawlers fetch HTML without executing JavaScript, so the browser tracker never sees them. Your middleware does — forward the hit:

// middleware.ts (Next.js)
import {
  isAiCrawlerUserAgent,
  reportAiCrawlerHit,
} from '@businessdash/sdk/analytics-server'

export function middleware(request: Request) {
  const ua = request.headers.get('user-agent')
  if (isAiCrawlerUserAgent(ua)) {
    // Fire-and-forget — never block or fail the crawler's request.
    void reportAiCrawlerHit({
      siteId: process.env.BD_SITE_ID!,
      baseUrl: process.env.NEXT_PUBLIC_BD_API_URL!,
      apiKey: process.env.NEXT_PUBLIC_BD_PUBLISHABLE_KEY!,
      userAgent: ua!,
      path: new URL(request.url).pathname,
    })
  }
}

isAiCrawlerUserAgent is a cheap prefilter so you only spend a network call on requests that will be kept. The platform's classifier is authoritative: a forwarded user-agent is stored only when it matches a known AI crawler, and anything else is dropped — this path records crawler visibility, it can never inflate your human visitor counts. Crawler rows are likewise excluded from every human metric (pageviews, visitors, device breakdowns).

Read the numbers back

const ai = await bd.site(siteId).analytics.aiVisibility({ days: 30 })

if (ai.available) {
  for (const c of ai.crawlers) {
    console.log(
      `${c.name} (${c.vendor}) — ${c.fetches} fetches`,
      `across ${c.distinctPages} pages, last seen ${c.lastSeenAt}`,
    )
  }
}

Each entry carries an intent that tells you what the fetch means:

  • training — corpus building (GPTBot, ClaudeBot, CCBot). Being read here means the next model generation may know the page.
  • search — answer-engine indexing (OAI-SearchBot, PerplexityBot). This is citation supply: pages these crawlers read can be cited today.
  • user — a live assistant fetching on a person's behalf. The closest thing to an AI-referred visit.

The underlying endpoint is GET /sites/{siteId}/analytics/ai-crawlers (days query param, default 30, capped at 365). It's scoped analytics:read on your publishable key and returns aggregates only — no event rows.

Entitlement

Like the rest of the analytics surface, AI visibility is gated on the analytics.web feature. An org without it gets { available: false } — branch on .available and render a fallback rather than catching.

Notes

  • A page no crawler has fetched can't be cited by an answer engine — treat "never fetched" as a finding, not an absence of data.
  • Google's Google-Extended is a robots.txt token, not a fetching user-agent (Googlebot does the fetching), so it never appears here.
  • Counts stay near zero until the middleware above is wired — the live assistants are the only AI visitors the browser tracker can see on its own.