geo-aeo-tracker
AGENTS.md328 starsChanged 28 days agoMITView on GitHub
GEO/AEO Tracker measures how often AI answer engines (ChatGPT, Perplexity, Gemini, Google AI Mode) mention and cite a brand. It also serves a remote MCP server at /api/mcp and ships a CLI on npm (geo-aeo-tracker, source in cli/). It is a Next.js 16 App Router app on React 19 and TypeScript, styled with Tailwind 4, with zod for request validation and Recharts for charts. State lives in the browser (idb-keyval over IndexedDB) by default, with optional Supabase sync. MIT licensed.…
- Reads credentials
- Installs packages
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 31,439 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository3 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- .github/copilot-instructions.mdCopilot instructions
- cli/llms.txtllms.txt
The file
Show its rules and text
We split this AGENTS.md into the rules it gives an agent
13 rules. About 0 are common, 0 are distinctive, and 13 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
13
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- could be automatic: a setting could enforce it
- Very specific: line 49
Never commit .env. .gitignore excludes .env* except .env.example.
- Very specific: line 69
All routes are unauthenticated except state.
- Very specific: line 90
Local-first: No server database by default.
- Very specific: line 91
Validation: Every route defines a zod schema at the top and parses the body before doing work.
- Very specific: line 92
Outbound calls: Use fetchWithTimeout or withTimeout from lib/server/http.ts.
- Very specific: line 93
User-supplied URLs fetched server-side: must pass assertPublicHttpUrl from lib/server/ssrf.ts.
- Very specific: line 101
Add it to the z.enum in app/api/scrape/route.ts, and to ProviderSchema and providerToDatasetEnv in lib/server/brightdata-scraper.ts, with a …
- Very specific: line 102
Add an entry to PLATFORMS in lib/server/brightdata-platforms.ts, and a label in ENGINE_LABELS in lib/server/visibility.ts (the MCP server …
- Very specific: line 103
Update lib/demo-data.ts, components/dashboard/tabs/visibility-analytics-tab.tsx and components/dashboard/tabs/reputation-sources-tab.tsx, …
- Very specific: line 104
Add the dataset env var to .env.example.
- Very specific: line 111
Add entries to tabIcons and tabMeta in components/sovereign-dashboard.tsx.
- Very specific: line 112
Create the component in components/dashboard/tabs/ and add a branch in renderActiveTab in the same shell file.
- Very specific: line 120
Never run paid calls without the user's keys and explicit consent.
≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
AGENTS.md
GEO/AEO Tracker measures how often AI answer engines (ChatGPT, Perplexity, Gemini, Google AI Mode) mention and cite a brand. It also serves a remote MCP server at /api/mcp and ships a CLI on npm (geo-aeo-tracker, source in cli/). It is a Next.js 16 App Router app on React 19 and TypeScript, styled with Tailwind 4, with zod for request validation and Recharts for charts. State lives in the browser (idb-keyval over IndexedDB) by default, with optional Supabase sync. MIT licensed. The package name is llm-tracker.
Setup and commands
npm install
cp .env.example .env # every variable is optional
npm run dev # http://localhost:3000| Command | What it does | Needs keys / costs money |
npm run dev | Next dev server (Turbopack) | No. An empty env shows the demo dataset |
npm run build | Production build | No. NEXT_PUBLIC_* vars are inlined here |
npm run start | Serve the production build | No |
npm test | tsx --test "lib/**/*.test.ts" "cli/**/*.test.ts" "tests/**/*.test.mjs" (Node test runner, about 190 tests) | No, offline |
npm run build:cli | Bundles cli/src with esbuild into cli/dist/cli.js (one file, no runtime deps) | No |
npm run lint | ESLint (eslint.config.mjs, next core-web-vitals + typescript) | No. Currently clean, 0 warnings |
npm run test:pillar -- 8 | Checks that the files for a pillar number (1 to 8) exist | No, offline |
npm run test:scraper | One live Bright Data AI-scraper call | Yes, bills Bright Data |
Free type check: npx tsc --noEmit (passes clean).
Tests go in lib/**/*.test.ts or cli/**/*.test.ts (next to the code) or tests/**/*.test.mjs and use node:test through tsx. Also run npx tsc --noEmit, npm run lint and npm run build.
Environment variables
Source of truth: .env.example. All are optional. The app builds and boots with an empty .env.
| Variable | Used by | Without it |
BRIGHT_DATA_KEY | all scraping routes, app/page.tsx | Home page switches to demo data. Scrape routes return errors. Bills Bright Data |
OPENROUTER_KEY | app/api/analyze/route.ts, app/api/site-context/route.ts, lib/server/openrouter-sro.ts | Analysis panels show "not configured". Scraping still works. Bills OpenRouter |
OPENROUTER_MODEL | same files | Defaults to google/gemini-3.5-flash |
GEMINI_API_KEY | lib/server/gemini-grounding.ts | Only the SRO grounding stage is skipped. Bills Google |
BRIGHT_DATA_SERP_ZONE | lib/server/serp.ts | Defaults to serp_api1 |
BRIGHT_DATA_UNLOCKER_ZONE | lib/server/unlocker.ts | Defaults to web_unlocker1 |
BRIGHT_DATA_DATASET_CHATGPT, _PERPLEXITY, _GEMINI, _GOOGLE_AI | lib/server/brightdata-scraper.ts, lib/server/brightdata-platforms.ts | Public default dataset IDs ship in code. Override only for private datasets |
MCP_TOKEN | app/api/mcp/route.ts, lib/server/mcp-auth.ts | /api/mcp answers 503. Clients send it as Authorization: Bearer <token>. MCP tools bill Bright Data |
MCP_ENGINE_TIMEOUT_MS | lib/server/mcp.ts | Per-engine deadline for check_visibility, default 240000, clamped to 10s-900s |
SUPABASE_URL or NEXT_PUBLIC_SUPABASE_URL | lib/server/supabase.ts | Cloud sync off |
SUPABASE_SERVICE_ROLE_KEY | lib/server/supabase.ts | Cloud sync off. Server only, never expose |
NEXT_PUBLIC_CLOUD_STORAGE_ENABLED | lib/client/cloud-mode.ts | Client hides the sync toggle. Inlined at build |
STATE_SYNC_SECRET | app/api/state/route.ts | The route answers 503 |
NEXT_PUBLIC_DEMO_ONLY | app/page.tsx | true forces the read-only demo. Inlined at build |
Never commit .env. .gitignore excludes .env* except .env.example.
Architecture
app/page.tsx: home. Picks demo or live mode on the server and rendersSovereignDashboard.app/demo/page.tsx: always-demo route.components/sovereign-dashboard.tsx: the client shell. Holds app state, tab switching and run orchestration (a very large file).components/dashboard/types.ts: shared types,ALL_PROVIDERS,PROVIDER_LABELS,COUNTRIES, thetabslist.components/dashboard/tabs/: one file per dashboard tab.lib/server/: server-only code. Bright Data scrapers, SERP, Unlocker, OpenRouter SRO, Gemini grounding,ssrf.ts,http.ts, Supabase KV.lib/client/:sovereign-store.ts(IndexedDB persistence with cloud fallback) andcloud-mode.ts.lib/demo-data.ts: the sample dataset used in demo mode.supabase/migrations/001_kv_store.sql: the singlekv_storetable for cloud sync.scripts/: the two plain-Node scripts behindtest:scraperandtest:pillar.proxy.ts: IP rate limiting on/api/*, plus markdown negotiation and Link headers on/and/demofor demo deploys. On demo deploys (NEXT_PUBLIC_DEMO_ONLY=true) every/api/*request gets a 403 before any route runs.lib/shared/: pure code used by client and server (rank-signal-stats.ts,strip-markdown.ts).lib/server/rank-signal*.ts: the Rank Signal pipeline behindapp/api/rank-signal/route.ts.lib/agent-discovery.ts: single source forrobots.txt,sitemap.xml,llms.txt,llms-full.txtand/index.md. Served only whenNEXT_PUBLIC_DEMO_ONLY=true; private deploys getDisallow: /and 404s. Edit the copy here, never in the route files.
API routes
All routes are unauthenticated except state. Limits are requests per IP per path per minute from proxy.ts. Paths not listed there get 30. The client IP is x-vercel-forwarded-for, then x-real-ip, then the last x-forwarded-for hop. On demo deploys none of these routes answer (403).
| File | Method | Purpose | Limit | Bills |
app/api/scrape/route.ts | POST | One AI engine query via a Bright Data dataset | 20 | Bright Data |
app/api/brightdata-platforms/route.ts | POST | Query all engines, or one platform, for a keyword | 12 | Bright Data |
app/api/bulk-sro/route.ts | POST | Full SRO pipeline, streamed as server-sent events | 4 | Every vendor |
app/api/sro-analyze/route.ts | POST | SRO scoring from prior stage output | 20 | OpenRouter |
app/api/analyze/route.ts | POST | Generic LLM prompt (scoring, battlecards), maxDuration 120. A client model is ignored unless it equals OPENROUTER_MODEL or the code default. Prompt capped at 20,000 chars, maxTokens at 2000 | 30 | OpenRouter |
app/api/site-context/route.ts | POST | Scrape a site and summarize its context | 30 | Bright Data, OpenRouter |
app/api/serp/route.ts | POST | Google SERP for a keyword | 30 | Bright Data |
app/api/unlocker/route.ts | POST | Fetch one URL, or up to 5, through Web Unlocker. Every URL passes assertPublicHttpUrl | 30 | Bright Data |
app/api/rank-signal/route.ts | GET, POST | Rank Signal paired A/B trials for one signal, streamed as server-sent events. GET lists signals and allowed judge models. maxDuration 300 | 6 | OpenRouter, Bright Data |
app/api/audit/route.ts | POST | AEO page audit, fetches the page directly. Logic in lib/server/audit.ts (runAudit) | 30 | Nothing |
app/api/mcp/route.ts | POST | Remote MCP server, stateless Streamable HTTP. Needs MCP_TOKEN as a Bearer token. Tools in lib/server/mcp.ts: list_engines, check_visibility, google_serp, audit_page (runAudit from lib/server/audit.ts). GET and DELETE answer 405. maxDuration 300 | 30 | Bright Data |
app/api/state/route.ts | GET, PUT, DELETE | Cloud KV sync, needs the x-sync-secret header | 30 | Supabase |
Conventions
- Next 16 uses
proxy.ts, notmiddleware.ts. Export a function namedproxyand aconfig.matcher. Do not createmiddleware.ts. - Demo mode.
app/page.tsxsets demo whenNEXT_PUBLIC_DEMO_ONLYistrueorBRIGHT_DATA_KEYis empty. Incomponents/sovereign-dashboard.tsx,demoModeskips workspace load and save. Changing the flag needs a rebuild. - Local-first. No server database by default. Read and write app state through
loadSovereignValueandsaveSovereignValueinlib/client/sovereign-store.ts. Do not call/api/statedirectly from tabs. - Validation. Every route defines a zod schema at the top and parses the body before doing work. Follow that shape. Cap array sizes and string lengths for anything that reaches an LLM prompt (see
app/api/sro-analyze/route.ts). - Outbound calls. Use
fetchWithTimeoutorwithTimeoutfromlib/server/http.ts. Every upstream call needs a deadline. - User-supplied URLs fetched server-side must pass
assertPublicHttpUrlfromlib/server/ssrf.ts. - Path alias.
@/maps to the repo root (tsconfig.json). - Server and client split. Anything reading a secret stays in
lib/server/or a route. Client files start with"use client". - Rate limits. A new route gets the default 30 per minute. Add an entry to
LIMITSinproxy.tsif it calls a paid API.
Add an AI engine
- Extend
Provider,ALL_PROVIDERSandPROVIDER_LABELSincomponents/dashboard/types.ts. - Add it to the
z.enuminapp/api/scrape/route.ts, and toProviderSchemaandproviderToDatasetEnvinlib/server/brightdata-scraper.ts, with a default dataset ID. - Add an entry to
PLATFORMSinlib/server/brightdata-platforms.ts, and a label inENGINE_LABELSinlib/server/visibility.ts(the MCP server and the CLI read the engine list fromProviderSchema). - Update
lib/demo-data.ts,components/dashboard/tabs/visibility-analytics-tab.tsxandcomponents/dashboard/tabs/reputation-sources-tab.tsx, which key on engine names. - Add the dataset env var to
.env.example.
Check https://docs.brightdata.com/scrapers.json first: an engine needs a live dataset there. Grok was removed in v1.5.0 because its Bright Data scraper never completes, and Copilot in v1.7.0 because Bright Data retired its scraper (CHANGELOG.md). Provider in components/dashboard/types.ts still lists copilot so runs saved before v1.7.0 render; it is not in ALL_PROVIDERS. Known outages go in ENGINE_NOTES in lib/server/visibility.ts.
Add a tab
- Append the name to
tabsincomponents/dashboard/types.ts. - Add entries to
tabIconsandtabMetaincomponents/sovereign-dashboard.tsx. - Create the component in
components/dashboard/tabs/and add a branch inrenderActiveTabin the same shell file.
Testing guidance
- Free and offline:
npm test,npx tsc --noEmit,npm run lint,npm run build,npm run test:pillar -- <1-8>. - MCP:
lib/server/mcp.test.tsdrives every tool through the real MCP client over an in-memory transport with fake Bright Data calls (free). For a live check,npm run build, start withMCP_TOKENset, and connect any MCP client tohttp://localhost:3000/api/mcp. - CLI: commands, flags, help text and
help --jsonall come fromcli/src/spec.ts; add a flag there and toParsedValuesincli/src/run.ts. The package also shipscli/llms.txtandcli/skill/SKILL.md; keep them in step with the spec.cli/src/run.test.tsruns every command with fake deps (free). For a release check,npm run build:cli, thennpm packincli/, install the tarball in an empty folder and runnpx geo-aeo-tracker audit https://example.com(free). Publish withnpm publishfromcli/after bumpingcli/package.json. - Paid:
npm run test:scraperwith keys, and any request to/api/scrape,/api/brightdata-platforms,/api/bulk-sro,/api/serp,/api/unlocker,/api/site-context,/api/analyzeor/api/sro-analyze. - Never run paid calls without the user's keys and explicit consent.
bulk-srofans out to every vendor in one request. - UI changes need a look in a real browser at
npm run dev. Use/demofor a no-key check.
Gotchas
scripts/test-scraper.jsonly goes live when aBRIGHT_DATA_DATASET_*var is set. Dataset IDs now have defaults in code, so with onlyBRIGHT_DATA_KEYset it prints a mock result.npm run test:scraperusesnode --env-file=.env, so it exits with "node: .env: not found" when no.envexists.NEXT_PUBLIC_DEMO_ONLYchanges whatapp/page.tsxrenders, andproxy.tsanswers 403 to every/api/*request on such a deploy, so keys on a demo deploy are never used. The flag is inlined at build, so changing it needs a rebuild.proxy.tskeeps its counters in process memory. Off Vercel a client can still send its ownx-real-ipwhen no proxy overwrites it, so the limit is best-effort. On serverless each instance counts alone, so the limit stops bursts and nothing more.scripts/*.jsis excluded from ESLint ineslint.config.mjs.lib/server/brightdata-scraper.tswaits 90 seconds and follows asnapshot_idhandoff. Keep that timeout, since Gemini needs it. Its follow-up poll can run for about 10 minutes on a stuck snapshot, which is whycheck_visibilitywraps each engine in its own deadline.- Google now wraps organic result links in
google.com/gotoredirects.resultDomaininlib/server/serp.tsreads the real site fromdisplay_link; Reddit threads show none, so theirdomainis empty. After a failed query Bright Data answers 200 with a text body asking to wait 15 seconds, sofetchSerpretries once after 16 seconds. app/api/bulk-sro/route.tsstreamstext/event-stream. Keep the response shape when editing it.- The cloud sync secret is entered in the UI and stored per browser (
lib/client/cloud-mode.ts). It never ships in the bundle. vercel.jsonholds only a schema line. Vercel needs no extra config.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

