AI Citation Tracker (the 2026 GEO measurement surface)
You build a multi-engine AI citation tracker. Modern SEO platforms still optimize for Google's blue links. Meanwhile, generative engines deliver 30%+ of informational answers without a click — and those engines DO cite sources, just not in a place Search Console can see. Your job is to make that surface measurable.
============================================================ === PRE-FLIGHT ===
- Brand name(s): canonical + variants (e.g., "Stripe" + "stripe.com" + product names + key personnel).
- Domain(s): your URL(s) to detect in citations.
- Competitor list: 5-15 brands you track share of voice against. Without competitors, you have a vanity-metric tracker.
- Query universe: 25-200 priority queries. From: GSC top queries, internal taxonomy, sales objection list, support tickets, customer interviews.
- API access: At least one of — OpenAI API (ChatGPT with browsing), Anthropic API (Claude), Perplexity API, Google AI Studio API (Gemini), Bing Custom Search. Without APIs, fall back to headless browser polling (slower, more brittle).
- Storage: SQLite for solo / Postgres for team / BigQuery for enterprise.
Recovery:
- No competitor list → auto-derive top 5 from cited URLs after 1 week of polling, then prompt user to confirm.
- No API keys → generate Playwright-based engine adapters that hit the consumer UIs (with explicit fragility warning + need-for-residential-proxy notice).
============================================================ === PHASE 1: ENGINE ADAPTERS ===
One adapter per engine. Each adapter answers: "given query Q, what answer was generated and which URLs were cited?"
class EngineAdapter(Protocol):
name: str # "chatgpt" | "perplexity" | "claude" | "gemini" | "ai_overviews" | "bing_copilot"
async def query(self, query: str) -> EngineResult: ...
@dataclass
class EngineResult:
engine: str
query: str
asked_at: datetime
answer_text: str # the synthesized answer
citations: list[Citation] # ordered as cited
sources_attribution: str | None # raw sources HTML/markup if returned
raw_response: dict # for replay/debug
cost_usd: Decimal
@dataclass
class Citation:
rank: int # 1-indexed position in the answer
url: str
title: str | None
snippet: str | None
Engine-specific notes:
| Engine | Auth | Key URL | Cost ballpark | Notes |
|---|---|---|---|---|
| ChatGPT (browsing) | OpenAI API | /v1/responses w/ web_search_preview tool |
$5-15 per 1k queries | Returns annotations[] with citation URLs |
| Perplexity | Perplexity API | /chat/completions with model=sonar-pro |
$5/1M input + $15/1M output | Returns citations array natively |
| Claude (with search) | Anthropic API | /v1/messages w/ tools=[web_search_20250305] |
$3/1M input + $15/1M output + $10 per 1k searches | Returns citations as part of content |
| Gemini | Google AI Studio API | gemini-2.5-pro w/ Google Search grounding |
$1.25/1M + $5/1M | Returns grounding metadata with URLs |
| AI Overviews | SerpAPI / Bright Data SERP | n/a (no first-party API) | $5-20 per 1k queries | Scrape Google SERP, extract AI Overview block + sources |
| Bing Copilot | Bing Custom Search + Copilot scraper | n/a (no API) | $5-20 per 1k queries | Headless or SerpAPI |
| You.com | You.com API (free tier) | /api/search |
Free / metered | Returns sources |
| Phind | scrape only | n/a | proxy cost | Headless |
VALIDATION: At least 3 engines wired and returning citations end-to-end against a smoke-test query.
FALLBACK: If an engine API is down/rate-limited, the polling job continues other engines + retries the failed one with exp backoff. Single-engine failure never blocks the run.
============================================================ === PHASE 2: STORAGE SCHEMA ===
SQLite/Postgres schema (Prisma-style):
CREATE TABLE Query (
id INTEGER PRIMARY KEY,
text TEXT NOT NULL UNIQUE,
intent TEXT, -- informational | commercial | transactional
priority INTEGER DEFAULT 5, -- 1=critical, 10=long-tail
added_at DATETIME
);
CREATE TABLE Brand (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL, -- canonical
aliases TEXT, -- JSON array
domain TEXT, -- if applicable
is_self BOOLEAN DEFAULT FALSE, -- our brand vs competitor
added_at DATETIME
);
CREATE TABLE Poll (
id INTEGER PRIMARY KEY,
query_id INTEGER REFERENCES Query(id),
engine TEXT NOT NULL,
polled_at DATETIME NOT NULL,
answer_text TEXT,
cost_usd DECIMAL,
raw_response JSON
);
CREATE TABLE Citation (
id INTEGER PRIMARY KEY,
poll_id INTEGER REFERENCES Poll(id),
rank INTEGER,
url TEXT NOT NULL,
domain TEXT, -- derived
title TEXT,
snippet TEXT,
brand_matched INTEGER REFERENCES Brand(id) -- nullable
);
CREATE TABLE BrandMention (
id INTEGER PRIMARY KEY,
poll_id INTEGER REFERENCES Poll(id),
brand_id INTEGER REFERENCES Brand(id),
mention_count INTEGER, -- in answer_text
cited BOOLEAN, -- did a URL of theirs appear in citations
sentiment REAL -- -1 to 1, optional
);
CREATE INDEX idx_poll_query_engine_date ON Poll(query_id, engine, polled_at DESC);
VALIDATION: Schema migrates cleanly. Sample query inserts + reads.
============================================================ === PHASE 3: POLLING ENGINE ===
Generate the polling worker (Python / Node). Schedule:
- Daily: top 25 queries × all engines (≈ $5-15/day for ~150 calls).
- Weekly: full universe (100-200 queries) × all engines.
- On-demand: ad-hoc query investigation.
For each poll:
- Call engine adapter.
- Persist
Pollrow +Citationrows. - Run brand-mention detection on
answer_text:- Match canonical + aliases (case-insensitive, word-boundary regex).
- Count occurrences.
- Optional: sentiment via small LLM call on the mention context.
- Run domain-matching on citations against
Brand.domain.
Rate-limit aware: respect each provider's RPM. Built-in token-bucket. Exponential backoff on 429.
VALIDATION: Daily run completes within window. Cost stays under budget (configurable, default $20/day cap).
============================================================ === PHASE 4: METRICS & DELTAS ===
Computed per (engine, query, week):
| Metric | Definition |
|---|---|
| Cited | Boolean — did our URL appear in citations? |
| Mentioned | Boolean — did our brand name appear in answer text? |
| Citation rank | If cited, what position (1 = first)? |
| Share of voice | (our brand mentions) / (total brand mentions in answer) |
| Citation share | (our citations) / (total citations) |
| Competitor cited count | Distinct competitor brands cited in answer |
| Answer length | Word count of synthesized answer |
| WoW citation delta | Cited this week minus cited last week, by query |
| Top movers | Queries with largest WoW citation gain/loss |
| Citation gaps | Queries where competitors are cited but you aren't |
| Win-back queue | Queries where you WERE cited 4 weeks ago but no longer are |
Persist daily; aggregate weekly. Generate metrics_weekly.csv.
VALIDATION: Metrics reconcile (per-query sums match aggregates). Weekly delta is non-empty after 2+ weeks of polling.
============================================================ === PHASE 5: REPORTING ===
Generate three reports:
weekly_report.md — for the team / boss:
- TL;DR: citations gained, lost, share-of-voice movement.
- Top 5 winners (queries newly citing you).
- Top 5 losers (queries no longer citing you).
- Top 5 citation gaps (where competitor X is cited 5+ engines, you're cited 0).
- Recommended content actions (per query, what to write/update to get cited).
competitor_matrix.csv — engine × competitor matrix of citations:
| Query | ChatGPT | Perplexity | Claude | Gemini | AI Overview | Bing Copilot |
|---|---|---|---|---|---|---|
| "best CRM" | Us, Hubspot, Salesforce | Hubspot, Salesforce | Us | Hubspot | Salesforce, Us | Hubspot |
citation_gap_actions.md — prescriptive:
For each citation gap, output: target query + which engines miss us + competitor URLs cited + a content brief stub (chain into /seo-content-brief).
VALIDATION: Reports render. CSV imports cleanly into Excel/Sheets.
============================================================ === PHASE 6: ALERTING & DASHBOARD ===
Push-style alerts:
- Brand mention sentiment swing (sentiment drops > 0.3 in any engine) → Slack/email.
- Citation loss on top-10 query → Slack/email same-day.
- Competitor newly cited on tracked query → daily digest.
- Cost over budget → throttle + alert.
Optional dashboard: Streamlit or Next.js + Prisma. Tabs: Overview, Per-Engine, Per-Query, Competitor Drill, Cost.
VALIDATION: Alerts fire on simulated event in test. Dashboard renders against the SQLite DB.
============================================================ === PHASE 7: HOW THIS BEATS LEGACY SEO TOOLS ===
| Capability | Semrush / Ahrefs | Visibly AI | This skill |
|---|---|---|---|
| Google rank tracking | ✅ | ✅ (via GSC) | (separate skill: gsc-pull) |
| AI Overview citation tracking | partial | ❌ | ✅ |
| ChatGPT citation tracking | ❌ | ❌ | ✅ |
| Perplexity citation tracking | ❌ | ❌ | ✅ |
| Claude / Gemini tracking | ❌ | ❌ | ✅ |
| Share-of-voice across all AI engines | ❌ | ❌ | ✅ |
| Citation-gap → content brief chain | ❌ | partial | ✅ (→ seo-content-brief) |
| Open data (your DB, no vendor lock) | ❌ | ❌ | ✅ |
| Composable with other agents | ❌ | partial | ✅ |
| Cost: per-month | $129-$499 | €39-€399 | API costs only (~$10-50/mo) |
VALIDATION: This positioning resonates with users who already pay for one of the above.
============================================================ === SELF-REVIEW ===
Score 1-5:
- Complete: 6+ engine adapters, schema, polling, metrics, reports, alerts?
- Robust: Single-engine failure doesn't break the pipeline? Rate-limit / budget controls?
- Clean: Citations dedupe? Brand matching handles aliases/case?
- GEO-credible: Would a CMO who's seen Visibly / Profound / Otterly / Brand24's AI module recognize this as production-grade?
Common gap: matching only canonical brand name, missing variants. Generate the alias seed list from the user's marketing site + Wikipedia + Crunchbase.
============================================================ === LEARNINGS CAPTURE ===
~/.claude/skills/ai-citation-tracker/LEARNINGS.md.
============================================================ === STRICT RULES ===
- Never poll without rate-limit / budget caps. AI engine APIs are billed; runaway loops are expensive.
- Never silently drop a failed engine. Surface the failure in the run report.
- Never use brand matching by canonical name alone. Aliases + variants are required.
- Always include competitor tracking. Share-of-voice without comparison is a vanity number.
- Always preserve raw_response. New citation patterns emerge; you'll want to replay.