Build CueScout

KINDAreplaces $49/mosaves $588/yrback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

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.

estimated effort multi-daythe files for this build are in the project pack

RuntimeNode 22 with TypeScript, a CLI plus a server-rendered dashboardStorageSQLite with one immutable row per sample, never updatedProvidersDocumented APIs only, one adapter per provider, keys in the environmentMatchingToken-boundary matching against an explicit alias listReportingCounts, sample sizes and spread on every figure

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. 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

  2. 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

  3. 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

  4. 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

  5. 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.

VariableNeededExampleWhere the value comes from
DATABASE_PATHrequired./data/visibility.dbProducts, questions, runs and every sample ever taken.
PROVIDER_A_KEYsecretrequiredpplx-...redactedFirst answer provider. Set a spend limit in their console too.
PROVIDER_B_KEYsecretrequiredsk-...redactedSecond answer provider. Two sources of movement, not one.
SAMPLES_PER_QUESTIONrequired5Attempts per question per provider per run. Below three the spread is meaningless.
MONTHLY_COST_CEILINGrequired20.00A run is refused before any call if its estimate would cross this.
RUN_LOCK_PATHrequired./data/run.lockStops two scheduled runs overlapping and doubling the bill.
OWNED_DOMAINSrequiredexample.com,docs.example.comUsed to mark owned citations. Subdomains are explicit, not inferred.

The build, in order

  1. 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.

    1. The negatives are the part that matters and they are easy to invent afterwards to fit.

      Files fixtures/matching.jsonsrc/match/index.ts

    2. Including hyphens, possessives and trailing punctuation.

      Files src/match/tokens.tstests/match.test.ts

    3. A two-letter alias will match everything and nobody will notice for a month.

      Files src/config/product.ts

    done when · tick each as it passes
    watch 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.
  2. 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.

    1. A gap in the data looks identical to a zero and means something else.

      Files src/run/sample.tssrc/db/schema.sql

    2. The immutability rule enforced somewhere it cannot be forgotten.

      Files src/db/schema.sqltests/immutable.test.ts

    3. A retry is not the same event as the original call.

      Files src/providers/retry.ts

    done when · tick each as it passes
    watch 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.
  3. Citations normalised once, kept raw forever

    Cited URLs are reduced to a comparable form without losing what the provider actually returned.

    1. Normalisation rules change and history should not.

      Files src/citations/store.ts

    2. With a fixture file of real examples from your own runs.

      Files src/citations/normalise.tsfixtures/urls.json

    3. So a fix to the rules applies to years of data.

      Files src/cli/renormalise.tstests/renormalise.test.ts

    done when · tick each as it passes
    watch 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.
  4. 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.

    1. The denominator is the part that stops this being astrology.

      Files src/report/rates.tstests/rates.test.ts

    2. Your own error bar, measured rather than assumed.

      Files src/report/spread.ts

    3. Not an arrow, not a colour, a sentence.

      Files src/views/compare.tsx

    done when · tick each as it passes
    watch 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.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more seo & marketing.