Vibecode AmICited
track this build4 phases, 12 steps, beginner friendly0%The scoring loop is genuinely weekend-buildable, but a faithful replacement is not, and that gap is the honest reason to keep paying. The naive personal build sends your questions through the model APIs, but API answers are not what a real user sees when they open ChatGPT, Perplexity, or AI Overviews, so that number is only a proxy. AmICited does not use LLM APIs at all: it drives real browsers to ask the actual consumer surfaces the way a person in a given country would, a browser-automation fleet routed through country-level proxies, kept working as every surface changes. The other gaps are the historical archive that compounds from day one and cannot be backfilled, an MCP server that lets your AI agents act on your visibility gaps, and, on higher plans, human AEO consulting from real experience that no script replaces. You can build the weekend proxy; the prompt below is that honest consolation build, with its limits stated plainly.
You are building a lean indie version of AmICited. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # AmICited · indie build A local answer-engine visibility tracker whose hard problems are staying honest about being a proxy rather than a measurement, keeping a run's cost bounded and predictable, and not reading a trend into noise from fifty non-deterministic answers. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22 with TypeScript, a CLI plus a server-rendered dashboard | no accounts, no telemetry, runs from cron | | Storage | SQLite via better-sqlite3, runs written once and never updated | the archive is the product and an overwrite is data loss | | Providers | One module per provider behind one ask function, search or grounding enabled | adding a provider should not touch the scheduler | | Cost | A ledger row per call and a hard ceiling checked before each request | a nightly job with retries is how a bill surprises you | | Matching | Word-boundary alias matching, never substring | a brand called Arc must not match the word March | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the CLI, the scheduler and the dashboard. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **API keys for the providers you enable** · pay per use, and the search component is billed per search on several providers - Why: Each provider is a separate account with its own grounded-search billing, and enabling four of them means four bills. - Get it: Create an account with each provider and generate an API key with the smallest scope offered. - Verify: A single one-prompt run completes against each enabled provider - [ ] **Each provider's current pricing page** · free - Why: The cost ceiling is only meaningful if the numbers behind it are today's, and every one of these providers has changed pricing. - Get it: Open each provider's pricing page, record the token rates and the grounded-search rate with the date. - Verify: Every enabled provider has a price and a checked date in the configuration - [ ] **A machine that stays awake** · a few pounds a month - Why: The value here is an archive that compounds daily, and a laptop that sleeps produces a chart with holes in it. - Get it: A small VPS, a home server or anything that can run a nightly job. - Verify: A scheduled run completes overnight without you present - [ ] **Your buyer questions and competitor list** · free - Why: The questions are the instrument, and changing them mid-way makes every earlier figure incomparable, so they deserve an hour before the first run. - Get it: Write the questions a buyer would actually type, and list the competitors by every name and spelling they use. - Verify: Both files exist, the prompt count is under the cap, and every alias is listed ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Any automation of consumer chat or search interfaces. - Country-level results through proxies. - An MCP server for agents. - Human advisory work. - the faithful signal: API answers only approximate what the ChatGPT app, AI Overviews, AI Mode, Copilot, and Grok actually serve real users, which is what AmICited measures with real browsers - country-level results: what users see varies by geography, which needs a proxy fleet a solo build will not stand up - scale and reliability: running and retrying thousands of real browser sessions in parallel is the hard engineering, not the scoring - months of stored answers and competitor baselines, without which a single week's visibility number is noise, and which you cannot backfill once you start late - human AEO consultations on higher plans, advice from real experience acting on your data, which no self-hosted script reproduces If one of those is essential to you, that is the reason to keep paying for AmICited, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · AmICited The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local tracker that asks answer-engine APIs your buyer questions and records, honestly, that this is a proxy to replace AmICited. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Three rules, and the first one is the whole honesty of this build. What you are measuring is not what your buyers see. This tool sends questions through model APIs with their search or grounding tools enabled. The consumer products people actually open are different surfaces with different retrieval, different personalisation and different geography, and the paid tools in this category exist because they drive real browsers through country-level proxies to capture that. So every screen, every export and the README say so in plain words, and no number in this app is ever labelled as what users see. Second, do not automate the consumer web interfaces to close that gap. Scripting a logged-in chat product to scrape answers breaks the terms you agreed to and gets the account closed. Use the documented APIs or accept the limitation. Third, a run is immutable. Answers are the asset and they compound; a row that gets overwritten by a later run has destroyed the only copy of what an engine said that day, and you cannot go back and ask again. ### Stack (fixed, do not substitute) - Runtime: Node 22 with TypeScript, a CLI plus a server-rendered dashboard · no accounts, no telemetry, runs from cron - Storage: SQLite via better-sqlite3, runs written once and never updated · the archive is the product and an overwrite is data loss - Providers: One module per provider behind one ask function, search or grounding enabled · adding a provider should not touch the scheduler - Cost: A ledger row per call and a hard ceiling checked before each request · a nightly job with retries is how a bill surprises you - Matching: Word-boundary alias matching, never substring · a brand called Arc must not match the word March ### Data model (create this before Phase 1) - brands(id, name, aliases_json, domain, is_competitor) · your brand and the competitors, same table - prompts(id, text, intent, active, added_at) · the buyer questions, capped and versioned by addition - runs(id, started_at, finished_at, provider_set_json, prompt_count, status, cost_estimate, cost_actual) - answers(run_id, prompt_id, provider, model_id, raw_text, finish_reason, latency_ms, error_text, tokens_in, tokens_out, searches, asked_at) · written once, never updated - citations(answer_id, ordinal, raw_url, host, canonical_path, is_owned) · normalised, tracking parameters stripped - mentions(answer_id, brand_id, count, first_offset) · offset as a crude prominence proxy, labelled as crude - cost_ledger(run_id, provider, kind, units, unit_note, estimated_at) · kind is tokens or searches, because they are billed separately - Nothing in this schema has an update path. Corrections are new rows, so the history stays true to what was returned on the day. ### Phase 1 · Inputs, and a run that cannot be overwritten Build: Read brand.json and prompts.json from disk. Cap the prompt list at a stated number. Create a run row, then one answer row per prompt per provider, inserted once. Make the answers table reject updates at the database level rather than by convention. Trap: Treating a re-run as a refresh and updating yesterday's row for the same prompt and provider destroys the archive that makes this tool worth anything after week three, and you will not notice until you want a chart. Insert only, and let the dashboard aggregate across runs. Done when: An attempt to update an existing answer row fails at the database level, tested. Two runs on the same day produce two complete sets of answers and neither modifies the other. Editing prompts.json between runs is recorded so a later chart can tell you the question set changed. A run interrupted halfway leaves the answers it completed and is marked incomplete rather than deleted. Do not build yet: No provider calls, scoring, dashboard or export yet. ### Phase 2 · Asking, with a bill you predicted Build: One module per provider, each enabling that provider's search or grounding tool. Cap concurrency per provider. Retry rate limits and server errors twice with backoff, and keep failed cells visible. Before the first call, print an estimate: grounded search is billed separately from tokens, and Anthropic publishes its web search tool at 10 US dollars per 1,000 searches while its web fetch tool adds no charge beyond the tokens the fetched page costs (checked on their pricing page). Look up the equivalent figures for every provider you enable and put them in the configuration, not in the code. Trap: A nightly cron with a retry on every failure quietly multiplies the run, and since grounded search is billed per search on top of tokens, a week of unnoticed retries costs several times what the first run's estimate said. Estimate before the run, record a ledger row per call, and refuse to start a run whose estimate exceeds the ceiling. Done when: A run prints a cost estimate broken into tokens and searches per provider before making a single call, and refuses to start above the configured ceiling. Per-provider concurrency is respected, verified with a fake provider that counts concurrent calls. A provider returning rate limit errors throughout leaves visible failed cells rather than an empty report or a silent skip. The ledger's actual figures are recorded per call and the run's total is shown beside its estimate. Do not build yet: No mention detection, scoring, dashboard or export yet. ### Phase 3 · Finding yourself without finding the wrong word Build: Match brand and competitor names case-insensitively on word boundaries using the alias list, never as a substring. Record a count and the first-mention character offset, and label the offset in the interface as a crude proxy for prominence. Normalise every citation to host plus canonical path with tracking parameters stripped, and mark which hosts you own. Trap: Substring matching a short brand name finds it inside ordinary words and inside competitor names, so a brand called Arc scores a mention every time an answer says March or Arcadia, and the resulting chart is confidently wrong in your favour. Match on word boundaries with an explicit alias list, and keep a fixture test of words that must not match. Done when: A fixture of near-miss strings for a short brand name produces zero matches, and the real name in ordinary sentence positions produces matches, both tested. Citations differing only by tracking parameters or a trailing slash normalise to the same row, tested. Owned-domain citation share is computed from the normalised hosts and matches a hand-counted fixture. The first-mention offset is stored and is labelled as a crude proxy everywhere it appears. Do not build yet: No dashboard, trends or export yet. ### Phase 4 · A dashboard that refuses to draw a trend from noise Build: Render visibility per provider over time, share of voice against each competitor, the cited sources table, and the gap list: prompts where a competitor is cited and you are not, which is the only genuinely actionable view here. Show the number of runs behind every figure and refuse to draw a trend line below a configured minimum number of runs. Trap: Plotting a visibility percentage from a single day's fifty prompts produces a line that moves every day for reasons that are entirely model non-determinism, and somebody makes a content decision from it. Show the run count beside every figure, suppress the trend below the minimum, and state the sample size on every chart. Done when: A chart with fewer than the configured minimum number of runs displays the points and an explicit note instead of a trend line. Every figure shows the number of answers behind it. The gap list is ordered by how often a competitor is cited where you are absent, and clicking an entry shows the full raw answer. A provider with failed cells in a run is shown as failed on the chart rather than as zero. Do not build yet: No export or cost view yet. ### Phase 5 · Export, cost and saying what this is not Build: Export runs, answers, mentions and citations as CSV and as JSON, with the raw answer text included. Show the cost ledger by run and by month against the ceiling. Put a permanent, unsuppressible line on the dashboard and at the top of every export stating that these are API answers with grounding enabled, not what a person sees in the consumer products, and that country-specific results are not measured here. Trap: A tidy dashboard with a caveat buried in the README gets screenshotted into a slide deck, and by the second meeting the number is being treated as what customers see. Put the limitation in the export header and on the chart itself, where it travels with the number. Done when: Every export file carries the limitation statement in its header or first record, verified in the test. The dashboard's limitation line cannot be dismissed or hidden by configuration. The cost view shows spend per run and per month against the ceiling, and the ceiling being hit pauses scheduled runs rather than skipping silently. Raw answer text is included in the JSON export so the archive is readable without this application. Do not build yet: Nothing after this. ### Out of scope (and why) - Driving the consumer chat and search products with a browser, which breaks their terms, gets accounts closed, and is the specific thing the paid products in this category do at scale with infrastructure you are not building this weekend. - Country-level results through a proxy fleet, which needs residential addresses and constant upkeep as each surface changes. - An MCP server exposing your gaps to other agents, which is a separate product on top of this data rather than part of it. - Any human advisory layer, which is the part of these subscriptions that is genuinely a person and cannot be scripted. ### README must contain - That this measures API answers with grounding enabled, which are a proxy and not what people see in the consumer products - That the consumer interfaces are deliberately not automated, and why that is a rule rather than a gap - That answers are insert-only, so the archive compounds and a late start cannot be backfilled - How the cost estimate is computed, that grounded search is billed separately from tokens, and where each price came from - That trends are suppressed below a minimum run count because fifty non-deterministic answers move on their own ===== AGENTS.md ===== # Agent instructions · AmICited indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22 with TypeScript, a CLI plus a server-rendered dashboard, SQLite via better-sqlite3, runs written once and never updated, One module per provider behind one ask function, search or grounding enabled, A ledger row per call and a hard ceiling checked before each request, Word-boundary alias matching, never substring. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Treating a re-run as a refresh and updating yesterday's row for the same prompt and provider destroys the archive that makes this tool worth anything after week three, and you will not notice until you want a chart. Insert only, and let the dashboard aggregate across runs. - A nightly cron with a retry on every failure quietly multiplies the run, and since grounded search is billed per search on top of tokens, a week of unnoticed retries costs several times what the first run's estimate said. Estimate before the run, record a ledger row per call, and refuse to start a run whose estimate exceeds the ceiling. - Substring matching a short brand name finds it inside ordinary words and inside competitor names, so a brand called Arc scores a mention every time an answer says March or Arcadia, and the resulting chart is confidently wrong in your favour. Match on word boundaries with an explicit alias list, and keep a fixture test of words that must not match. - Plotting a visibility percentage from a single day's fifty prompts produces a line that moves every day for reasons that are entirely model non-determinism, and somebody makes a content decision from it. Show the run count beside every figure, suppress the trend below the minimum, and state the sample size on every chart. ===== BUILD_PLAN.md ===== # Build plan · AmICited A local answer-engine visibility tracker whose hard problems are staying honest about being a proxy rather than a measurement, keeping a run's cost bounded and predictable, and not reading a trend into noise from fifty non-deterministic answers. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Inputs, and a run that cannot be overwritten Brand, competitors and questions are files you edit, and each run appends an immutable set of answers. ### Steps 1. Make answers insert-only in the schema A trigger or a constraint, not a code review habit. Files: `src/db/schema.sql`, `tests/immutable-answers.test.ts` 2. Record the prompt set per run So a shift in the numbers can be traced to a shift in the questions. Files: `src/runs.ts` 3. Mark an interrupted run incomplete Partial data that says it is partial is useful; partial data that lies is not. Files: `src/runs.ts` ### Done when - [ ] Updating an answer row fails at the database level - [ ] Two runs in a day coexist intact - [ ] A prompt set change is recorded per run - [ ] An interrupted run is marked incomplete ### Watch out - Treating a re-run as a refresh and updating yesterday's row for the same prompt and provider destroys the archive that makes this tool worth anything after week three, and you will not notice until you want a chart. Insert only, and let the dashboard aggregate across runs. ## Phase 2 · Asking, with a bill you predicted Every provider is called with grounding on, concurrency bounded, and the cost estimated before the run starts. ### Steps 1. Put every price in configuration Because prices change and a number in the code is a number nobody rechecks. Files: `src/config/pricing.ts`, `docs/pricing-sources.md` 2. Estimate before the first call The estimate is the only thing between a cron job and a surprise. Files: `src/cost.ts` 3. Keep failed cells visible A missing provider that looks like zero visibility is a wrong conclusion. Files: `src/providers/index.ts` ### Done when - [ ] An estimate prints before any call and blocks above the ceiling - [ ] Per-provider concurrency is respected under test - [ ] Rate-limited providers leave visible failed cells - [ ] The ledger records actual tokens and searches per call ### Watch out - A nightly cron with a retry on every failure quietly multiplies the run, and since grounded search is billed per search on top of tokens, a week of unnoticed retries costs several times what the first run's estimate said. Estimate before the run, record a ledger row per call, and refuse to start a run whose estimate exceeds the ceiling. ## Phase 3 · Finding yourself without finding the wrong word Brand and competitor mentions and cited sources are extracted in a way you would defend. ### Steps 1. Word boundaries and an alias list The difference between a chart and a flattering error. Files: `src/match.ts`, `tests/near-miss.test.ts` 2. Normalise citation URLs Same page, same row, whatever the campaign parameters say. Files: `src/citations.ts`, `tests/url-normalise.test.ts` 3. Label the prominence proxy as a proxy In the interface, not only in a comment. Files: `src/views/answer.tsx` ### Done when - [ ] Near-miss strings produce zero matches - [ ] Citations differing only by parameters collapse to one row - [ ] Owned-domain share matches a hand-counted fixture - [ ] The offset is labelled as crude wherever shown ### Watch out - Substring matching a short brand name finds it inside ordinary words and inside competitor names, so a brand called Arc scores a mention every time an answer says March or Arcadia, and the resulting chart is confidently wrong in your favour. Match on word boundaries with an explicit alias list, and keep a fixture test of words that must not match. ## Phase 4 · A dashboard that refuses to draw a trend from noise The report shows share of voice, the gap list and how much of it is signal. ### Steps 1. Suppress trends below the minimum run count The honest version of a sparse chart. Files: `src/views/trend.tsx` 2. Build the gap list first It is the only view here that tells you what to do next. Files: `src/views/gaps.tsx` 3. Distinguish failed from zero They look the same on a chart and mean opposite things. Files: `src/views/provider.tsx` ### Done when - [ ] Sparse charts show points and a note, not a trend - [ ] Every figure states its sample size - [ ] The gap list links to full raw answers - [ ] Failed cells are never rendered as zero ### Watch out - Plotting a visibility percentage from a single day's fifty prompts produces a line that moves every day for reasons that are entirely model non-determinism, and somebody makes a content decision from it. Show the run count beside every figure, suppress the trend below the minimum, and state the sample size on every chart. ## Not in this build - Any automation of consumer chat or search interfaces. - Country-level results through proxies. - An MCP server for agents. - Human advisory work. ## After v1, if you want it - A fifth provider added as a module, once its pricing is in the configuration with a date - Weekly digests of new gap-list entries, which need nothing beyond a query and a mail call - Storing answer embeddings so near-identical answers across days can be collapsed in the archive view ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Runs, answers, citations and the cost ledger. This is the archive. DATABASE_PATH=./data/visibility.db # Required. Your brand, its aliases, its domain and the competitor list. BRAND_FILE=./brand.json # Required. The buyer questions. Changing them is recorded against the run. PROMPTS_FILE=./prompts.json # Required. A cap, because cost scales with prompts times providers. MAX_PROMPTS=50 # Required. Per provider. Higher is not faster once you are rate limited. PROVIDER_CONCURRENCY=3 # Required. Reaching it pauses scheduled runs and says so, rather than skipping. MONTHLY_COST_CEILING_USD=40 # Required. Below this the charts show points and a note instead of a trend line. MIN_RUNS_FOR_TREND=14 # Optional · secret. One provider's key. Every provider key is optional and the run skips providers with no key, visibly. ANTHROPIC_API_KEY=sk-ant-...redacted
You are building a lean indie version of AmICited. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # AmICited · indie build A local answer-engine visibility tracker whose hard problems are staying honest about being a proxy rather than a measurement, keeping a run's cost bounded and predictable, and not reading a trend into noise from fifty non-deterministic answers. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22 with TypeScript, a CLI plus a server-rendered dashboard | no accounts, no telemetry, runs from cron | | Storage | SQLite via better-sqlite3, runs written once and never updated | the archive is the product and an overwrite is data loss | | Providers | One module per provider behind one ask function, search or grounding enabled | adding a provider should not touch the scheduler | | Cost | A ledger row per call and a hard ceiling checked before each request | a nightly job with retries is how a bill surprises you | | Matching | Word-boundary alias matching, never substring | a brand called Arc must not match the word March | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the CLI, the scheduler and the dashboard. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **API keys for the providers you enable** · pay per use, and the search component is billed per search on several providers - Why: Each provider is a separate account with its own grounded-search billing, and enabling four of them means four bills. - Get it: Create an account with each provider and generate an API key with the smallest scope offered. - Verify: A single one-prompt run completes against each enabled provider - [ ] **Each provider's current pricing page** · free - Why: The cost ceiling is only meaningful if the numbers behind it are today's, and every one of these providers has changed pricing. - Get it: Open each provider's pricing page, record the token rates and the grounded-search rate with the date. - Verify: Every enabled provider has a price and a checked date in the configuration - [ ] **A machine that stays awake** · a few pounds a month - Why: The value here is an archive that compounds daily, and a laptop that sleeps produces a chart with holes in it. - Get it: A small VPS, a home server or anything that can run a nightly job. - Verify: A scheduled run completes overnight without you present - [ ] **Your buyer questions and competitor list** · free - Why: The questions are the instrument, and changing them mid-way makes every earlier figure incomparable, so they deserve an hour before the first run. - Get it: Write the questions a buyer would actually type, and list the competitors by every name and spelling they use. - Verify: Both files exist, the prompt count is under the cap, and every alias is listed ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Any automation of consumer chat or search interfaces. - Country-level results through proxies. - An MCP server for agents. - Human advisory work. - the faithful signal: API answers only approximate what the ChatGPT app, AI Overviews, AI Mode, Copilot, and Grok actually serve real users, which is what AmICited measures with real browsers - country-level results: what users see varies by geography, which needs a proxy fleet a solo build will not stand up - scale and reliability: running and retrying thousands of real browser sessions in parallel is the hard engineering, not the scoring - months of stored answers and competitor baselines, without which a single week's visibility number is noise, and which you cannot backfill once you start late - human AEO consultations on higher plans, advice from real experience acting on your data, which no self-hosted script reproduces If one of those is essential to you, that is the reason to keep paying for AmICited, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · AmICited The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local tracker that asks answer-engine APIs your buyer questions and records, honestly, that this is a proxy to replace AmICited. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Three rules, and the first one is the whole honesty of this build. What you are measuring is not what your buyers see. This tool sends questions through model APIs with their search or grounding tools enabled. The consumer products people actually open are different surfaces with different retrieval, different personalisation and different geography, and the paid tools in this category exist because they drive real browsers through country-level proxies to capture that. So every screen, every export and the README say so in plain words, and no number in this app is ever labelled as what users see. Second, do not automate the consumer web interfaces to close that gap. Scripting a logged-in chat product to scrape answers breaks the terms you agreed to and gets the account closed. Use the documented APIs or accept the limitation. Third, a run is immutable. Answers are the asset and they compound; a row that gets overwritten by a later run has destroyed the only copy of what an engine said that day, and you cannot go back and ask again. ### Stack (fixed, do not substitute) - Runtime: Node 22 with TypeScript, a CLI plus a server-rendered dashboard · no accounts, no telemetry, runs from cron - Storage: SQLite via better-sqlite3, runs written once and never updated · the archive is the product and an overwrite is data loss - Providers: One module per provider behind one ask function, search or grounding enabled · adding a provider should not touch the scheduler - Cost: A ledger row per call and a hard ceiling checked before each request · a nightly job with retries is how a bill surprises you - Matching: Word-boundary alias matching, never substring · a brand called Arc must not match the word March ### Data model (create this before Phase 1) - brands(id, name, aliases_json, domain, is_competitor) · your brand and the competitors, same table - prompts(id, text, intent, active, added_at) · the buyer questions, capped and versioned by addition - runs(id, started_at, finished_at, provider_set_json, prompt_count, status, cost_estimate, cost_actual) - answers(run_id, prompt_id, provider, model_id, raw_text, finish_reason, latency_ms, error_text, tokens_in, tokens_out, searches, asked_at) · written once, never updated - citations(answer_id, ordinal, raw_url, host, canonical_path, is_owned) · normalised, tracking parameters stripped - mentions(answer_id, brand_id, count, first_offset) · offset as a crude prominence proxy, labelled as crude - cost_ledger(run_id, provider, kind, units, unit_note, estimated_at) · kind is tokens or searches, because they are billed separately - Nothing in this schema has an update path. Corrections are new rows, so the history stays true to what was returned on the day. ### Phase 1 · Inputs, and a run that cannot be overwritten Build: Read brand.json and prompts.json from disk. Cap the prompt list at a stated number. Create a run row, then one answer row per prompt per provider, inserted once. Make the answers table reject updates at the database level rather than by convention. Trap: Treating a re-run as a refresh and updating yesterday's row for the same prompt and provider destroys the archive that makes this tool worth anything after week three, and you will not notice until you want a chart. Insert only, and let the dashboard aggregate across runs. Done when: An attempt to update an existing answer row fails at the database level, tested. Two runs on the same day produce two complete sets of answers and neither modifies the other. Editing prompts.json between runs is recorded so a later chart can tell you the question set changed. A run interrupted halfway leaves the answers it completed and is marked incomplete rather than deleted. Do not build yet: No provider calls, scoring, dashboard or export yet. ### Phase 2 · Asking, with a bill you predicted Build: One module per provider, each enabling that provider's search or grounding tool. Cap concurrency per provider. Retry rate limits and server errors twice with backoff, and keep failed cells visible. Before the first call, print an estimate: grounded search is billed separately from tokens, and Anthropic publishes its web search tool at 10 US dollars per 1,000 searches while its web fetch tool adds no charge beyond the tokens the fetched page costs (checked on their pricing page). Look up the equivalent figures for every provider you enable and put them in the configuration, not in the code. Trap: A nightly cron with a retry on every failure quietly multiplies the run, and since grounded search is billed per search on top of tokens, a week of unnoticed retries costs several times what the first run's estimate said. Estimate before the run, record a ledger row per call, and refuse to start a run whose estimate exceeds the ceiling. Done when: A run prints a cost estimate broken into tokens and searches per provider before making a single call, and refuses to start above the configured ceiling. Per-provider concurrency is respected, verified with a fake provider that counts concurrent calls. A provider returning rate limit errors throughout leaves visible failed cells rather than an empty report or a silent skip. The ledger's actual figures are recorded per call and the run's total is shown beside its estimate. Do not build yet: No mention detection, scoring, dashboard or export yet. ### Phase 3 · Finding yourself without finding the wrong word Build: Match brand and competitor names case-insensitively on word boundaries using the alias list, never as a substring. Record a count and the first-mention character offset, and label the offset in the interface as a crude proxy for prominence. Normalise every citation to host plus canonical path with tracking parameters stripped, and mark which hosts you own. Trap: Substring matching a short brand name finds it inside ordinary words and inside competitor names, so a brand called Arc scores a mention every time an answer says March or Arcadia, and the resulting chart is confidently wrong in your favour. Match on word boundaries with an explicit alias list, and keep a fixture test of words that must not match. Done when: A fixture of near-miss strings for a short brand name produces zero matches, and the real name in ordinary sentence positions produces matches, both tested. Citations differing only by tracking parameters or a trailing slash normalise to the same row, tested. Owned-domain citation share is computed from the normalised hosts and matches a hand-counted fixture. The first-mention offset is stored and is labelled as a crude proxy everywhere it appears. Do not build yet: No dashboard, trends or export yet. ### Phase 4 · A dashboard that refuses to draw a trend from noise Build: Render visibility per provider over time, share of voice against each competitor, the cited sources table, and the gap list: prompts where a competitor is cited and you are not, which is the only genuinely actionable view here. Show the number of runs behind every figure and refuse to draw a trend line below a configured minimum number of runs. Trap: Plotting a visibility percentage from a single day's fifty prompts produces a line that moves every day for reasons that are entirely model non-determinism, and somebody makes a content decision from it. Show the run count beside every figure, suppress the trend below the minimum, and state the sample size on every chart. Done when: A chart with fewer than the configured minimum number of runs displays the points and an explicit note instead of a trend line. Every figure shows the number of answers behind it. The gap list is ordered by how often a competitor is cited where you are absent, and clicking an entry shows the full raw answer. A provider with failed cells in a run is shown as failed on the chart rather than as zero. Do not build yet: No export or cost view yet. ### Phase 5 · Export, cost and saying what this is not Build: Export runs, answers, mentions and citations as CSV and as JSON, with the raw answer text included. Show the cost ledger by run and by month against the ceiling. Put a permanent, unsuppressible line on the dashboard and at the top of every export stating that these are API answers with grounding enabled, not what a person sees in the consumer products, and that country-specific results are not measured here. Trap: A tidy dashboard with a caveat buried in the README gets screenshotted into a slide deck, and by the second meeting the number is being treated as what customers see. Put the limitation in the export header and on the chart itself, where it travels with the number. Done when: Every export file carries the limitation statement in its header or first record, verified in the test. The dashboard's limitation line cannot be dismissed or hidden by configuration. The cost view shows spend per run and per month against the ceiling, and the ceiling being hit pauses scheduled runs rather than skipping silently. Raw answer text is included in the JSON export so the archive is readable without this application. Do not build yet: Nothing after this. ### Out of scope (and why) - Driving the consumer chat and search products with a browser, which breaks their terms, gets accounts closed, and is the specific thing the paid products in this category do at scale with infrastructure you are not building this weekend. - Country-level results through a proxy fleet, which needs residential addresses and constant upkeep as each surface changes. - An MCP server exposing your gaps to other agents, which is a separate product on top of this data rather than part of it. - Any human advisory layer, which is the part of these subscriptions that is genuinely a person and cannot be scripted. ### README must contain - That this measures API answers with grounding enabled, which are a proxy and not what people see in the consumer products - That the consumer interfaces are deliberately not automated, and why that is a rule rather than a gap - That answers are insert-only, so the archive compounds and a late start cannot be backfilled - How the cost estimate is computed, that grounded search is billed separately from tokens, and where each price came from - That trends are suppressed below a minimum run count because fifty non-deterministic answers move on their own ===== AGENTS.md ===== # Agent instructions · AmICited indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22 with TypeScript, a CLI plus a server-rendered dashboard, SQLite via better-sqlite3, runs written once and never updated, One module per provider behind one ask function, search or grounding enabled, A ledger row per call and a hard ceiling checked before each request, Word-boundary alias matching, never substring. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Treating a re-run as a refresh and updating yesterday's row for the same prompt and provider destroys the archive that makes this tool worth anything after week three, and you will not notice until you want a chart. Insert only, and let the dashboard aggregate across runs. - A nightly cron with a retry on every failure quietly multiplies the run, and since grounded search is billed per search on top of tokens, a week of unnoticed retries costs several times what the first run's estimate said. Estimate before the run, record a ledger row per call, and refuse to start a run whose estimate exceeds the ceiling. - Substring matching a short brand name finds it inside ordinary words and inside competitor names, so a brand called Arc scores a mention every time an answer says March or Arcadia, and the resulting chart is confidently wrong in your favour. Match on word boundaries with an explicit alias list, and keep a fixture test of words that must not match. - Plotting a visibility percentage from a single day's fifty prompts produces a line that moves every day for reasons that are entirely model non-determinism, and somebody makes a content decision from it. Show the run count beside every figure, suppress the trend below the minimum, and state the sample size on every chart. ===== BUILD_PLAN.md ===== # Build plan · AmICited A local answer-engine visibility tracker whose hard problems are staying honest about being a proxy rather than a measurement, keeping a run's cost bounded and predictable, and not reading a trend into noise from fifty non-deterministic answers. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Inputs, and a run that cannot be overwritten Brand, competitors and questions are files you edit, and each run appends an immutable set of answers. ### Steps 1. Make answers insert-only in the schema A trigger or a constraint, not a code review habit. Files: `src/db/schema.sql`, `tests/immutable-answers.test.ts` 2. Record the prompt set per run So a shift in the numbers can be traced to a shift in the questions. Files: `src/runs.ts` 3. Mark an interrupted run incomplete Partial data that says it is partial is useful; partial data that lies is not. Files: `src/runs.ts` ### Done when - [ ] Updating an answer row fails at the database level - [ ] Two runs in a day coexist intact - [ ] A prompt set change is recorded per run - [ ] An interrupted run is marked incomplete ### Watch out - Treating a re-run as a refresh and updating yesterday's row for the same prompt and provider destroys the archive that makes this tool worth anything after week three, and you will not notice until you want a chart. Insert only, and let the dashboard aggregate across runs. ## Phase 2 · Asking, with a bill you predicted Every provider is called with grounding on, concurrency bounded, and the cost estimated before the run starts. ### Steps 1. Put every price in configuration Because prices change and a number in the code is a number nobody rechecks. Files: `src/config/pricing.ts`, `docs/pricing-sources.md` 2. Estimate before the first call The estimate is the only thing between a cron job and a surprise. Files: `src/cost.ts` 3. Keep failed cells visible A missing provider that looks like zero visibility is a wrong conclusion. Files: `src/providers/index.ts` ### Done when - [ ] An estimate prints before any call and blocks above the ceiling - [ ] Per-provider concurrency is respected under test - [ ] Rate-limited providers leave visible failed cells - [ ] The ledger records actual tokens and searches per call ### Watch out - A nightly cron with a retry on every failure quietly multiplies the run, and since grounded search is billed per search on top of tokens, a week of unnoticed retries costs several times what the first run's estimate said. Estimate before the run, record a ledger row per call, and refuse to start a run whose estimate exceeds the ceiling. ## Phase 3 · Finding yourself without finding the wrong word Brand and competitor mentions and cited sources are extracted in a way you would defend. ### Steps 1. Word boundaries and an alias list The difference between a chart and a flattering error. Files: `src/match.ts`, `tests/near-miss.test.ts` 2. Normalise citation URLs Same page, same row, whatever the campaign parameters say. Files: `src/citations.ts`, `tests/url-normalise.test.ts` 3. Label the prominence proxy as a proxy In the interface, not only in a comment. Files: `src/views/answer.tsx` ### Done when - [ ] Near-miss strings produce zero matches - [ ] Citations differing only by parameters collapse to one row - [ ] Owned-domain share matches a hand-counted fixture - [ ] The offset is labelled as crude wherever shown ### Watch out - Substring matching a short brand name finds it inside ordinary words and inside competitor names, so a brand called Arc scores a mention every time an answer says March or Arcadia, and the resulting chart is confidently wrong in your favour. Match on word boundaries with an explicit alias list, and keep a fixture test of words that must not match. ## Phase 4 · A dashboard that refuses to draw a trend from noise The report shows share of voice, the gap list and how much of it is signal. ### Steps 1. Suppress trends below the minimum run count The honest version of a sparse chart. Files: `src/views/trend.tsx` 2. Build the gap list first It is the only view here that tells you what to do next. Files: `src/views/gaps.tsx` 3. Distinguish failed from zero They look the same on a chart and mean opposite things. Files: `src/views/provider.tsx` ### Done when - [ ] Sparse charts show points and a note, not a trend - [ ] Every figure states its sample size - [ ] The gap list links to full raw answers - [ ] Failed cells are never rendered as zero ### Watch out - Plotting a visibility percentage from a single day's fifty prompts produces a line that moves every day for reasons that are entirely model non-determinism, and somebody makes a content decision from it. Show the run count beside every figure, suppress the trend below the minimum, and state the sample size on every chart. ## Not in this build - Any automation of consumer chat or search interfaces. - Country-level results through proxies. - An MCP server for agents. - Human advisory work. ## After v1, if you want it - A fifth provider added as a module, once its pricing is in the configuration with a date - Weekly digests of new gap-list entries, which need nothing beyond a query and a mail call - Storing answer embeddings so near-identical answers across days can be collapsed in the archive view ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Runs, answers, citations and the cost ledger. This is the archive. DATABASE_PATH=./data/visibility.db # Required. Your brand, its aliases, its domain and the competitor list. BRAND_FILE=./brand.json # Required. The buyer questions. Changing them is recorded against the run. PROMPTS_FILE=./prompts.json # Required. A cap, because cost scales with prompts times providers. MAX_PROMPTS=50 # Required. Per provider. Higher is not faster once you are rate limited. PROVIDER_CONCURRENCY=3 # Required. Reaching it pauses scheduled runs and says so, rather than skipping. MONTHLY_COST_CEILING_USD=40 # Required. Below this the charts show points and a note instead of a trend line. MIN_RUNS_FOR_TREND=14 # Optional · secret. One provider's key. Every provider key is optional and the run skips providers with no key, visibly. ANTHROPIC_API_KEY=sk-ant-...redacted
You are building a production product version of AmICited. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # AmICited · product brief ## Problem The scoring loop is genuinely weekend-buildable, but a faithful replacement is not, and that gap is the honest reason to keep paying. The naive personal build sends your questions through the model APIs, but API answers are not what a real user sees when they open ChatGPT, Perplexity, or AI Overviews, so that number is only a proxy. AmICited does not use LLM APIs at all: it drives real browsers to ask the actual consumer surfaces the way a person in a given country would, a browser-automation fleet routed through country-level proxies, kept working as every surface changes. The other gaps are the historical archive that compounds from day one and cannot be backfilled, an MCP server that lets your AI agents act on your visibility gaps, and, on higher plans, human AEO consulting from real experience that no script replaces. You can build the weekend proxy; the prompt below is that honest consolation build, with its limits stated plainly. ## Product outcome A tracker that builds an archive you cannot buy later, tells you which questions your competitors own and you do not, predicts its own bill, and never lets anyone mistake its numbers for what customers actually see. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - headless browser automation (Playwright/Puppeteer) to reach the real consumer surfaces, not just model APIs - residential or geo-targeted proxies to query from specific countries - a scalable scheduler/queue and the infra to run and retry many browser sessions in parallel - durable per-run storage that never overwrites an answer, so history compounds - ongoing upkeep as each engine and surface changes its UI, models, and citation format ## Explicit non-goals for v1 - Any automation of consumer chat or search interfaces. - Country-level results through proxies. - An MCP server for agents. - Human advisory work. - the faithful signal: API answers only approximate what the ChatGPT app, AI Overviews, AI Mode, Copilot, and Grok actually serve real users, which is what AmICited measures with real browsers - country-level results: what users see varies by geography, which needs a proxy fleet a solo build will not stand up - scale and reliability: running and retrying thousands of real browser sessions in parallel is the hard engineering, not the scoring - months of stored answers and competitor baselines, without which a single week's visibility number is noise, and which you cannot backfill once you start late - human AEO consultations on higher plans, advice from real experience acting on your data, which no self-hosted script reproduces ## Success criteria - Answer rows are confirmed impossible to update - A cost estimate is confirmed to print and block above the ceiling - Near-miss brand strings are confirmed not to match - Sparse charts are confirmed to suppress the trend line - Every export is confirmed to carry the limitation statement ===== BRIEF.md ===== # Build brief · AmICited The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local tracker that asks answer-engine APIs your buyer questions and records, honestly, that this is a proxy to replace AmICited. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Three rules, and the first one is the whole honesty of this build. What you are measuring is not what your buyers see. This tool sends questions through model APIs with their search or grounding tools enabled. The consumer products people actually open are different surfaces with different retrieval, different personalisation and different geography, and the paid tools in this category exist because they drive real browsers through country-level proxies to capture that. So every screen, every export and the README say so in plain words, and no number in this app is ever labelled as what users see. Second, do not automate the consumer web interfaces to close that gap. Scripting a logged-in chat product to scrape answers breaks the terms you agreed to and gets the account closed. Use the documented APIs or accept the limitation. Third, a run is immutable. Answers are the asset and they compound; a row that gets overwritten by a later run has destroyed the only copy of what an engine said that day, and you cannot go back and ask again. ### Stack (fixed, do not substitute) - Runtime: Node 22 with TypeScript, a CLI plus a server-rendered dashboard · no accounts, no telemetry, runs from cron - Storage: SQLite via better-sqlite3, runs written once and never updated · the archive is the product and an overwrite is data loss - Providers: One module per provider behind one ask function, search or grounding enabled · adding a provider should not touch the scheduler - Cost: A ledger row per call and a hard ceiling checked before each request · a nightly job with retries is how a bill surprises you - Matching: Word-boundary alias matching, never substring · a brand called Arc must not match the word March ### Data model (create this before Phase 1) - brands(id, name, aliases_json, domain, is_competitor) · your brand and the competitors, same table - prompts(id, text, intent, active, added_at) · the buyer questions, capped and versioned by addition - runs(id, started_at, finished_at, provider_set_json, prompt_count, status, cost_estimate, cost_actual) - answers(run_id, prompt_id, provider, model_id, raw_text, finish_reason, latency_ms, error_text, tokens_in, tokens_out, searches, asked_at) · written once, never updated - citations(answer_id, ordinal, raw_url, host, canonical_path, is_owned) · normalised, tracking parameters stripped - mentions(answer_id, brand_id, count, first_offset) · offset as a crude prominence proxy, labelled as crude - cost_ledger(run_id, provider, kind, units, unit_note, estimated_at) · kind is tokens or searches, because they are billed separately - Nothing in this schema has an update path. Corrections are new rows, so the history stays true to what was returned on the day. ### Phase 1 · Inputs, and a run that cannot be overwritten Build: Read brand.json and prompts.json from disk. Cap the prompt list at a stated number. Create a run row, then one answer row per prompt per provider, inserted once. Make the answers table reject updates at the database level rather than by convention. Trap: Treating a re-run as a refresh and updating yesterday's row for the same prompt and provider destroys the archive that makes this tool worth anything after week three, and you will not notice until you want a chart. Insert only, and let the dashboard aggregate across runs. Done when: An attempt to update an existing answer row fails at the database level, tested. Two runs on the same day produce two complete sets of answers and neither modifies the other. Editing prompts.json between runs is recorded so a later chart can tell you the question set changed. A run interrupted halfway leaves the answers it completed and is marked incomplete rather than deleted. Do not build yet: No provider calls, scoring, dashboard or export yet. ### Phase 2 · Asking, with a bill you predicted Build: One module per provider, each enabling that provider's search or grounding tool. Cap concurrency per provider. Retry rate limits and server errors twice with backoff, and keep failed cells visible. Before the first call, print an estimate: grounded search is billed separately from tokens, and Anthropic publishes its web search tool at 10 US dollars per 1,000 searches while its web fetch tool adds no charge beyond the tokens the fetched page costs (checked on their pricing page). Look up the equivalent figures for every provider you enable and put them in the configuration, not in the code. Trap: A nightly cron with a retry on every failure quietly multiplies the run, and since grounded search is billed per search on top of tokens, a week of unnoticed retries costs several times what the first run's estimate said. Estimate before the run, record a ledger row per call, and refuse to start a run whose estimate exceeds the ceiling. Done when: A run prints a cost estimate broken into tokens and searches per provider before making a single call, and refuses to start above the configured ceiling. Per-provider concurrency is respected, verified with a fake provider that counts concurrent calls. A provider returning rate limit errors throughout leaves visible failed cells rather than an empty report or a silent skip. The ledger's actual figures are recorded per call and the run's total is shown beside its estimate. Do not build yet: No mention detection, scoring, dashboard or export yet. ### Phase 3 · Finding yourself without finding the wrong word Build: Match brand and competitor names case-insensitively on word boundaries using the alias list, never as a substring. Record a count and the first-mention character offset, and label the offset in the interface as a crude proxy for prominence. Normalise every citation to host plus canonical path with tracking parameters stripped, and mark which hosts you own. Trap: Substring matching a short brand name finds it inside ordinary words and inside competitor names, so a brand called Arc scores a mention every time an answer says March or Arcadia, and the resulting chart is confidently wrong in your favour. Match on word boundaries with an explicit alias list, and keep a fixture test of words that must not match. Done when: A fixture of near-miss strings for a short brand name produces zero matches, and the real name in ordinary sentence positions produces matches, both tested. Citations differing only by tracking parameters or a trailing slash normalise to the same row, tested. Owned-domain citation share is computed from the normalised hosts and matches a hand-counted fixture. The first-mention offset is stored and is labelled as a crude proxy everywhere it appears. Do not build yet: No dashboard, trends or export yet. ### Phase 4 · A dashboard that refuses to draw a trend from noise Build: Render visibility per provider over time, share of voice against each competitor, the cited sources table, and the gap list: prompts where a competitor is cited and you are not, which is the only genuinely actionable view here. Show the number of runs behind every figure and refuse to draw a trend line below a configured minimum number of runs. Trap: Plotting a visibility percentage from a single day's fifty prompts produces a line that moves every day for reasons that are entirely model non-determinism, and somebody makes a content decision from it. Show the run count beside every figure, suppress the trend below the minimum, and state the sample size on every chart. Done when: A chart with fewer than the configured minimum number of runs displays the points and an explicit note instead of a trend line. Every figure shows the number of answers behind it. The gap list is ordered by how often a competitor is cited where you are absent, and clicking an entry shows the full raw answer. A provider with failed cells in a run is shown as failed on the chart rather than as zero. Do not build yet: No export or cost view yet. ### Phase 5 · Export, cost and saying what this is not Build: Export runs, answers, mentions and citations as CSV and as JSON, with the raw answer text included. Show the cost ledger by run and by month against the ceiling. Put a permanent, unsuppressible line on the dashboard and at the top of every export stating that these are API answers with grounding enabled, not what a person sees in the consumer products, and that country-specific results are not measured here. Trap: A tidy dashboard with a caveat buried in the README gets screenshotted into a slide deck, and by the second meeting the number is being treated as what customers see. Put the limitation in the export header and on the chart itself, where it travels with the number. Done when: Every export file carries the limitation statement in its header or first record, verified in the test. The dashboard's limitation line cannot be dismissed or hidden by configuration. The cost view shows spend per run and per month against the ceiling, and the ceiling being hit pauses scheduled runs rather than skipping silently. Raw answer text is included in the JSON export so the archive is readable without this application. Do not build yet: Nothing after this. ### Out of scope (and why) - Driving the consumer chat and search products with a browser, which breaks their terms, gets accounts closed, and is the specific thing the paid products in this category do at scale with infrastructure you are not building this weekend. - Country-level results through a proxy fleet, which needs residential addresses and constant upkeep as each surface changes. - An MCP server exposing your gaps to other agents, which is a separate product on top of this data rather than part of it. - Any human advisory layer, which is the part of these subscriptions that is genuinely a person and cannot be scripted. ### README must contain - That this measures API answers with grounding enabled, which are a proxy and not what people see in the consumer products - That the consumer interfaces are deliberately not automated, and why that is a rule rather than a gap - That answers are insert-only, so the archive compounds and a late start cannot be backfilled - How the cost estimate is computed, that grounded search is billed separately from tokens, and where each price came from - That trends are suppressed below a minimum run count because fifty non-deterministic answers move on their own ===== ARCHITECTURE.md ===== # Architecture · AmICited ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22 with TypeScript, a CLI plus a server-rendered dashboard | no accounts, no telemetry, runs from cron | | Storage | SQLite via better-sqlite3, runs written once and never updated | the archive is the product and an overwrite is data loss | | Providers | One module per provider behind one ask function, search or grounding enabled | adding a provider should not touch the scheduler | | Cost | A ledger row per call and a hard ceiling checked before each request | a nightly job with retries is how a bill surprises you | | Matching | Word-boundary alias matching, never substring | a brand called Arc must not match the word March | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Inputs | Brand, aliases, competitors and prompts as files. | Any format, provided prompt set changes are recorded per run. | | Providers | One module each, grounding enabled. | Any provider with a documented API and published pricing. | | Cost | Estimation, the ledger and the ceiling. | Any pricing source, provided the figures carry a checked date. | | Extraction | Word-boundary matching and URL normalisation. | Not swappable for substring matching, which flatters you. | | Reporting | Gap list, share of voice and trend suppression. | Any presentation, provided sample size travels with every figure. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_PATH` · required · Runs, answers, citations and the cost ledger. This is the archive. - `BRAND_FILE` · required · Your brand, its aliases, its domain and the competitor list. - `PROMPTS_FILE` · required · The buyer questions. Changing them is recorded against the run. - `MAX_PROMPTS` · required · A cap, because cost scales with prompts times providers. - `PROVIDER_CONCURRENCY` · required · Per provider. Higher is not faster once you are rate limited. - `MONTHLY_COST_CEILING_USD` · required · Reaching it pauses scheduled runs and says so, rather than skipping. - `MIN_RUNS_FOR_TREND` · required · Below this the charts show points and a note instead of a trend line. - `ANTHROPIC_API_KEY` · optional, secret · One provider's key. Every provider key is optional and the run skips providers with no key, visibly. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · AmICited product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Node 22 with TypeScript, a CLI plus a server-rendered dashboard, SQLite via better-sqlite3, runs written once and never updated, One module per provider behind one ask function, search or grounding enabled, A ledger row per call and a hard ceiling checked before each request, Word-boundary alias matching, never substring. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Treating a re-run as a refresh and updating yesterday's row for the same prompt and provider destroys the archive that makes this tool worth anything after week three, and you will not notice until you want a chart. Insert only, and let the dashboard aggregate across runs. - A nightly cron with a retry on every failure quietly multiplies the run, and since grounded search is billed per search on top of tokens, a week of unnoticed retries costs several times what the first run's estimate said. Estimate before the run, record a ledger row per call, and refuse to start a run whose estimate exceeds the ceiling. - Substring matching a short brand name finds it inside ordinary words and inside competitor names, so a brand called Arc scores a mention every time an answer says March or Arcadia, and the resulting chart is confidently wrong in your favour. Match on word boundaries with an explicit alias list, and keep a fixture test of words that must not match. - Plotting a visibility percentage from a single day's fifty prompts produces a line that moves every day for reasons that are entirely model non-determinism, and somebody makes a content decision from it. Show the run count beside every figure, suppress the trend below the minimum, and state the sample size on every chart. - A tidy dashboard with a caveat buried in the README gets screenshotted into a slide deck, and by the second meeting the number is being treated as what customers see. Put the limitation in the export header and on the chart itself, where it travels with the number. ===== MILESTONES.md ===== # Delivery milestones · AmICited Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Inputs, and a run that cannot be overwritten Brand, competitors and questions are files you edit, and each run appends an immutable set of answers. ### Steps 1. Make answers insert-only in the schema A trigger or a constraint, not a code review habit. Files: `src/db/schema.sql`, `tests/immutable-answers.test.ts` 2. Record the prompt set per run So a shift in the numbers can be traced to a shift in the questions. Files: `src/runs.ts` 3. Mark an interrupted run incomplete Partial data that says it is partial is useful; partial data that lies is not. Files: `src/runs.ts` ### Done when - [ ] Updating an answer row fails at the database level - [ ] Two runs in a day coexist intact - [ ] A prompt set change is recorded per run - [ ] An interrupted run is marked incomplete ### Watch out - Treating a re-run as a refresh and updating yesterday's row for the same prompt and provider destroys the archive that makes this tool worth anything after week three, and you will not notice until you want a chart. Insert only, and let the dashboard aggregate across runs. ## M2 · Asking, with a bill you predicted Every provider is called with grounding on, concurrency bounded, and the cost estimated before the run starts. ### Steps 1. Put every price in configuration Because prices change and a number in the code is a number nobody rechecks. Files: `src/config/pricing.ts`, `docs/pricing-sources.md` 2. Estimate before the first call The estimate is the only thing between a cron job and a surprise. Files: `src/cost.ts` 3. Keep failed cells visible A missing provider that looks like zero visibility is a wrong conclusion. Files: `src/providers/index.ts` ### Done when - [ ] An estimate prints before any call and blocks above the ceiling - [ ] Per-provider concurrency is respected under test - [ ] Rate-limited providers leave visible failed cells - [ ] The ledger records actual tokens and searches per call ### Watch out - A nightly cron with a retry on every failure quietly multiplies the run, and since grounded search is billed per search on top of tokens, a week of unnoticed retries costs several times what the first run's estimate said. Estimate before the run, record a ledger row per call, and refuse to start a run whose estimate exceeds the ceiling. ## M3 · Finding yourself without finding the wrong word Brand and competitor mentions and cited sources are extracted in a way you would defend. ### Steps 1. Word boundaries and an alias list The difference between a chart and a flattering error. Files: `src/match.ts`, `tests/near-miss.test.ts` 2. Normalise citation URLs Same page, same row, whatever the campaign parameters say. Files: `src/citations.ts`, `tests/url-normalise.test.ts` 3. Label the prominence proxy as a proxy In the interface, not only in a comment. Files: `src/views/answer.tsx` ### Done when - [ ] Near-miss strings produce zero matches - [ ] Citations differing only by parameters collapse to one row - [ ] Owned-domain share matches a hand-counted fixture - [ ] The offset is labelled as crude wherever shown ### Watch out - Substring matching a short brand name finds it inside ordinary words and inside competitor names, so a brand called Arc scores a mention every time an answer says March or Arcadia, and the resulting chart is confidently wrong in your favour. Match on word boundaries with an explicit alias list, and keep a fixture test of words that must not match. ## M4 · A dashboard that refuses to draw a trend from noise The report shows share of voice, the gap list and how much of it is signal. ### Steps 1. Suppress trends below the minimum run count The honest version of a sparse chart. Files: `src/views/trend.tsx` 2. Build the gap list first It is the only view here that tells you what to do next. Files: `src/views/gaps.tsx` 3. Distinguish failed from zero They look the same on a chart and mean opposite things. Files: `src/views/provider.tsx` ### Done when - [ ] Sparse charts show points and a note, not a trend - [ ] Every figure states its sample size - [ ] The gap list links to full raw answers - [ ] Failed cells are never rendered as zero ### Watch out - Plotting a visibility percentage from a single day's fifty prompts produces a line that moves every day for reasons that are entirely model non-determinism, and somebody makes a content decision from it. Show the run count beside every figure, suppress the trend below the minimum, and state the sample size on every chart. ## M5 · Export, cost and saying what this is not (production only) The archive leaves in open formats and the limitation is stated everywhere it matters. ### Steps 1. Put the caveat in the export header So it survives the trip into a spreadsheet. Files: `src/export.ts`, `tests/export-caveat.test.ts` 2. Show spend against the ceiling Per run and per month, in the interface, not in a log. Files: `src/views/cost.tsx` 3. Pause on ceiling, do not skip A silently skipped run looks exactly like a flat chart. Files: `src/scheduler.ts` ### Done when - [ ] Every export carries the limitation statement - [ ] The dashboard caveat cannot be hidden - [ ] Cost per run and per month is shown against the ceiling - [ ] Hitting the ceiling pauses rather than skips ### Watch out - A tidy dashboard with a caveat buried in the README gets screenshotted into a slide deck, and by the second meeting the number is being treated as what customers see. Put the limitation in the export header and on the chart itself, where it travels with the number. ===== OPERATIONS.md ===== # Operations · AmICited ## Backup The database, daily, off the machine. The archive cannot be recreated because you cannot ask an engine what it would have said last Tuesday, which makes this the rare case where the backup is worth more than the code. ## Restore Restore and confirm the run history is continuous, then check for a gap. A missing week is not recoverable, so noticing it is the whole point of checking. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Runs completed versus scheduled, failed cells per provider, spend against the ceiling, and the age of each price in the configuration. A price checked six months ago is a wrong estimate. ## Incident checklist A cost surprise is diagnosed from the ledger, which separates tokens from searches so you can see which one ran away. A chart that moved sharply is diagnosed by opening the raw answers, which is why they are stored in full. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Answer rows are confirmed impossible to update - [ ] A cost estimate is confirmed to print and block above the ceiling - [ ] Near-miss brand strings are confirmed not to match - [ ] Sparse charts are confirmed to suppress the trend line - [ ] Every export is confirmed to carry the limitation statement ## Launch constraint Do not market omitted AmICited capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Runs, answers, citations and the cost ledger. This is the archive. DATABASE_PATH=./data/visibility.db # Required. Your brand, its aliases, its domain and the competitor list. BRAND_FILE=./brand.json # Required. The buyer questions. Changing them is recorded against the run. PROMPTS_FILE=./prompts.json # Required. A cap, because cost scales with prompts times providers. MAX_PROMPTS=50 # Required. Per provider. Higher is not faster once you are rate limited. PROVIDER_CONCURRENCY=3 # Required. Reaching it pauses scheduled runs and says so, rather than skipping. MONTHLY_COST_CEILING_USD=40 # Required. Below this the charts show points and a note instead of a trend line. MIN_RUNS_FOR_TREND=14 # Optional · secret. One provider's key. Every provider key is optional and the run skips providers with no key, visibly. ANTHROPIC_API_KEY=sk-ant-...redacted
# AmICited · indie build A local answer-engine visibility tracker whose hard problems are staying honest about being a proxy rather than a measurement, keeping a run's cost bounded and predictable, and not reading a trend into noise from fifty non-deterministic answers. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22 with TypeScript, a CLI plus a server-rendered dashboard | no accounts, no telemetry, runs from cron | | Storage | SQLite via better-sqlite3, runs written once and never updated | the archive is the product and an overwrite is data loss | | Providers | One module per provider behind one ask function, search or grounding enabled | adding a provider should not touch the scheduler | | Cost | A ledger row per call and a hard ceiling checked before each request | a nightly job with retries is how a bill surprises you | | Matching | Word-boundary alias matching, never substring | a brand called Arc must not match the word March | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the CLI, the scheduler and the dashboard. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **API keys for the providers you enable** · pay per use, and the search component is billed per search on several providers - Why: Each provider is a separate account with its own grounded-search billing, and enabling four of them means four bills. - Get it: Create an account with each provider and generate an API key with the smallest scope offered. - Verify: A single one-prompt run completes against each enabled provider - [ ] **Each provider's current pricing page** · free - Why: The cost ceiling is only meaningful if the numbers behind it are today's, and every one of these providers has changed pricing. - Get it: Open each provider's pricing page, record the token rates and the grounded-search rate with the date. - Verify: Every enabled provider has a price and a checked date in the configuration - [ ] **A machine that stays awake** · a few pounds a month - Why: The value here is an archive that compounds daily, and a laptop that sleeps produces a chart with holes in it. - Get it: A small VPS, a home server or anything that can run a nightly job. - Verify: A scheduled run completes overnight without you present - [ ] **Your buyer questions and competitor list** · free - Why: The questions are the instrument, and changing them mid-way makes every earlier figure incomparable, so they deserve an hour before the first run. - Get it: Write the questions a buyer would actually type, and list the competitors by every name and spelling they use. - Verify: Both files exist, the prompt count is under the cap, and every alias is listed ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Any automation of consumer chat or search interfaces. - Country-level results through proxies. - An MCP server for agents. - Human advisory work. - the faithful signal: API answers only approximate what the ChatGPT app, AI Overviews, AI Mode, Copilot, and Grok actually serve real users, which is what AmICited measures with real browsers - country-level results: what users see varies by geography, which needs a proxy fleet a solo build will not stand up - scale and reliability: running and retrying thousands of real browser sessions in parallel is the hard engineering, not the scoring - months of stored answers and competitor baselines, without which a single week's visibility number is noise, and which you cannot backfill once you start late - human AEO consultations on higher plans, advice from real experience acting on your data, which no self-hosted script reproduces If one of those is essential to you, that is the reason to keep paying for AmICited, and the README should say so rather than pretend.
# Build brief · AmICited The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local tracker that asks answer-engine APIs your buyer questions and records, honestly, that this is a proxy to replace AmICited. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Three rules, and the first one is the whole honesty of this build. What you are measuring is not what your buyers see. This tool sends questions through model APIs with their search or grounding tools enabled. The consumer products people actually open are different surfaces with different retrieval, different personalisation and different geography, and the paid tools in this category exist because they drive real browsers through country-level proxies to capture that. So every screen, every export and the README say so in plain words, and no number in this app is ever labelled as what users see. Second, do not automate the consumer web interfaces to close that gap. Scripting a logged-in chat product to scrape answers breaks the terms you agreed to and gets the account closed. Use the documented APIs or accept the limitation. Third, a run is immutable. Answers are the asset and they compound; a row that gets overwritten by a later run has destroyed the only copy of what an engine said that day, and you cannot go back and ask again. ### Stack (fixed, do not substitute) - Runtime: Node 22 with TypeScript, a CLI plus a server-rendered dashboard · no accounts, no telemetry, runs from cron - Storage: SQLite via better-sqlite3, runs written once and never updated · the archive is the product and an overwrite is data loss - Providers: One module per provider behind one ask function, search or grounding enabled · adding a provider should not touch the scheduler - Cost: A ledger row per call and a hard ceiling checked before each request · a nightly job with retries is how a bill surprises you - Matching: Word-boundary alias matching, never substring · a brand called Arc must not match the word March ### Data model (create this before Phase 1) - brands(id, name, aliases_json, domain, is_competitor) · your brand and the competitors, same table - prompts(id, text, intent, active, added_at) · the buyer questions, capped and versioned by addition - runs(id, started_at, finished_at, provider_set_json, prompt_count, status, cost_estimate, cost_actual) - answers(run_id, prompt_id, provider, model_id, raw_text, finish_reason, latency_ms, error_text, tokens_in, tokens_out, searches, asked_at) · written once, never updated - citations(answer_id, ordinal, raw_url, host, canonical_path, is_owned) · normalised, tracking parameters stripped - mentions(answer_id, brand_id, count, first_offset) · offset as a crude prominence proxy, labelled as crude - cost_ledger(run_id, provider, kind, units, unit_note, estimated_at) · kind is tokens or searches, because they are billed separately - Nothing in this schema has an update path. Corrections are new rows, so the history stays true to what was returned on the day. ### Phase 1 · Inputs, and a run that cannot be overwritten Build: Read brand.json and prompts.json from disk. Cap the prompt list at a stated number. Create a run row, then one answer row per prompt per provider, inserted once. Make the answers table reject updates at the database level rather than by convention. Trap: Treating a re-run as a refresh and updating yesterday's row for the same prompt and provider destroys the archive that makes this tool worth anything after week three, and you will not notice until you want a chart. Insert only, and let the dashboard aggregate across runs. Done when: An attempt to update an existing answer row fails at the database level, tested. Two runs on the same day produce two complete sets of answers and neither modifies the other. Editing prompts.json between runs is recorded so a later chart can tell you the question set changed. A run interrupted halfway leaves the answers it completed and is marked incomplete rather than deleted. Do not build yet: No provider calls, scoring, dashboard or export yet. ### Phase 2 · Asking, with a bill you predicted Build: One module per provider, each enabling that provider's search or grounding tool. Cap concurrency per provider. Retry rate limits and server errors twice with backoff, and keep failed cells visible. Before the first call, print an estimate: grounded search is billed separately from tokens, and Anthropic publishes its web search tool at 10 US dollars per 1,000 searches while its web fetch tool adds no charge beyond the tokens the fetched page costs (checked on their pricing page). Look up the equivalent figures for every provider you enable and put them in the configuration, not in the code. Trap: A nightly cron with a retry on every failure quietly multiplies the run, and since grounded search is billed per search on top of tokens, a week of unnoticed retries costs several times what the first run's estimate said. Estimate before the run, record a ledger row per call, and refuse to start a run whose estimate exceeds the ceiling. Done when: A run prints a cost estimate broken into tokens and searches per provider before making a single call, and refuses to start above the configured ceiling. Per-provider concurrency is respected, verified with a fake provider that counts concurrent calls. A provider returning rate limit errors throughout leaves visible failed cells rather than an empty report or a silent skip. The ledger's actual figures are recorded per call and the run's total is shown beside its estimate. Do not build yet: No mention detection, scoring, dashboard or export yet. ### Phase 3 · Finding yourself without finding the wrong word Build: Match brand and competitor names case-insensitively on word boundaries using the alias list, never as a substring. Record a count and the first-mention character offset, and label the offset in the interface as a crude proxy for prominence. Normalise every citation to host plus canonical path with tracking parameters stripped, and mark which hosts you own. Trap: Substring matching a short brand name finds it inside ordinary words and inside competitor names, so a brand called Arc scores a mention every time an answer says March or Arcadia, and the resulting chart is confidently wrong in your favour. Match on word boundaries with an explicit alias list, and keep a fixture test of words that must not match. Done when: A fixture of near-miss strings for a short brand name produces zero matches, and the real name in ordinary sentence positions produces matches, both tested. Citations differing only by tracking parameters or a trailing slash normalise to the same row, tested. Owned-domain citation share is computed from the normalised hosts and matches a hand-counted fixture. The first-mention offset is stored and is labelled as a crude proxy everywhere it appears. Do not build yet: No dashboard, trends or export yet. ### Phase 4 · A dashboard that refuses to draw a trend from noise Build: Render visibility per provider over time, share of voice against each competitor, the cited sources table, and the gap list: prompts where a competitor is cited and you are not, which is the only genuinely actionable view here. Show the number of runs behind every figure and refuse to draw a trend line below a configured minimum number of runs. Trap: Plotting a visibility percentage from a single day's fifty prompts produces a line that moves every day for reasons that are entirely model non-determinism, and somebody makes a content decision from it. Show the run count beside every figure, suppress the trend below the minimum, and state the sample size on every chart. Done when: A chart with fewer than the configured minimum number of runs displays the points and an explicit note instead of a trend line. Every figure shows the number of answers behind it. The gap list is ordered by how often a competitor is cited where you are absent, and clicking an entry shows the full raw answer. A provider with failed cells in a run is shown as failed on the chart rather than as zero. Do not build yet: No export or cost view yet. ### Phase 5 · Export, cost and saying what this is not Build: Export runs, answers, mentions and citations as CSV and as JSON, with the raw answer text included. Show the cost ledger by run and by month against the ceiling. Put a permanent, unsuppressible line on the dashboard and at the top of every export stating that these are API answers with grounding enabled, not what a person sees in the consumer products, and that country-specific results are not measured here. Trap: A tidy dashboard with a caveat buried in the README gets screenshotted into a slide deck, and by the second meeting the number is being treated as what customers see. Put the limitation in the export header and on the chart itself, where it travels with the number. Done when: Every export file carries the limitation statement in its header or first record, verified in the test. The dashboard's limitation line cannot be dismissed or hidden by configuration. The cost view shows spend per run and per month against the ceiling, and the ceiling being hit pauses scheduled runs rather than skipping silently. Raw answer text is included in the JSON export so the archive is readable without this application. Do not build yet: Nothing after this. ### Out of scope (and why) - Driving the consumer chat and search products with a browser, which breaks their terms, gets accounts closed, and is the specific thing the paid products in this category do at scale with infrastructure you are not building this weekend. - Country-level results through a proxy fleet, which needs residential addresses and constant upkeep as each surface changes. - An MCP server exposing your gaps to other agents, which is a separate product on top of this data rather than part of it. - Any human advisory layer, which is the part of these subscriptions that is genuinely a person and cannot be scripted. ### README must contain - That this measures API answers with grounding enabled, which are a proxy and not what people see in the consumer products - That the consumer interfaces are deliberately not automated, and why that is a rule rather than a gap - That answers are insert-only, so the archive compounds and a late start cannot be backfilled - How the cost estimate is computed, that grounded search is billed separately from tokens, and where each price came from - That trends are suppressed below a minimum run count because fifty non-deterministic answers move on their own
# Agent instructions · AmICited indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22 with TypeScript, a CLI plus a server-rendered dashboard, SQLite via better-sqlite3, runs written once and never updated, One module per provider behind one ask function, search or grounding enabled, A ledger row per call and a hard ceiling checked before each request, Word-boundary alias matching, never substring. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Treating a re-run as a refresh and updating yesterday's row for the same prompt and provider destroys the archive that makes this tool worth anything after week three, and you will not notice until you want a chart. Insert only, and let the dashboard aggregate across runs. - A nightly cron with a retry on every failure quietly multiplies the run, and since grounded search is billed per search on top of tokens, a week of unnoticed retries costs several times what the first run's estimate said. Estimate before the run, record a ledger row per call, and refuse to start a run whose estimate exceeds the ceiling. - Substring matching a short brand name finds it inside ordinary words and inside competitor names, so a brand called Arc scores a mention every time an answer says March or Arcadia, and the resulting chart is confidently wrong in your favour. Match on word boundaries with an explicit alias list, and keep a fixture test of words that must not match. - Plotting a visibility percentage from a single day's fifty prompts produces a line that moves every day for reasons that are entirely model non-determinism, and somebody makes a content decision from it. Show the run count beside every figure, suppress the trend below the minimum, and state the sample size on every chart.
# Build plan · AmICited A local answer-engine visibility tracker whose hard problems are staying honest about being a proxy rather than a measurement, keeping a run's cost bounded and predictable, and not reading a trend into noise from fifty non-deterministic answers. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Inputs, and a run that cannot be overwritten Brand, competitors and questions are files you edit, and each run appends an immutable set of answers. ### Steps 1. Make answers insert-only in the schema A trigger or a constraint, not a code review habit. Files: `src/db/schema.sql`, `tests/immutable-answers.test.ts` 2. Record the prompt set per run So a shift in the numbers can be traced to a shift in the questions. Files: `src/runs.ts` 3. Mark an interrupted run incomplete Partial data that says it is partial is useful; partial data that lies is not. Files: `src/runs.ts` ### Done when - [ ] Updating an answer row fails at the database level - [ ] Two runs in a day coexist intact - [ ] A prompt set change is recorded per run - [ ] An interrupted run is marked incomplete ### Watch out - Treating a re-run as a refresh and updating yesterday's row for the same prompt and provider destroys the archive that makes this tool worth anything after week three, and you will not notice until you want a chart. Insert only, and let the dashboard aggregate across runs. ## Phase 2 · Asking, with a bill you predicted Every provider is called with grounding on, concurrency bounded, and the cost estimated before the run starts. ### Steps 1. Put every price in configuration Because prices change and a number in the code is a number nobody rechecks. Files: `src/config/pricing.ts`, `docs/pricing-sources.md` 2. Estimate before the first call The estimate is the only thing between a cron job and a surprise. Files: `src/cost.ts` 3. Keep failed cells visible A missing provider that looks like zero visibility is a wrong conclusion. Files: `src/providers/index.ts` ### Done when - [ ] An estimate prints before any call and blocks above the ceiling - [ ] Per-provider concurrency is respected under test - [ ] Rate-limited providers leave visible failed cells - [ ] The ledger records actual tokens and searches per call ### Watch out - A nightly cron with a retry on every failure quietly multiplies the run, and since grounded search is billed per search on top of tokens, a week of unnoticed retries costs several times what the first run's estimate said. Estimate before the run, record a ledger row per call, and refuse to start a run whose estimate exceeds the ceiling. ## Phase 3 · Finding yourself without finding the wrong word Brand and competitor mentions and cited sources are extracted in a way you would defend. ### Steps 1. Word boundaries and an alias list The difference between a chart and a flattering error. Files: `src/match.ts`, `tests/near-miss.test.ts` 2. Normalise citation URLs Same page, same row, whatever the campaign parameters say. Files: `src/citations.ts`, `tests/url-normalise.test.ts` 3. Label the prominence proxy as a proxy In the interface, not only in a comment. Files: `src/views/answer.tsx` ### Done when - [ ] Near-miss strings produce zero matches - [ ] Citations differing only by parameters collapse to one row - [ ] Owned-domain share matches a hand-counted fixture - [ ] The offset is labelled as crude wherever shown ### Watch out - Substring matching a short brand name finds it inside ordinary words and inside competitor names, so a brand called Arc scores a mention every time an answer says March or Arcadia, and the resulting chart is confidently wrong in your favour. Match on word boundaries with an explicit alias list, and keep a fixture test of words that must not match. ## Phase 4 · A dashboard that refuses to draw a trend from noise The report shows share of voice, the gap list and how much of it is signal. ### Steps 1. Suppress trends below the minimum run count The honest version of a sparse chart. Files: `src/views/trend.tsx` 2. Build the gap list first It is the only view here that tells you what to do next. Files: `src/views/gaps.tsx` 3. Distinguish failed from zero They look the same on a chart and mean opposite things. Files: `src/views/provider.tsx` ### Done when - [ ] Sparse charts show points and a note, not a trend - [ ] Every figure states its sample size - [ ] The gap list links to full raw answers - [ ] Failed cells are never rendered as zero ### Watch out - Plotting a visibility percentage from a single day's fifty prompts produces a line that moves every day for reasons that are entirely model non-determinism, and somebody makes a content decision from it. Show the run count beside every figure, suppress the trend below the minimum, and state the sample size on every chart. ## Not in this build - Any automation of consumer chat or search interfaces. - Country-level results through proxies. - An MCP server for agents. - Human advisory work. ## After v1, if you want it - A fifth provider added as a module, once its pricing is in the configuration with a date - Weekly digests of new gap-list entries, which need nothing beyond a query and a mail call - Storing answer embeddings so near-identical answers across days can be collapsed in the archive view
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Runs, answers, citations and the cost ledger. This is the archive. DATABASE_PATH=./data/visibility.db # Required. Your brand, its aliases, its domain and the competitor list. BRAND_FILE=./brand.json # Required. The buyer questions. Changing them is recorded against the run. PROMPTS_FILE=./prompts.json # Required. A cap, because cost scales with prompts times providers. MAX_PROMPTS=50 # Required. Per provider. Higher is not faster once you are rate limited. PROVIDER_CONCURRENCY=3 # Required. Reaching it pauses scheduled runs and says so, rather than skipping. MONTHLY_COST_CEILING_USD=40 # Required. Below this the charts show points and a note instead of a trend line. MIN_RUNS_FOR_TREND=14 # Optional · secret. One provider's key. Every provider key is optional and the run skips providers with no key, visibly. ANTHROPIC_API_KEY=sk-ant-...redacted
# AmICited · product brief ## Problem The scoring loop is genuinely weekend-buildable, but a faithful replacement is not, and that gap is the honest reason to keep paying. The naive personal build sends your questions through the model APIs, but API answers are not what a real user sees when they open ChatGPT, Perplexity, or AI Overviews, so that number is only a proxy. AmICited does not use LLM APIs at all: it drives real browsers to ask the actual consumer surfaces the way a person in a given country would, a browser-automation fleet routed through country-level proxies, kept working as every surface changes. The other gaps are the historical archive that compounds from day one and cannot be backfilled, an MCP server that lets your AI agents act on your visibility gaps, and, on higher plans, human AEO consulting from real experience that no script replaces. You can build the weekend proxy; the prompt below is that honest consolation build, with its limits stated plainly. ## Product outcome A tracker that builds an archive you cannot buy later, tells you which questions your competitors own and you do not, predicts its own bill, and never lets anyone mistake its numbers for what customers actually see. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - headless browser automation (Playwright/Puppeteer) to reach the real consumer surfaces, not just model APIs - residential or geo-targeted proxies to query from specific countries - a scalable scheduler/queue and the infra to run and retry many browser sessions in parallel - durable per-run storage that never overwrites an answer, so history compounds - ongoing upkeep as each engine and surface changes its UI, models, and citation format ## Explicit non-goals for v1 - Any automation of consumer chat or search interfaces. - Country-level results through proxies. - An MCP server for agents. - Human advisory work. - the faithful signal: API answers only approximate what the ChatGPT app, AI Overviews, AI Mode, Copilot, and Grok actually serve real users, which is what AmICited measures with real browsers - country-level results: what users see varies by geography, which needs a proxy fleet a solo build will not stand up - scale and reliability: running and retrying thousands of real browser sessions in parallel is the hard engineering, not the scoring - months of stored answers and competitor baselines, without which a single week's visibility number is noise, and which you cannot backfill once you start late - human AEO consultations on higher plans, advice from real experience acting on your data, which no self-hosted script reproduces ## Success criteria - Answer rows are confirmed impossible to update - A cost estimate is confirmed to print and block above the ceiling - Near-miss brand strings are confirmed not to match - Sparse charts are confirmed to suppress the trend line - Every export is confirmed to carry the limitation statement
# Build brief · AmICited The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local tracker that asks answer-engine APIs your buyer questions and records, honestly, that this is a proxy to replace AmICited. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Three rules, and the first one is the whole honesty of this build. What you are measuring is not what your buyers see. This tool sends questions through model APIs with their search or grounding tools enabled. The consumer products people actually open are different surfaces with different retrieval, different personalisation and different geography, and the paid tools in this category exist because they drive real browsers through country-level proxies to capture that. So every screen, every export and the README say so in plain words, and no number in this app is ever labelled as what users see. Second, do not automate the consumer web interfaces to close that gap. Scripting a logged-in chat product to scrape answers breaks the terms you agreed to and gets the account closed. Use the documented APIs or accept the limitation. Third, a run is immutable. Answers are the asset and they compound; a row that gets overwritten by a later run has destroyed the only copy of what an engine said that day, and you cannot go back and ask again. ### Stack (fixed, do not substitute) - Runtime: Node 22 with TypeScript, a CLI plus a server-rendered dashboard · no accounts, no telemetry, runs from cron - Storage: SQLite via better-sqlite3, runs written once and never updated · the archive is the product and an overwrite is data loss - Providers: One module per provider behind one ask function, search or grounding enabled · adding a provider should not touch the scheduler - Cost: A ledger row per call and a hard ceiling checked before each request · a nightly job with retries is how a bill surprises you - Matching: Word-boundary alias matching, never substring · a brand called Arc must not match the word March ### Data model (create this before Phase 1) - brands(id, name, aliases_json, domain, is_competitor) · your brand and the competitors, same table - prompts(id, text, intent, active, added_at) · the buyer questions, capped and versioned by addition - runs(id, started_at, finished_at, provider_set_json, prompt_count, status, cost_estimate, cost_actual) - answers(run_id, prompt_id, provider, model_id, raw_text, finish_reason, latency_ms, error_text, tokens_in, tokens_out, searches, asked_at) · written once, never updated - citations(answer_id, ordinal, raw_url, host, canonical_path, is_owned) · normalised, tracking parameters stripped - mentions(answer_id, brand_id, count, first_offset) · offset as a crude prominence proxy, labelled as crude - cost_ledger(run_id, provider, kind, units, unit_note, estimated_at) · kind is tokens or searches, because they are billed separately - Nothing in this schema has an update path. Corrections are new rows, so the history stays true to what was returned on the day. ### Phase 1 · Inputs, and a run that cannot be overwritten Build: Read brand.json and prompts.json from disk. Cap the prompt list at a stated number. Create a run row, then one answer row per prompt per provider, inserted once. Make the answers table reject updates at the database level rather than by convention. Trap: Treating a re-run as a refresh and updating yesterday's row for the same prompt and provider destroys the archive that makes this tool worth anything after week three, and you will not notice until you want a chart. Insert only, and let the dashboard aggregate across runs. Done when: An attempt to update an existing answer row fails at the database level, tested. Two runs on the same day produce two complete sets of answers and neither modifies the other. Editing prompts.json between runs is recorded so a later chart can tell you the question set changed. A run interrupted halfway leaves the answers it completed and is marked incomplete rather than deleted. Do not build yet: No provider calls, scoring, dashboard or export yet. ### Phase 2 · Asking, with a bill you predicted Build: One module per provider, each enabling that provider's search or grounding tool. Cap concurrency per provider. Retry rate limits and server errors twice with backoff, and keep failed cells visible. Before the first call, print an estimate: grounded search is billed separately from tokens, and Anthropic publishes its web search tool at 10 US dollars per 1,000 searches while its web fetch tool adds no charge beyond the tokens the fetched page costs (checked on their pricing page). Look up the equivalent figures for every provider you enable and put them in the configuration, not in the code. Trap: A nightly cron with a retry on every failure quietly multiplies the run, and since grounded search is billed per search on top of tokens, a week of unnoticed retries costs several times what the first run's estimate said. Estimate before the run, record a ledger row per call, and refuse to start a run whose estimate exceeds the ceiling. Done when: A run prints a cost estimate broken into tokens and searches per provider before making a single call, and refuses to start above the configured ceiling. Per-provider concurrency is respected, verified with a fake provider that counts concurrent calls. A provider returning rate limit errors throughout leaves visible failed cells rather than an empty report or a silent skip. The ledger's actual figures are recorded per call and the run's total is shown beside its estimate. Do not build yet: No mention detection, scoring, dashboard or export yet. ### Phase 3 · Finding yourself without finding the wrong word Build: Match brand and competitor names case-insensitively on word boundaries using the alias list, never as a substring. Record a count and the first-mention character offset, and label the offset in the interface as a crude proxy for prominence. Normalise every citation to host plus canonical path with tracking parameters stripped, and mark which hosts you own. Trap: Substring matching a short brand name finds it inside ordinary words and inside competitor names, so a brand called Arc scores a mention every time an answer says March or Arcadia, and the resulting chart is confidently wrong in your favour. Match on word boundaries with an explicit alias list, and keep a fixture test of words that must not match. Done when: A fixture of near-miss strings for a short brand name produces zero matches, and the real name in ordinary sentence positions produces matches, both tested. Citations differing only by tracking parameters or a trailing slash normalise to the same row, tested. Owned-domain citation share is computed from the normalised hosts and matches a hand-counted fixture. The first-mention offset is stored and is labelled as a crude proxy everywhere it appears. Do not build yet: No dashboard, trends or export yet. ### Phase 4 · A dashboard that refuses to draw a trend from noise Build: Render visibility per provider over time, share of voice against each competitor, the cited sources table, and the gap list: prompts where a competitor is cited and you are not, which is the only genuinely actionable view here. Show the number of runs behind every figure and refuse to draw a trend line below a configured minimum number of runs. Trap: Plotting a visibility percentage from a single day's fifty prompts produces a line that moves every day for reasons that are entirely model non-determinism, and somebody makes a content decision from it. Show the run count beside every figure, suppress the trend below the minimum, and state the sample size on every chart. Done when: A chart with fewer than the configured minimum number of runs displays the points and an explicit note instead of a trend line. Every figure shows the number of answers behind it. The gap list is ordered by how often a competitor is cited where you are absent, and clicking an entry shows the full raw answer. A provider with failed cells in a run is shown as failed on the chart rather than as zero. Do not build yet: No export or cost view yet. ### Phase 5 · Export, cost and saying what this is not Build: Export runs, answers, mentions and citations as CSV and as JSON, with the raw answer text included. Show the cost ledger by run and by month against the ceiling. Put a permanent, unsuppressible line on the dashboard and at the top of every export stating that these are API answers with grounding enabled, not what a person sees in the consumer products, and that country-specific results are not measured here. Trap: A tidy dashboard with a caveat buried in the README gets screenshotted into a slide deck, and by the second meeting the number is being treated as what customers see. Put the limitation in the export header and on the chart itself, where it travels with the number. Done when: Every export file carries the limitation statement in its header or first record, verified in the test. The dashboard's limitation line cannot be dismissed or hidden by configuration. The cost view shows spend per run and per month against the ceiling, and the ceiling being hit pauses scheduled runs rather than skipping silently. Raw answer text is included in the JSON export so the archive is readable without this application. Do not build yet: Nothing after this. ### Out of scope (and why) - Driving the consumer chat and search products with a browser, which breaks their terms, gets accounts closed, and is the specific thing the paid products in this category do at scale with infrastructure you are not building this weekend. - Country-level results through a proxy fleet, which needs residential addresses and constant upkeep as each surface changes. - An MCP server exposing your gaps to other agents, which is a separate product on top of this data rather than part of it. - Any human advisory layer, which is the part of these subscriptions that is genuinely a person and cannot be scripted. ### README must contain - That this measures API answers with grounding enabled, which are a proxy and not what people see in the consumer products - That the consumer interfaces are deliberately not automated, and why that is a rule rather than a gap - That answers are insert-only, so the archive compounds and a late start cannot be backfilled - How the cost estimate is computed, that grounded search is billed separately from tokens, and where each price came from - That trends are suppressed below a minimum run count because fifty non-deterministic answers move on their own
# Architecture · AmICited ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22 with TypeScript, a CLI plus a server-rendered dashboard | no accounts, no telemetry, runs from cron | | Storage | SQLite via better-sqlite3, runs written once and never updated | the archive is the product and an overwrite is data loss | | Providers | One module per provider behind one ask function, search or grounding enabled | adding a provider should not touch the scheduler | | Cost | A ledger row per call and a hard ceiling checked before each request | a nightly job with retries is how a bill surprises you | | Matching | Word-boundary alias matching, never substring | a brand called Arc must not match the word March | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Inputs | Brand, aliases, competitors and prompts as files. | Any format, provided prompt set changes are recorded per run. | | Providers | One module each, grounding enabled. | Any provider with a documented API and published pricing. | | Cost | Estimation, the ledger and the ceiling. | Any pricing source, provided the figures carry a checked date. | | Extraction | Word-boundary matching and URL normalisation. | Not swappable for substring matching, which flatters you. | | Reporting | Gap list, share of voice and trend suppression. | Any presentation, provided sample size travels with every figure. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_PATH` · required · Runs, answers, citations and the cost ledger. This is the archive. - `BRAND_FILE` · required · Your brand, its aliases, its domain and the competitor list. - `PROMPTS_FILE` · required · The buyer questions. Changing them is recorded against the run. - `MAX_PROMPTS` · required · A cap, because cost scales with prompts times providers. - `PROVIDER_CONCURRENCY` · required · Per provider. Higher is not faster once you are rate limited. - `MONTHLY_COST_CEILING_USD` · required · Reaching it pauses scheduled runs and says so, rather than skipping. - `MIN_RUNS_FOR_TREND` · required · Below this the charts show points and a note instead of a trend line. - `ANTHROPIC_API_KEY` · optional, secret · One provider's key. Every provider key is optional and the run skips providers with no key, visibly. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · AmICited product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Node 22 with TypeScript, a CLI plus a server-rendered dashboard, SQLite via better-sqlite3, runs written once and never updated, One module per provider behind one ask function, search or grounding enabled, A ledger row per call and a hard ceiling checked before each request, Word-boundary alias matching, never substring. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Treating a re-run as a refresh and updating yesterday's row for the same prompt and provider destroys the archive that makes this tool worth anything after week three, and you will not notice until you want a chart. Insert only, and let the dashboard aggregate across runs. - A nightly cron with a retry on every failure quietly multiplies the run, and since grounded search is billed per search on top of tokens, a week of unnoticed retries costs several times what the first run's estimate said. Estimate before the run, record a ledger row per call, and refuse to start a run whose estimate exceeds the ceiling. - Substring matching a short brand name finds it inside ordinary words and inside competitor names, so a brand called Arc scores a mention every time an answer says March or Arcadia, and the resulting chart is confidently wrong in your favour. Match on word boundaries with an explicit alias list, and keep a fixture test of words that must not match. - Plotting a visibility percentage from a single day's fifty prompts produces a line that moves every day for reasons that are entirely model non-determinism, and somebody makes a content decision from it. Show the run count beside every figure, suppress the trend below the minimum, and state the sample size on every chart. - A tidy dashboard with a caveat buried in the README gets screenshotted into a slide deck, and by the second meeting the number is being treated as what customers see. Put the limitation in the export header and on the chart itself, where it travels with the number.
# Delivery milestones · AmICited Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Inputs, and a run that cannot be overwritten Brand, competitors and questions are files you edit, and each run appends an immutable set of answers. ### Steps 1. Make answers insert-only in the schema A trigger or a constraint, not a code review habit. Files: `src/db/schema.sql`, `tests/immutable-answers.test.ts` 2. Record the prompt set per run So a shift in the numbers can be traced to a shift in the questions. Files: `src/runs.ts` 3. Mark an interrupted run incomplete Partial data that says it is partial is useful; partial data that lies is not. Files: `src/runs.ts` ### Done when - [ ] Updating an answer row fails at the database level - [ ] Two runs in a day coexist intact - [ ] A prompt set change is recorded per run - [ ] An interrupted run is marked incomplete ### Watch out - Treating a re-run as a refresh and updating yesterday's row for the same prompt and provider destroys the archive that makes this tool worth anything after week three, and you will not notice until you want a chart. Insert only, and let the dashboard aggregate across runs. ## M2 · Asking, with a bill you predicted Every provider is called with grounding on, concurrency bounded, and the cost estimated before the run starts. ### Steps 1. Put every price in configuration Because prices change and a number in the code is a number nobody rechecks. Files: `src/config/pricing.ts`, `docs/pricing-sources.md` 2. Estimate before the first call The estimate is the only thing between a cron job and a surprise. Files: `src/cost.ts` 3. Keep failed cells visible A missing provider that looks like zero visibility is a wrong conclusion. Files: `src/providers/index.ts` ### Done when - [ ] An estimate prints before any call and blocks above the ceiling - [ ] Per-provider concurrency is respected under test - [ ] Rate-limited providers leave visible failed cells - [ ] The ledger records actual tokens and searches per call ### Watch out - A nightly cron with a retry on every failure quietly multiplies the run, and since grounded search is billed per search on top of tokens, a week of unnoticed retries costs several times what the first run's estimate said. Estimate before the run, record a ledger row per call, and refuse to start a run whose estimate exceeds the ceiling. ## M3 · Finding yourself without finding the wrong word Brand and competitor mentions and cited sources are extracted in a way you would defend. ### Steps 1. Word boundaries and an alias list The difference between a chart and a flattering error. Files: `src/match.ts`, `tests/near-miss.test.ts` 2. Normalise citation URLs Same page, same row, whatever the campaign parameters say. Files: `src/citations.ts`, `tests/url-normalise.test.ts` 3. Label the prominence proxy as a proxy In the interface, not only in a comment. Files: `src/views/answer.tsx` ### Done when - [ ] Near-miss strings produce zero matches - [ ] Citations differing only by parameters collapse to one row - [ ] Owned-domain share matches a hand-counted fixture - [ ] The offset is labelled as crude wherever shown ### Watch out - Substring matching a short brand name finds it inside ordinary words and inside competitor names, so a brand called Arc scores a mention every time an answer says March or Arcadia, and the resulting chart is confidently wrong in your favour. Match on word boundaries with an explicit alias list, and keep a fixture test of words that must not match. ## M4 · A dashboard that refuses to draw a trend from noise The report shows share of voice, the gap list and how much of it is signal. ### Steps 1. Suppress trends below the minimum run count The honest version of a sparse chart. Files: `src/views/trend.tsx` 2. Build the gap list first It is the only view here that tells you what to do next. Files: `src/views/gaps.tsx` 3. Distinguish failed from zero They look the same on a chart and mean opposite things. Files: `src/views/provider.tsx` ### Done when - [ ] Sparse charts show points and a note, not a trend - [ ] Every figure states its sample size - [ ] The gap list links to full raw answers - [ ] Failed cells are never rendered as zero ### Watch out - Plotting a visibility percentage from a single day's fifty prompts produces a line that moves every day for reasons that are entirely model non-determinism, and somebody makes a content decision from it. Show the run count beside every figure, suppress the trend below the minimum, and state the sample size on every chart. ## M5 · Export, cost and saying what this is not (production only) The archive leaves in open formats and the limitation is stated everywhere it matters. ### Steps 1. Put the caveat in the export header So it survives the trip into a spreadsheet. Files: `src/export.ts`, `tests/export-caveat.test.ts` 2. Show spend against the ceiling Per run and per month, in the interface, not in a log. Files: `src/views/cost.tsx` 3. Pause on ceiling, do not skip A silently skipped run looks exactly like a flat chart. Files: `src/scheduler.ts` ### Done when - [ ] Every export carries the limitation statement - [ ] The dashboard caveat cannot be hidden - [ ] Cost per run and per month is shown against the ceiling - [ ] Hitting the ceiling pauses rather than skips ### Watch out - A tidy dashboard with a caveat buried in the README gets screenshotted into a slide deck, and by the second meeting the number is being treated as what customers see. Put the limitation in the export header and on the chart itself, where it travels with the number.
# Operations · AmICited ## Backup The database, daily, off the machine. The archive cannot be recreated because you cannot ask an engine what it would have said last Tuesday, which makes this the rare case where the backup is worth more than the code. ## Restore Restore and confirm the run history is continuous, then check for a gap. A missing week is not recoverable, so noticing it is the whole point of checking. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Runs completed versus scheduled, failed cells per provider, spend against the ceiling, and the age of each price in the configuration. A price checked six months ago is a wrong estimate. ## Incident checklist A cost surprise is diagnosed from the ledger, which separates tokens from searches so you can see which one ran away. A chart that moved sharply is diagnosed by opening the raw answers, which is why they are stored in full. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Answer rows are confirmed impossible to update - [ ] A cost estimate is confirmed to print and block above the ceiling - [ ] Near-miss brand strings are confirmed not to match - [ ] Sparse charts are confirmed to suppress the trend line - [ ] Every export is confirmed to carry the limitation statement ## Launch constraint Do not market omitted AmICited capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Runs, answers, citations and the cost ledger. This is the archive. DATABASE_PATH=./data/visibility.db # Required. Your brand, its aliases, its domain and the competitor list. BRAND_FILE=./brand.json # Required. The buyer questions. Changing them is recorded against the run. PROMPTS_FILE=./prompts.json # Required. A cap, because cost scales with prompts times providers. MAX_PROMPTS=50 # Required. Per provider. Higher is not faster once you are rate limited. PROVIDER_CONCURRENCY=3 # Required. Reaching it pauses scheduled runs and says so, rather than skipping. MONTHLY_COST_CEILING_USD=40 # Required. Below this the charts show points and a note instead of a trend line. MIN_RUNS_FOR_TREND=14 # Optional · secret. One provider's key. Every provider key is optional and the run skips providers with no key, visibly. ANTHROPIC_API_KEY=sk-ant-...redacted
$ choose a build depth, inspect the files, then open the complete pack in your agent
Because the scoring is the cheap half and everything real is the expensive half. AmICited does not call the model APIs, it drives real browsers through country-level proxies to capture what users genuinely see, which is faithful and hard to run at scale and keep working as the surfaces change. It stores every full answer so the value compounds into a history you cannot recreate once you start late, exposes an MCP server so your AI agents can act on the gaps, and on higher plans adds human AEO consulting that acts on your data from real experience. A personal API script gives you a rough proxy; it does not give you any of that.
xthe faithful signal: API answers only approximate what the ChatGPT app, AI Overviews, AI Mode, Copilot, and Grok actually serve real users, which is what AmICited measures with real browsers
xcountry-level results: what users see varies by geography, which needs a proxy fleet a solo build will not stand up
xscale and reliability: running and retrying thousands of real browser sessions in parallel is the hard engineering, not the scoring
xmonths of stored answers and competitor baselines, without which a single week's visibility number is noise, and which you cannot backfill once you start late
xhuman AEO consultations on higher plans, advice from real experience acting on your data, which no self-hosted script reproduces
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
AmICited pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| starter | $57.73/workspace | $52.91/workspace | 50 credits; 3,000 responses; 1 domain; 200 prompts/domain; 10 competitors; 10 articles; 1 workspace; 1 member. |
| pro | $138.54/workspace | $127/workspace | 120 credits; 8,000 responses; 5 domains; 800 prompts; 30 competitors; 50 articles; 15 workspaces; 10 members/workspace. |
| premium | $577.25/workspace | $529.15/workspace | 500 credits; 33,000 responses; 50 domains; 3,200 prompts; unlimited competitors; 500 articles; 50 workspaces; 100 members/workspace. |
| enterprise | — | $1731.75/workspace | Custom or unlimited limits; annual agreement. |
free tierno permanent free tier; 14-day no-card trial with 1 domain, 5 prompts, 3 competitors and 3 articles
billingmonthly + annual; annual gives 1 month free; Enterprise uses an annual agreement; source prices are in EUR
hidden costs1 response consumes 0.015 credit, while agents/articles consume more; unused credits and article allowances do not roll over; extra credits are sold as top-ups; upgrades can be prorated
verified 2026-08-13 · source ↗
Vibecode AmICited
Kinda. The core of AmICited is buildable in a weekend with the prompt on this page, but there are real gaps: the faithful signal: API answers only approximate what the ChatGPT app, AI Overviews, AI Mode, Copilot, and Grok actually serve real users, which is what AmICited measures with real browsers, country-level results: what users see varies by geography, which needs a proxy fleet a solo build will not stand up. Read the honest list above before committing.
How much does AmICited cost?
AmICited costs about $57.73/month (Starter, checked 2026-08-13), which is $692.76 per year.
What do I lose by replacing AmICited?
Honestly: the faithful signal: API answers only approximate what the ChatGPT app, AI Overviews, AI Mode, Copilot, and Grok actually serve real users, which is what AmICited measures with real browsers; country-level results: what users see varies by geography, which needs a proxy fleet a solo build will not stand up; scale and reliability: running and retrying thousands of real browser sessions in parallel is the hard engineering, not the scoring; months of stored answers and competitor baselines, without which a single week's visibility number is noise, and which you cannot backfill once you start late; human AEO consultations on higher plans, advice from real experience acting on your data, which no self-hosted script reproduces. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to AmICited?
Yes: Elmo (Self-hosted AEO tracker covering the API-proxy scoring loop; it queries model APIs, which is exactly the faithfulness gap the paid product exists to cross.) The prompt is for when you want it exactly your way.