Build CueScout
KINDAreplaces $49/mosaves $588/yrback to the verdict
A visibility sampler whose hard problems are matching a brand name without matching every word that contains it, treating a model answer as one noisy sample rather than a measurement, and reporting change only when it exceeds the variance the same question produces on its own.
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 your version manager. open ↗
Verify
node --version prints v22 or higher - API keypay per call; the estimate in this build is what stops it surprising you
Why One provider is one model's opinion, and the whole point of sampling is seeing how much the answer moves, which needs more than one source of movement.
Get it Create keys through each provider's documented developer console and set a spend limit there as well.
Verify
One question returns a parsed answer with citations from each provider - decidefree
Why The matcher is the difference between a real signal and a chart that responds to unrelated English, and the false positives have to be written down before the matcher exists.
Get it List every spelling, misspelling and abbreviation of your product and competitors, plus words that would wrongly match.
Verify
Both lists are in the fixture file and the suite fails if a false positive matches - have readyfree
Why Questions written from your own positioning ask what you would like to be known for rather than what anyone actually asks, and the resulting figures flatter you.
Get it Collect real questions from your support inbox, sales calls and search console queries.
Verify
At least twenty questions exist and their origin is recorded on each row - decidefree
Why These two numbers together determine both the cost and whether the output means anything, and setting them after the first bill means setting samples to one.
Get it Decide the monthly ceiling first, then work out how many samples per question it affords at your question count.
Verify
Both figures are in the configuration and the estimator refuses a run that would cross the ceiling
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- products(id, name, aliases_json, domains_json, description) · aliases are explicit, including the awkward ones and the misspellings - competitors(id, product_id, name, aliases_json, domains_json) - questions(id, product_id, text, origin, added_at, retired_at) · origin is generated, supplied or observed, and retired questions stay for history - runs(id, product_id, started_at, finished_at, samples_per_question, provider_set_json, cost_minor) - samples(id, run_id, question_id, provider, attempt, model_id, raw_response, finish_reason, error_text, latency_ms, created_at) · never updated, never deleted - mentions(sample_id, entity_kind, entity_id, count, first_offset) · entity_kind is product or competitor - citations(sample_id, url_raw, host, path_canonical, is_owned) · normalised at write time and the raw URL kept beside it - The primary key of the truth in this app is the sample, not the run. A run is an aggregation over samples and can be recomputed from them at any time.
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 | Products, questions, runs and every sample ever taken. |
PROVIDER_A_KEYsecret | required | pplx-...redacted | First answer provider. Set a spend limit in their console too. |
PROVIDER_B_KEYsecret | required | sk-...redacted | Second answer provider. Two sources of movement, not one. |
SAMPLES_PER_QUESTION | required | 5 | Attempts per question per provider per run. Below three the spread is meaningless. |
MONTHLY_COST_CEILING | required | 20.00 | A run is refused before any call if its estimate would cross this. |
RUN_LOCK_PATH | required | ./data/run.lock | Stops two scheduled runs overlapping and doubling the bill. |
OWNED_DOMAINS | required | example.com,docs.example.com | Used to mark owned citations. Subdomains are explicit, not inferred. |
The build, in order
An identity you can match without lying to yourself
Brand and competitor detection works on token boundaries against an explicit alias list, and is tested against the cases that break it.
The negatives are the part that matters and they are easy to invent afterwards to fit.
Files
fixtures/matching.jsonsrc/match/index.tsIncluding hyphens, possessives and trailing punctuation.
Files
src/match/tokens.tstests/match.test.tsA two-letter alias will match everything and nobody will notice for a month.
Files
src/config/product.ts
done when · tick each as it passeswatch out- Matching a brand name as a substring means a product called Cue is mentioned by every answer containing the word rescue, and the resulting graph goes up and to the right for reasons that have nothing to do with your product. Match on token boundaries, keep an explicit list of known false positives in the fixtures, and fail the build when one of them matches.
A run is many samples, not one answer
Every question is asked several times per provider per run, and every response is stored immutably with its errors.
A gap in the data looks identical to a zero and means something else.
Files
src/run/sample.tssrc/db/schema.sqlThe immutability rule enforced somewhere it cannot be forgotten.
Files
src/db/schema.sqltests/immutable.test.tsA retry is not the same event as the original call.
Files
src/providers/retry.ts
done when · tick each as it passeswatch out- Asking each question once per run and plotting the result produces a chart that moves every day because the models are sampling, not because anything changed, and you will spend a week explaining a fluctuation to a client that was never there. Ask several times per run, store each attempt, and compute the within-run spread before you display any between-run difference.
Citations normalised once, kept raw forever
Cited URLs are reduced to a comparable form without losing what the provider actually returned.
Normalisation rules change and history should not.
Files
src/citations/store.tsWith a fixture file of real examples from your own runs.
Files
src/citations/normalise.tsfixtures/urls.jsonSo a fix to the rules applies to years of data.
Files
src/cli/renormalise.tstests/renormalise.test.ts
done when · tick each as it passeswatch out- Normalising in place and discarding the raw URL means that when your canonicalisation turns out to merge two different pages, every historical citation has already been rewritten and there is nothing to recompute from. Keep the raw string on the row, derive the normalised fields, and be able to rebuild them all with one command.
Change, only when it is bigger than the noise
Figures are reported with their sample size and spread, and a difference inside the spread is reported as no change.
The denominator is the part that stops this being astrology.
Files
src/report/rates.tstests/rates.test.tsYour own error bar, measured rather than assumed.
Files
src/report/spread.tsNot an arrow, not a colour, a sentence.
Files
src/views/compare.tsx
done when · tick each as it passeswatch out- Multiplying two rates together into one headline number is the move that makes this category look like analytics, and it hides the fact that a mention rate of two out of thirty and a citation rate of one out of two produced it. Show the counts that made every figure, and refuse to render the combined number anywhere without them.
Scheduling, spend and a report you can hand overproduct builder
Runs happen on a schedule inside a cost ceiling, and the output leaves as files rather than as a login.
Two overlapping runs cost double and answer nothing.
Files
src/schedule/lock.tstests/lock.test.tsThe estimate is computed from samples times providers times questions.
Files
src/cost/estimate.tsSomething you can send, not something you have to log into.
Files
src/report/static.tssrc/cli/export.ts
done when · tick each as it passeswatch out- A scheduler with no lock and a retrying wrapper will start a second run while the first is still going, doubling the day's spend and producing two partial runs that both look like the day's answer. Take a lock, record the run as started before the first call, and make a crashed run resumable rather than restartable.
That is the whole plan for CueScout. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Scraping answer engine web interfaces.
- Search rank tracking.
- Continuous forum monitoring.
- Content plans and draft generation.
- the continuous Reddit and Hacker News scan and buyer-question clustering, which finds where buyers are already asking instead of guessing questions to feed a model
- Google rank badges on matched threads, sourced from a paid search API
- the writing-plan-to-draft loop that turns a gap into a dated 30-day plan and AI-ready page drafts, not just a dashboard number
- weeks of trend history and a competitor 'what moved' digest; a single run is mostly noise
- a hosted, shareable report link you can hand a client without exposing your own infra
- A second matcher pass that records the sentence around each mention, so you can see whether being mentioned was good
- A question retirement workflow, since a question nobody asks any more still costs a sample every run
- A per-question stability figure, which tells you which questions are worth tracking at all
Need the files? The project pack on the verdict page hands your agent the whole brief · more seo & marketing.