Track whether AI answer engines mention and cite your brand. A GEO / AEO visibility & share-of-voice tracker for the age of ChatGPT, Perplexity, Google AI Overviews and Gemini.
Search is moving from ten blue links to a single AI-generated answer. If the answer engine doesn't mention or cite you, you're invisible — and classic rank trackers can't see it. gh-ai-rank-tracker measures your presence inside the answers: are you named, how prominently, are you cited, and how do you stack up against competitors.
Generative Engine Optimization (GEO) and Answer Engine Optimization (AEO) are the new front line of organic visibility. Teams need a repeatable way to answer:
- For the prompts our buyers actually ask, does the AI mention us at all?
- When it does, are we named early or buried at the end?
- Does it cite our domain as a source — and at what rank?
- What's our share of voice versus the competitors the AI keeps recommending?
This tool turns those questions into a single, repeatable AI Visibility Score (0–100) plus an actionable breakdown.
- Mention detection — boundary-aware, alias-aware, case-insensitive (no "Notion" inside "promotional" false positives).
- Citation detection — normalizes URLs and matches your domain (and subdomains), with citation rank.
- Visibility scoring — a transparent, tunable 0–100 score combining mention presence, mention prominence, citation presence and citation rank.
- Share of voice — benchmark presence and mention volume against any set of competitors.
- Coverage + gaps — see exactly which prompts return zero mentions of you.
- Recommendations — prioritized, rule-based next steps (high / medium / low).
- Campaigns & tracking over time — define a prompt set per brand, run it repeatedly, and persist every run to a local-first JSON store (no external DB) so you can watch your visibility + share-of-voice trend across runs.
- Competitor share-of-voice — track competitors alongside your brand and get a head-to-head SoV gap, aggregated across prompts × engines.
- Exportable reports — a Markdown report or a dependency-free PDF.
- Live answer engines — first-class adapters for OpenAI, Perplexity, Anthropic (Claude) and Google Gemini, all behind your own API keys and all implementing one
AnswerEngineProviderinterface. The deterministicMockProvideris the no-key default, so the whole engine still runs offline and is fully unit-tested. - Reports — console, Markdown, or raw JSON.
git clone https://github.com/aymandakir-gh/gh-ai-rank-tracker.git
cd gh-ai-rank-tracker
npm installRun the built-in demo (no API keys, no setup):
npm run demo # console report
npm run demo -- --markdown # Markdown report
npm run demo -- --json # raw JSONRun the tests:
npm testCreate a JSON TrackingConfig (see examples/demo-config.json):
{
"brand": { "name": "GrowthHackers", "aliases": ["GH"], "domain": "growthackers.io" },
"competitors": [{ "name": "HubSpot", "domain": "hubspot.com" }],
"prompts": [
{ "prompt": "best growth marketing agencies for B2B SaaS", "weight": 2 }
]
}npm run cli -- --config ./examples/demo-config.json --markdownOr use it as a library:
import { runTracking, MockProvider } from "gh-ai-rank-tracker";
const report = await runTracking(config, [new MockProvider({ /* scripted answers */ })]);
console.log(report.visibilityScore, report.shareOfVoice, report.gaps);A campaign is a named brand + competitors + a set of prompts. Each run is
scored, benchmarked for share-of-voice, and appended to a local-first JSON
store (default ./.tracker/store.json, or set TRACKER_STORE_PATH) — no
external database. Run it again over time and the trend builds up.
# Run + persist the built-in demo campaign (no API keys):
npm run cli -- campaign run --demo
# Run your own campaign (JSON Campaign file) against a live engine:
OPENAI_API_KEY=sk-... npm run cli -- campaign run --config ./my-campaign.json --provider openai
npm run cli -- campaign list # stored campaigns + run counts
npm run cli -- campaign history demo-growthhackers # visibility + SoV trend over timeA Campaign JSON file looks like:
{
"id": "acme-geo",
"name": "Acme — GEO visibility",
"brand": { "name": "Acme", "domain": "acme.com" },
"competitors": [{ "name": "Rival", "domain": "rival.com" }],
"prompts": [
{ "prompt": "best widget vendors for enterprise", "weight": 2 },
{ "prompt": "how to choose a widget platform", "weight": 1 }
]
}From code, the same flow is runCampaign(campaign, providers) → append to a
JsonFileStore/InMemoryStore → computeTrend(runs).
npm run cli -- campaign export acme-geo --format md --out report.md
npm run cli -- campaign export acme-geo --format pdf --out report.pdfThe PDF writer is pure TypeScript (no native deps): a valid PDF 1.4 with the base-14 Helvetica font and a correct cross-reference table — it opens in any viewer. The web dashboard also offers a one-click Markdown download.
The Next.js app (web/) includes a campaign dashboard (/campaign): a
share-of-voice trend chart over time, a per-engine breakdown, a
competitor comparison, and an expandable per-prompt drill-down — fully
internationalized across 9 languages (?lang=), Tailwind-only charts with
<table> accessibility fallbacks, and self-contained (works without API keys).
cd web && npm install && npm run dev # http://localhost:3003/campaignFull math + assumptions + limitations: METHODOLOGY.md.
Each (brand, response) pair earns up to 100 points from four signals (weights are tunable via ScoreWeights):
| Signal | Default weight | Meaning |
|---|---|---|
| Mention presence | 35 | The brand is named in the answer |
| Mention prominence | 20 | Scaled by how early the first mention appears |
| Citation presence | 30 | The brand's domain appears in the sources |
| Citation prominence | 15 | Scaled by how near the top the citation sits |
Per-prompt scores are the mean across engines; the overall AI Visibility Score is the prompt-weighted average.
The scoring engine is provider-agnostic — every engine implements one interface:
export interface AnswerEngineProvider {
readonly engine: string;
query(prompt: string): Promise<EngineResponse>;
}Four live adapters ship alongside the offline MockProvider. Each reads its
key from the environment only (never committed) and uses the provider's
web-search / grounding capability so answers come back with real source
citations:
| Provider | --provider value |
Env var | Model override | Default model |
|---|---|---|---|---|
| Mock (offline default) | mock |
— | — | — |
| OpenAI | openai |
OPENAI_API_KEY |
OPENAI_MODEL |
gpt-4o |
| Perplexity | perplexity |
PERPLEXITY_API_KEY |
PERPLEXITY_MODEL |
sonar |
| Anthropic (Claude) | anthropic |
ANTHROPIC_API_KEY |
ANTHROPIC_MODEL |
claude-sonnet-4-6 |
| Google Gemini | gemini |
GEMINI_API_KEY (or GOOGLE_API_KEY) |
GEMINI_MODEL |
gemini-2.0-flash |
Gemini grounding note: Gemini returns sources as grounding-redirect links with the source domain in the
title. The adapter maps a domain-shaped title tohttps://<domain>so brand-domain citation detection works, falling back to the raw redirect URL otherwise.
Copy the example env file and fill in only the keys you have — any provider whose key is missing simply isn't available (the others still work):
cp .env.example .env # then edit .env
# or export inline:
export OPENAI_API_KEY=sk-...
export PERPLEXITY_API_KEY=pplx-...
export ANTHROPIC_API_KEY=sk-ant-...# Quick analysis of a brand URL with a live engine:
OPENAI_API_KEY=sk-... npm run cli -- --provider openai --url https://yourbrand.com --markdown
PERPLEXITY_API_KEY=pplx-... npm run cli -- --provider perplexity --url https://yourbrand.com
ANTHROPIC_API_KEY=sk-ant-... npm run cli -- --provider anthropic --config ./examples/demo-config.jsonOr from code:
import { runTracking, OpenAIProvider, AnthropicProvider } from "gh-ai-rank-tracker";
const report = await runTracking(config, [
new OpenAIProvider(), // reads OPENAI_API_KEY
new AnthropicProvider(), // reads ANTHROPIC_API_KEY
]);Cost note: live providers make real, billable API calls. The CLI and library never call out unless you select a live
--provider; the default is always the offlineMockProvider.
A committed, plain-text transcript of the real CLI (--demo, campaign run/list/history/export) lives at docs/demo.txt — a
zero-dependency fallback for the animated GIF.
To render the GIF, a ready-to-run vhs
script is committed at docs/demo.tape:
vhs docs/demo.tape # → docs/demo.gifThen swap docs/demo.gif into the <!-- DEMO PLACEHOLDER --> block at the top
of this README.
The Hono HTTP API exposes:
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/health |
None | Health check — returns { ok, version, ts } |
POST |
/api/scan |
Bearer | Run a full AI visibility scan for a URL |
POST |
/api/campaign |
Bearer | Run + persist a campaign; returns the run, full history and trend |
GET |
/api/campaign/:id |
Bearer | Read a campaign's persisted history + trend |
curl -X POST https://<your-domain>/api/scan \
-H "Authorization: Bearer $SCAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://yoursite.com","providers":["mock"]}'Request body:
| Field | Type | Required | Description |
|---|---|---|---|
url |
string | ✅ | Brand URL — name + domain inferred automatically |
providers |
string[] | ❌ | any of "mock", "perplexity", "openai", "anthropic", "gemini" (default: ["mock"]). Live providers require the matching API key in the server env. |
-
Create a new Railway service linked to this repo.
-
Set environment variables in the Railway dashboard:
Variable Required Description SCAN_API_KEY✅ Bearer token for API auth — generate with openssl rand -hex 32PERPLEXITY_API_KEYOnly for provider=perplexityPerplexity API key OPENAI_API_KEYOnly for provider=openaiOpenAI API key ANTHROPIC_API_KEYOnly for provider=anthropicAnthropic API key PORT❌ Injected automatically by Railway -
Deploy — Railway picks up
railway.tomlautomatically:- Build:
npm run build(TypeScript →dist/) - Start:
npm start(node dist/src/server.js) - Healthcheck:
GET /health(timeout 30 s)
- Build:
Note: The server will refuse to start (
process.exit(1)) ifSCAN_API_KEYis not set outside ofNODE_ENV=development. This is intentional — prevents accidental open deployments.
# Run API server without auth (dev mode)
NODE_ENV=development npm run api:dev
# Run with auth (mirrors production)
SCAN_API_KEY=dev-secret npm run api:devThe Next.js web UI runs the engine in-process (no separate API server needed) and serves on port 3003:
cd web
npm install
npm run dev # → http://localhost:3003 (dashboard at /campaign)Optional web env vars live in web/.env.example (e.g. LEADS_API_URL for the
lead-capture route). The web app does not proxy to the Hono API.
- Core scoring engine (mention + citation + share of voice + gaps)
- REST API (Hono, Bearer auth, rate limiting)
- Web UI (Next.js, i18n 9 languages, a11y)
- Live provider adapters — OpenAI, Perplexity, Anthropic (Claude), Google Gemini
- Email gate + lead capture (web)
- Observability — Sentry + PostHog on the web app (graceful-degrade)
- Campaigns + local-first persisted store + historical trend tracking
- Competitor share-of-voice benchmarking (CLI + API + web)
- Web campaign dashboard — trend chart, per-engine, competitor, drill-down (i18n)
- Exportable campaign report — Markdown + dependency-free PDF
- Google AI Overviews adapter
Built and maintained by GrowthHackers, a data-driven growth marketing agency. We build open tools for GEO, AEO and AI search visibility. Issues and PRs welcome.
MIT © GrowthHackers (GH)
