Build AmICited
KINDAreplaces $57.73/mosaves $692.76/yrback to the verdict
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.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the CLI, the scheduler and the dashboard.
Get it nodejs.org, or nvm install 22. open ↗
Verify
node --version prints v22 or higher - API keypay 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 - have readyfree
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 - have readya 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 - decidefree
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
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- 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.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATABASE_PATH | required | ./data/visibility.db | Runs, answers, citations and the cost ledger. This is the archive. |
BRAND_FILE | required | ./brand.json | Your brand, its aliases, its domain and the competitor list. |
PROMPTS_FILE | required | ./prompts.json | The buyer questions. Changing them is recorded against the run. |
MAX_PROMPTS | required | 50 | A cap, because cost scales with prompts times providers. |
PROVIDER_CONCURRENCY | required | 3 | Per provider. Higher is not faster once you are rate limited. |
MONTHLY_COST_CEILING_USD | required | 40 | Reaching it pauses scheduled runs and says so, rather than skipping. |
MIN_RUNS_FOR_TREND | required | 14 | Below this the charts show points and a note instead of a trend line. |
ANTHROPIC_API_KEYsecret | optional | sk-ant-...redacted | One provider's key. Every provider key is optional and the run skips providers with no key, visibly. |
The build, in order
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.
A trigger or a constraint, not a code review habit.
Files
src/db/schema.sqltests/immutable-answers.test.tsSo a shift in the numbers can be traced to a shift in the questions.
Files
src/runs.tsPartial data that says it is partial is useful; partial data that lies is not.
Files
src/runs.ts
done when · tick each as it passeswatch 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.
Asking, with a bill you predicted
Every provider is called with grounding on, concurrency bounded, and the cost estimated before the run starts.
Because prices change and a number in the code is a number nobody rechecks.
Files
src/config/pricing.tsdocs/pricing-sources.mdThe estimate is the only thing between a cron job and a surprise.
Files
src/cost.tsA missing provider that looks like zero visibility is a wrong conclusion.
Files
src/providers/index.ts
done when · tick each as it passeswatch 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.
Finding yourself without finding the wrong word
Brand and competitor mentions and cited sources are extracted in a way you would defend.
The difference between a chart and a flattering error.
Files
src/match.tstests/near-miss.test.tsSame page, same row, whatever the campaign parameters say.
Files
src/citations.tstests/url-normalise.test.tsIn the interface, not only in a comment.
Files
src/views/answer.tsx
done when · tick each as it passeswatch 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.
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.
The honest version of a sparse chart.
Files
src/views/trend.tsxIt is the only view here that tells you what to do next.
Files
src/views/gaps.tsxThey look the same on a chart and mean opposite things.
Files
src/views/provider.tsx
done when · tick each as it passeswatch 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.
Export, cost and saying what this is notproduct builder
The archive leaves in open formats and the limitation is stated everywhere it matters.
So it survives the trip into a spreadsheet.
Files
src/export.tstests/export-caveat.test.tsPer run and per month, in the interface, not in a log.
Files
src/views/cost.tsxA silently skipped run looks exactly like a flat chart.
Files
src/scheduler.ts
done when · tick each as it passeswatch 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.
That is the whole plan for AmICited. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- 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
- 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
Need the files? The project pack on the verdict page hands your agent the whole brief · more seo & marketing.