🧠 HeyCMO
Features

AI Citation Tracking

Daily monitoring of brand visibility across ChatGPT, Perplexity, Gemini, and Google AI Overviews — with email alerts when citation status changes.

AI Citation Tracking

The Citation Tracker answers a question that's gone from "nice to have" to existential for any modern brand: "Do we appear in AI answers when someone asks the questions our customers ask?"

It runs the queries you care about against ChatGPT, Perplexity, Gemini, and Google AI Overviews on a daily cron, captures whether your brand appears (and where), and emails you the moment that status changes.

What it does

  1. You add tracked queries — e.g. best AI marketing agent, top tools for SaaS SEO. Optionally specify the brand terms / domains the classifier should scan for. Empty list = inferred from customer.brandName + customer.website host.
  2. A daily cron fires each query against each engine in parallel.
  3. Results are persisted as immutable rows: appeared (true/false/null), position (when listed), citationType (primary / mentioned / cited-source), full rawResponse (truncated to 16 KB for replay-debugging).
  4. Status changes trigger an email alert — flipping into / out of presence, or moving in/out of the top-3 — when alertOnChange is on.

Configuration

PropertyValue
Phase7
SchemaTrackedQuery, CitationResult
Migrationprisma/migrations/20260428000400_citation_tracking/
Engine clientsapps/api/agent/lib/citation-tracking/engines/*
Runnerapps/api/agent/lib/citation-tracking/runner.ts
Classifierapps/api/agent/lib/citation-tracking/classify.ts
Alert sinkapps/api/agent/lib/citation-tracking/alert-sink.ts
UI/intelligence/citations
Crondaily-citation-tracking0 8 * * * (daily 8 AM UTC)
Workflowapps/api/agent/workflows/citation-tracking.ts (registered as a global Inngest function — single run, not per-customer fan-out)

Engines and credentials

Each engine has a graceful "no credential" fallback: if the relevant key isn't set we record appeared = NULL with a friendly errorMessage rather than failing the whole run.

EngineCredentialImplementation
ChatGPTOPENAI_API_KEYOfficial openai SDK, model gpt-4o-mini, max 600 tokens
PerplexityPERPLEXITY_API_KEYREST → https://api.perplexity.ai/chat/completions, model sonar (online)
GeminiGOOGLE_GENERATIVE_AI_API_KEYREST → Generative Language API, model gemini-1.5-flash-latest
Google AI OverviewsSERPER_API_KEY or SERPAPI_KEYComposes AI-overview text + answer-box + top-5 organic into one classified blob

Classifier heuristics

Pure-function — apps/api/agent/lib/citation-tracking/classify.ts. Designed so the runner can mock engines and test classification in isolation.

  • Appeared — case-insensitive substring match on any brandTerm (whole-word OR hostname).
  • Position — line-by-line list parser that recognises 1., 1), (1), - , * , markers. Honours declared numbering.
  • Citation type:
    • primary — at position 1, OR within first 240 chars near a strong-recommendation phrase (e.g. "top pick", "#1", "best overall").
    • cited-source — appears in markdown link [text](url) or under a Sources: / References: block.
    • mentioned — anything else with appeared = true.

API endpoints

All under /api/citation-tracking/:customerId/.... Auth: API key, tenant-scoped via getTenantId(c).

MethodPathRBACPurpose
POST/querieseditorCreate a tracked query ({ query, engines?, brandTerms?, alertOnChange? })
GET/queriesviewerList the customer's enabled queries with the latest snapshot per engine
PATCH/queries/:queryIdeditorToggle alertOnChange / enabled
DELETE/queries/:queryIdeditorSoft-delete (sets enabled = false; results retained for trend charts)
GET/queries/:queryId/resultsviewerLast 30 daily results × 4 engines, newest first, for the trend sparkline
POST/queries/:queryId/run-noweditorManual trigger — calls the same runCitationCheck the cron does

Tenant scoping: every handler resolves customerId via getTenantId(c) so a leaked API key never leaks across tenants. Soft-delete keeps history queryable; hard-delete cascades through Prisma.

Change-detection rules

Implemented in isMeaningfulChange(prev, next):

  • ✅ Flip appeared = true ↔ false
  • ✅ Move into / out of top-3 by position
  • citationType changes (e.g. mentionedprimary)
  • ❌ A run with appeared = null (engine skipped) on either side never counts — prevents false alerts when a key is briefly missing.

Testing

  • Pure-function tests__tests__/classify.test.ts (18 tests, all passing). Cover regex escape, list parsing, citation-type detection, and change-detection edge cases.
  • Engine clients — adapter pattern lets each engine be unit-tested with a stubbed network adapter. The probeChatGPT(input, { adapter: { complete: stub }}) shape is the contract.
  • E2ErunCitationCheck(...) is wired so a real PrismaClient + mock probers can exercise the persistence + alert path end-to-end.

Why this exists

AI citation visibility is the new SERP rank. SEO told you when you fell off page 1; nothing told you when ChatGPT stopped recommending you. This closes that gap.

On this page