# Changelog of AI Brand Monitor — AI Search Visibility in ChatGPT & Perplexity (`highbrow_fame/ai-search-visibility-tracker`) Actor

- **URL**: https://apify.com/highbrow\_fame/ai-search-visibility-tracker/changelog.md
- **Full Actor documentation**: https://apify.com/highbrow\_fame/ai-search-visibility-tracker.md

## Changelog

All notable changes to the **AI Search Visibility Tracker** Apify Actor.

### \[Unreleased] — ChatGPT without a key, 2026-09-09

#### Added

- **ChatGPT now runs on the operator's key, like Perplexity and Gemini already do.**
  Set `SHARED_OPENAI_API_KEY` on the deployed version and a user who supplies nothing
  gets ChatGPT results. Why: the one rival on this shelf with meaningfully more traction
  (48 users / 14 monthly active against our 16 / 2) bundles every engine and charges
  **80% more** for it — measured 2026-09-09. Price was never the thing holding us back.

  ChatGPT carries its own allowance rather than joining the existing one, because it is
  not the same kind of cost:

  | engine | cost per call | we keep | margin |
  |---|---|---|---|
  | Perplexity Agent | ~$0 | $0.04 | ~100% |
  | Gemini (shared) | $0.0019 | $0.04 | 95% |
  | **ChatGPT (shared)** | **~$0.027** | $0.04 | **33%** |

  Allowance: **3 calls** on a free Apify plan (~$0.08 of the operator's money for a run
  that bills nothing — the same $0.08 ceiling the shared Gemini pool already uses),
  **60 calls** on a paid plan (~$1.62 exposure against ~$2.40 of revenue, covering
  20 questions × 3 samples). A user-supplied `openaiApiKey` still wins and is uncapped.

#### Safety

- Every branch is inert when `SHARED_OPENAI_API_KEY` is unset: `usingSharedOpenai` is
  false, the engine resolves to the user's key exactly as before, and the dispatch loop
  takes the same path it took previously. Nothing about the currently earning
  configuration changes until that variable is set.
- The per-run cap is a speed bump, not a defence against a determined abuser. The
  cross-run brake has to live on the vendor side — a **monthly project spend cap in the
  OpenAI dashboard**, the same role Google's 300/day grounding quota plays for Gemini.

### 0.2.15 (2026-08-23)

#### Fixed

- **An empty answer is no longer charged.** The keyless `google-aio` engine is English-only,
  and on any other language it does not fail — it returns HTTP 200 with an empty answer and
  no citations, because Google withholds the locale rendering bundle through the SERP proxy.
  Nothing threw, so that empty result was written as a normal citation record **and billed at
  $0.05**. Measured 2026-08-23, one query each on German, Hungarian and French with
  `engines: ["google-aio"]`:

  | language | run | status | answer chars | citations | charged |
  |---|---|---|---|---|---|
  | en | `H5G9vQx8f1eFytjbp` | SUCCEEDED | 1359 | 4 | 1 record |
  | de | `hjgW0ZTJhu8Qm7yXW` | SUCCEEDED | **0** | **0** | **1 record** |
  | hu | `XqSGdGUcjz2KZ2HbC` | SUCCEEDED | **0** | **0** | **1 record** |
  | fr | `blpGl9qOYnbgQlthd` | SUCCEEDED | **0** | **0** | **1 record** |

  A result with no answer text and no citations is now written as an unbilled `empty` record
  naming the cause, and the run carries a status message explaining it. Verified on beta build
  0.2.15: Hungarian run `dW8c0kStJsPbmEiVn` charged **0** citation records where the same input
  charged 1 before, while English run `NST13aSapghmFYITP` is unchanged at 1359 chars, 4
  citations, 1 record. This is the same defect the Google Maps Actor had with rows carrying no
  name, phone or address: the record looks real because the request fields are echoed back,
  but the user received nothing.
- **The English-only limit is now stated before the run spends anything.** Selecting a
  non-English language while `google-aio` is active logs a warning naming the engines that do
  support that language. The `language` input description says the same thing: the 24 languages
  are real on the key-based engines and not on the keyless one. Measured the same day, a Gemini
  run at `de` returned 3159 characters citing `zendesk.de`, `hubspot.de` and `smc-it.de`, and at
  `hu` 4354 characters citing `webergoline.hu` and `cegemone.hu` — so the multilingual claim
  holds, it just needs a key.
- **`.actor/actor.json` re-synced with the live Store card.** The repo copy still described
  "ChatGPT, Perplexity, Gemini and Google AI Overviews across 24 languages", which reads as
  though all four engines cover all 24. The live card was already correct and is unchanged;
  the stale repo copy would have overwritten it on the next metadata sync.

### 0.2.14 (2026-08-13)

#### Fixed

- **A rejected API key no longer fails the run.** When every engine call died on an
  auth rejection, the run ended with `Actor.fail()` — but a bad key is a setup
  mistake, not a broken Actor, and this was the likeliest way a real user turned
  into a "failed run" on the public success rate (75% in August; the platform blocks
  empty inputs before a run starts, so a bad key was the setup error that actually
  reached the code). Measured against the live APIs on 2026-08-13: OpenAI, Perplexity
  and Anthropic reject a bad key with HTTP 401/403, **Gemini rejects it with HTTP 400
  and "API key not valid"** — both shapes are now recognized. Such runs exit cleanly
  with a status message and an explanatory record naming the fix (and the keyless
  `google-aio` engine). A mixed or 5xx failure pattern still fails the run loudly.
- **Empty required inputs no longer fail the run.** Submitting the form with no
  `queries` or no `brandDomains` called `Actor.fail()`, so every blank-form attempt
  counted as a failed run in the public success rate — part of why the Store quality
  profile (70/100, 2026-08-13) flagged the Actor as "failing often" (75% success rate
  in August). A setup mistake is not a broken Actor: these paths now behave exactly
  like the no-key path — loud status message, explanatory dataset record, clean exit.
  Genuine delivery failures (keys supplied, every call dead) still fail the run.
- **The no-key message now mentions the keyless engine.** It suggested only
  `geminiApiKey`, although `google-aio` runs with no key at all.

#### Added

- **Dataset schema with two views** (`citations`, `sources`): run results now render
  as organized tables in the Console instead of raw JSON. Also a prerequisite for
  publishing a pre-configured task on the Store page.
- **Actor output schema** (`.actor/output_schema.json`): the Output tab links the
  citation records as JSON and CSV.

> Gap warning: builds 0.2.0–0.2.3 shipped without changelog entries. The build
> tagged `latest` before this round was **0.2.3**; the entries below resume from
> there. Treat anything between 0.1.3 and 0.2.3 as undocumented.
>
> **Second gap, same mistake: 0.2.6–0.2.9 also shipped without entries.** The
> entries below were **reconstructed on 2026-08-05** from the Apify build API
> (build numbers and finish times) plus local file modification times. Build
> numbers, timestamps and the current state of the code are verified facts. The
> mapping of *which* change went into *which* build is inferred, not recorded —
> treat it as a best reconstruction, not a log.

### \[Unreleased] — audit fixes, 2026-08-05

Found by a full two-repo audit (77 confirmed findings). **Nothing here is live yet — it all
needs one `apify push`.** Backup of the pre-edit files: session scratchpad `backup-fixes-0447/`.

#### Fixed — user-visible false claims

- **`.actor/input_schema.json`, Gemini key field**: promised "up to 250 queries/day" free
  tier. Measured 2026-08-02 against the live API: **20 requests/day per model**
  (`GenerateRequestsPerDayPerProjectPerModel-FreeTier`, quotaValue 20). The README was
  corrected at the time, the schema was missed, so the **live 0.2.9 build still serves the
  12.5× overstatement to every user who opens the input form.**
- **`.actor/input_schema.json`, engines field**: described the $0 entry point as Gemini's
  free tier. The $0 entry point is the keyless `google-aio` engine, which is the schema
  default and prefill. Rewritten to say so.
- **`README.md:107`** — comparison table "Entry price $0/mo (free Gemini tier)" →
  "(keyless English runs)". Same misattribution.
- **`README.md:128`** — engine table still carried the old quota (250–1000/day) and priced
  Gemini at $0.005/query. The adapter calls with `google_search` grounding
  (`src/engines/gemini.js:67`), which is **billed per request, not per token**: ~$0.035.
  Both corrected.
- **`README.md:133, 331, 332`** — the same $0.035 grounding cost was missing from every
  per-query total. All-four-engines figure moved from ~$0.25–0.28 to ~$0.275–0.30.
- **`README.md:306`** — agency revenue math assumed one Gemini key covers 5 clients at 300
  prompts/month each. That is 50 requests/day against a 20/day cap. Rewritten around the
  keyless default, which has no per-key cap.
- **`README.md:328-332`** — the pricing section listed Gemini free / Gemini paid / all four
  engines, and **omitted the default engine entirely**. Anyone pressing Start on the Store
  runs `google-aio`, and its price appeared nowhere. Added as the first row.

#### Removed — claims the code disproves

- **`README.md`, FAQ "Why no Google AI Overviews in v0.1?"** and the limitations bullet
  "Google AI Overviews are **not yet covered** in v0.1", both promising AIO "in v0.2 via
  `apify/google-search-scraper`". `src/engines/google-aio.js` has shipped, is wired into
  `src/main.js:21/153/186`, and is the **default** engine. Replaced with what is actually
  true, including the English-only limit and why (bot detection on the SERP proxy blocks the
  language JS bundle; seven fixes attempted, all failed).

### \[0.2.9] — 2026-08-05 *(reconstructed)*

Tagged `latest` at 12:49 local (10:49 UTC), two minutes after 0.2.8 took the
`beta` tag. That gap matches the documented release order: push to `beta`, run
against it, then move `latest`.

#### Added — keyless English path is now the default

`src/engines/google-aio.js` (written 2026-08-04 22:06) reads Google's own result
page through Apify's `GOOGLE_SERP` proxy with Playwright and extracts the AI
Overview text plus its cited sources. **No API key is required from anyone** —
not the user, not the author.

Wired into `src/main.js`:

- line 21 — `import { queryGoogleAio } from './engines/google-aio.js'`
- line 153 — registered in the engine map as `'google-aio'`
- line 186 — `if (e !== 'google-aio' && !engineKeys[e])` exempts it from the
  API-key precondition that gates every other engine
- proxy handling is conditional, so a run without `google-aio` reserves no proxy

`.actor/input_schema.json` sets **both** `default` and `prefill` to
`["google-aio"]`, so a new user who opens the Store listing and presses Start
runs with zero configuration.

Measured on 2026-08-04, English only: **8.9 s per query, 1247 characters,
4–5 sources, ~$0.005/query.** At the $0.05 price taking effect 2026-08-10, after
Apify's 20% margin, that is roughly **87% gross margin**.

⛔ **English only, and this is a hard limit — do not reopen it.** Seven fixes were
tried against German/Hungarian and all seven failed. The decisive measurement:
Playwright loads `example.com` through the residential proxy in 1.4 s but does
not load `google.com` in 35 s, while `curl` on the same proxy gets a 302 in
1.3 s. Google declines to serve a browser from a proxy IP. The language JS
bundle (`/xjs/_/js/k=xjs.s.de…`) returns `ERR_ABORTED` through the SERP proxy, so
the renderer never runs. This is bot detection, not a language bug. Details and
the full list of attempted fixes are in the project memory.

ℹ️ Two facts worth keeping: the SERP proxy serves **HTTP only** (HTTPS tunnelling
fails with `ERR_TUNNEL_CONNECTION_FAILED`), and the AI Overview header is
identified by **`jsname="cUzNTd"`**, which is language-independent. Never match
on the visible caption — it differs per language.

#### Changed

- `.actor/input_schema.json` and `.actor/actor.json` updated at 12:45 local,
  four minutes before this build finished, so both are in it.
- `README.md` updated at 12:46 local — including the corrected Gemini free-tier
  figure (**20 requests/day per model**, measured 2026-08-02, down from the 250
  and ~1000 previously published).

#### Known defect shipped in this build — fixed after the fact

`.actor/input_schema.json` still described the Gemini free tier as
**"up to 250 queries/day"** while the README already carried the corrected
20/day figure. Every user opening the input form saw the wrong number. Corrected
on 2026-08-05 after this build; **the fix is not in 0.2.9 and needs a push.**

### \[0.2.8] — 2026-08-05 *(reconstructed)*

Tagged `beta` at 12:47 local (10:47 UTC). Same content as 0.2.9 as far as the
file timestamps show; this was the test build that 0.2.9 promoted two minutes
later.

### \[0.2.7] — 2026-08-04 *(reconstructed)*

Built at 15:59 local (13:59 UTC), **five minutes after 0.2.6**. The only source
file touched in that window is `src/engines/http.js` (modified 15:44 local).
A five-minute gap between two builds is the signature of a follow-up correction
to the change in 0.2.6, but what specifically was corrected is not recorded.

### \[0.2.6] — 2026-08-04 *(reconstructed)*

Built at 15:54 local (13:54 UTC). Carries the `src/engines/http.js` changes made
at 15:44 local — the shared retry/backoff layer introduced in 0.2.4 that all four
API adapters route through. The specific change is not recorded.

⚠️ `src/main.js` (21:25) and `src/engines/google-aio.js` (22:06) were both written
**after** this build, so neither is in 0.2.6 or 0.2.7. They first shipped in 0.2.8.

### \[0.2.5] — 2026-08-02

#### Fixed — a missing API key failed the run, which flags the Store listing

Regression introduced by 0.2.4, caught the same day. Apify runs every Store Actor
automatically against the **prefilled input schema** and expects it to succeed
within 5 minutes. That prefill has `queries` and `brandDomains` but — necessarily,
for a bring-your-own-key Actor — **no API key**. So the automated run lands on the
"no engines runnable" branch.

Before 0.2.4 that branch called `Actor.exit(1)`, which the SDK silently treats as
exit code **0**, so the run reported Succeeded and the check passed. Fixing the
exit-code bug in 0.2.4 turned that into a genuine `FAILED` run — verified against
the live platform with the exact prefill input (run `udMBpN5Wgq8PSAlkf`,
`exitCode: 1`). In other words the Actor had been passing quality assurance only
by accident, and 0.2.4 removed the accident without supplying the substance.

The distinction the code now draws:

- **No API key supplied** → a configuration gap, not a broken Actor. Exits 0 with
  a terminal status message the Console surfaces on the run, plus an `error`
  record in the dataset so the run is not silently empty.
- **Keys supplied, every call failed** → the Actor failed to deliver. Still
  `Actor.fail()`, as introduced in 0.2.4.

This is not a retreat to the old silent-success behaviour: that was a green run
after real work failed, this is an honest report that the run never had the
credentials to start.

⚠️ Context worth keeping: the listing was already flagged "Under maintenance" by
Apify before this release, so the underlying problem predates 0.2.4 — an Actor
that cannot do anything without a user-supplied key cannot pass an automated
check that has no key to give. This release stops the bleeding. The durable fix
is the zero-setup path (shared Gemini key), which is a cost decision, not a code
one — and the newly measured 20-requests/day free tier makes a *free* shared key
a non-starter.

### \[0.2.4] — 2026-08-02 — superseded by 0.2.5 the same day

#### Fixed — engine calls had no retry, and a single transient error lost the query

The defect: `src/engines/*.js` each did one `fetch`, threw on any non-2xx, and
`main.js` logged the failure and moved on. A provider hiccup therefore cost the
user a query permanently. On a 25-call local run on 2026-07-28, **23 calls died**
with Gemini `503 UNAVAILABLE ("The model is overloaded")`. Nothing was charged
(`Actor.charge` fires only after a successful call), but the user received a
near-empty dataset — paying nothing and getting nothing is still a reason to
leave, and this is a plausible contributor to the "30 days: 5 users → 7 days: 0"
trend.

- **New `src/engines/http.js`** — one retry layer shared by all four adapters.
  Retries 408/429/500/502/503/504, network errors and timeouts; **never** retries
  other 4xx (a bad key fails identically on attempt 4, so retrying only delays the
  error message). 3 retries, full-jitter exponential backoff, 180 s per-attempt
  timeout, 75 s total sleep budget per call.
- All four adapters (`gemini`, `openai`, `anthropic`, `perplexity`) now route
  through it and accept optional `onRetry` / `retries`.
- `main.js` logs every retry, counts them in `stats.retries`, and records
  `attempts` on each error row in the dataset.

#### Fixed — a run where every call failed reported success

An all-failed run wrote a summary and exited 0, so the Apify run list showed
green next to an empty dataset. It now exits non-zero with an explanatory log
line. **This will lower the Store-visible success rate** when providers are
down — deliberately: the number should reflect what users actually received.
Nothing is charged on such a run.

(See 0.2.5: applying this to the *no key supplied* branch as well was a mistake,
and broke Apify's automated quality check within hours.)

#### Measured, 2026-08-02 (gemini-2.5-flash, 12 calls at concurrency 8)

| | ok | failed |
|---|---|---|
| before (single attempt) | **1** | 11 |
| after (3 retries) | **5** | 7 |

Honest caveat: the two runs are not a clean A/B. The measurement itself burned
the key's daily quota (see below), so the "after" run started from a worse state
than the "before" run — the comparison understates the fix rather than
flattering it. The recovery path is covered deterministically instead by
`scripts/test-retry.js` (39 assertions against a scripted local server).

#### Two things the live API taught us that docs did not

1. **Google sends no `Retry-After` header.** It puts the wait in the JSON body,
   as a `google.rpc.RetryInfo` detail (`"retryDelay": "18s"`) and in prose
   ("Please retry in 18.852153328s."). The first version of this fix ignored
   both and used computed backoff — it spent all three attempts inside 7 s while
   the limiter wanted 19 s, i.e. the retry was pure added latency on a
   guaranteed failure. Now parsed, header first, body second.
2. **A server hint is a floor, not a ceiling.** Under sustained rate limiting
   Gemini alternates real waits (`"38s"`) with `"retryDelay": "0s"`. Obeying the
   0 literally burned attempts 2 and 3 in the same millisecond. The wait is now
   `max(hint, computed backoff)`. Regression-tested.

#### Fixed — the free Gemini tier is 20 requests/day, not 250

Measured directly against the API on 2026-08-02, on both `gemini-2.5-flash` and
`gemini-2.5-flash-lite`:

```
quotaId:    GenerateRequestsPerDayPerProjectPerModel-FreeTier
quotaValue: 20
```

Verified by sending one grounded and one plain call: **the cap applies to both**,
so it is not a grounding surcharge. The README promised 250/day and ~1000/day;
those numbers were true when written and are not any more. README and the
`gemini.js` header now carry the measured figure, dated, with a pointer to
re-measure rather than trust it.

Consequence for the run loop: Google attaches a *short* `retryDelay` (33 s) even
to a spent **daily** quota, which made every worker sleep a minute for nothing.
The layer now reads the `QuotaFailure` violation instead of the delay, and fails
a per-day exhaustion immediately with an actionable message. Wall time on an
exhausted key went from ~60 s of sleeping per call to **0.4 s** for the whole run.

⚠️ This is a live product concern, not just a doc fix: a free-key user gets **20
citation records per day**, which is thin ground for a "$0 to start" pitch. Worth
revisiting alongside the shared-key decision.

#### Tooling

- `scripts/test-retry.js` (`npm run test:retry`) — 39 assertions against a
  scripted localhost server: real 503 recovery, no-retry-on-401, Retry-After,
  Google's body hints, the `"0s"` regression, per-day fast-fail, per-attempt
  timeout, connection refused, sleep budget. No network, no API keys.
- `scripts/bench-retry.js` (`npm run bench:retry`) — hits the live API N times at
  the Actor's real concurrency and reports the success rate. `--no-retry`
  reproduces the pre-fix adapter for A/B. Keep N small: 20/day/model.

### \[0.1.3] — 2026-05-07

#### Added — Long-form README for Apify Store SEO

- **Comparison table** vs Otterly, Profound, Peec AI, Semrush AI Toolkit, Ahrefs Brand Radar — long-tail "Otterly alternative", "Profound alternative" keyword coverage.
- **Stability score visualisation** ([screenshot](https://api.apify.com/v2/key-value-stores/DxruSC8pQlvOevATQ/records/stability-score.png)) — 5-sample bar chart from real Phase 3 data showing custify.com (1.00), encharge.io (0.80) down to memberspace.com (0.20). Documents the recommended `stability >= 0.5` filter.
- **Delta-mode visualisation** ([screenshot](https://api.apify.com/v2/key-value-stores/DxruSC8pQlvOevATQ/records/delta-diff.png)) — gained / lost domains per query, week-over-week.
- **Engine comparison visualisation** ([screenshot](https://api.apify.com/v2/key-value-stores/DxruSC8pQlvOevATQ/records/engine-compare.png)) — same query on Gemini / ChatGPT / Perplexity / Claude side-by-side, with FREE TIER vs BYOK badges and per-engine cost.
- **Five use-case personas**: agency SEO lead, in-house SEO at B2B SaaS, PR/comms team, indie SEO consultant, Hungarian/German/French/Polish B2B brand. Each with concrete numeric ROI.
- **FAQ section** — 14 questions covering: engine selection, API key safety, manual ChatGPT comparison, scraping vs API, Google AIO absence, multilingual support, CSV export, scheduling, market analysis, model upgrades, paid Gemini tiers, free pricing window.
- **Troubleshooting section** — 7 documented issues with causes and resolutions (vertexaisearch redirects, free-tier quota, non-determinism, missing keys, empty datasets, 503s, CDN cache).
- **Pricing breakdown table** — three pricing scenarios with concrete dollar figures.
- **Table of contents** at top.

#### Tooling

- `scripts/make-extra-screenshots.py` — renders the 3 new screenshots (stability bars, delta gained/lost, engine comparison).
- `scripts/upload-screenshots.js` — extended to upload all 6 PNGs (icon + 5 screenshots) idempotently.

#### Notes

README grew from 184 lines / 1,500 words to 435 lines / 3,651 words. All new content is Apify-Store-SEO-targeted long-tail keyword coverage. No code changes — actor logic identical to v0.1.2.

### \[0.1.1] — 2026-04-29

#### Added — Store listing visuals (README inline images)

- **Hero image** at the top of the README showing the *real cloud-run output* of the same "best CRM for small business" intent in EN / DE / FR / HU. The German prompt surfaces 5 of 7 sources from `.de/.at/.ch` TLDs, the Hungarian one 3 of 7 from `.hu`, the English one 0 of 11 — making the multilingual edge legible at a glance.
- **Output-detail image** in the *Output* section showing a single citation record from the Apify cloud run `sIXsrVM5qKzAHY8HP` — query, engine, citation count, top sources, and the brand-match table with all three tracked brands cited.
- **Branded actor icon** (512×512 PNG) — indigo→cyan gradient with a centered crosshair / target ring, mint "AI" wordmark, and a "GEO" chip below. Replaces the generic Store gradient; signals the tracking + AI-search category at a glance.
- All three assets are stored in a named, public Apify key-value store (`ai-search-visibility-tracker-marketing`) so they don't expire and the README CDN-proxies them via `images.apifyusercontent.com`.

#### Tooling — repeatable visual generation

- `scripts/make-icon.py` — generates `marketing/icon-512.png` and `marketing/icon-128.png` with the branded design. Re-run on each redesign.
- `scripts/make-screenshots.py` — renders both README screenshots from real captured run data (Phase 2 multilingual run + cloud run `sIXsrVM5qKzAHY8HP`). Glyph-independent: drawn check marks (line segments) replace ✓ to avoid font-fallback rendering issues.
- `scripts/upload-screenshots.js` — idempotent uploader that finds (or creates once) the named public KVS and PUTs each PNG.

#### Notes

No code changes in this release — purely Store-listing polish. The actor itself is bit-for-bit identical to v0.1.0; the new build only ships an updated README.

***

### \[0.1.0] — 2026-04-29

#### Initial scaffold — $0 build

First runnable version. Validates the core thesis: a multilingual GEO tracker can run on the Apify platform without the author paying for any third-party API.

##### Engines (all share the same return shape)

- **Gemini 2.5 Flash** with `google_search` grounding — works on Google AI Studio's **free tier** (10 RPM, 250 RPD). Citations are returned as structured `groundingChunks` (URI + title + snippet).
- **OpenAI** Responses API with `web_search_preview` tool (gpt-4o-mini-search-preview) — BYOK, ~$0.025/query.
- **Perplexity Sonar** chat completions — BYOK, ~$0.005–0.015/query.
- **Anthropic Claude** Messages API with `web_search_20250305` tool — BYOK, ~$0.01–0.02/query.

##### Pure-JS utils with full unit coverage

- `canonicalHost` — strips scheme/path/www, lowercases.
- `registrableDomain` — heuristic eTLD+1, handles `.co.uk`, `.com.br`, `.co.jp`, etc. without a Public Suffix List dependency.
- `extractUrlsFromText` + `normaliseCitations` — merges engine-supplied structured citations with URLs mined from the answer text, dedup by canonical host+path.
- `detectBrandCitation` — flags brand mentions via citation match OR whole-word brand-token match in the answer text.
- `citationShare` — share-of-voice across tracked domains.
- `stabilityAcrossSamples` — fraction of N samples in which each domain was cited (filters one-shot hallucinations when `samplesPerQuery > 1`).
- `diffRuns` — gained/lost domains per query vs a prior dataset (delta mode).
- `pMap` — bounded-concurrency async map, no external dep.

26 offline assertions (`npm test`) cover all of the above. Pass on first run.

##### Orchestrator (`src/main.js`)

- Validates input, drops engines with missing keys (warns instead of failing).
- Builds a work plan of `(query × engine × sample)` tasks.
- Runs them with `maxConcurrency` (default 4 — stays under Gemini free-tier 10 RPM).
- Writes per-call `citation` records, plus optional `stability` (when `samplesPerQuery > 1`), `delta` (when `previousRunDatasetId` set), and a final `summary` record.
- Mirrors the `summary` into the run's KV store under key `SUMMARY`.

##### Input schema

- `queries`, `brandDomains` are required; `competitorDomains`, language, engines and BYOK keys are optional.
- 24 language enum (en, de, fr, es, it, pt, nl, hu, pl, cs, sk, ro, bg, hr, tr, ja, ko, zh, ru, uk, sv, no, da, fi).
- `samplesPerQuery` (1–5) for stability scoring.
- `previousRunDatasetId` for delta mode.
- Per-engine model overrides (`geminiModel`, `openaiModel`, `perplexityModel`, `anthropicModel`).

##### Tooling

- `scripts/test-utils.js` — offline assertions for `utils.js`. No network, no keys.
- `scripts/smoke.js` — live-network smoke against whichever engines have a key set in env.
- `scripts/test-comprehensive.js` — 6-phase live integration runner with billing-aware error handling and retry on 503.
- `scripts/cloud-run.js` — REST-API trigger + status poll, used to verify the actor on Apify cloud.
- `scripts/update-actor-metadata.js` — applies Store metadata (categories, seoTitle/description, exampleRunInput) idempotently.

##### Why "BYOK + free Gemini tier"

Research showed that the cheapest viable competitor (Otterly Lite at $29/mo) gates users at 15 prompts/month. Profound has no free trial. Peec AI starts at €89/mo. The whole category locks out indie SEOs and small agencies. By passing the API-key cost transparently to the user, we charge **only** Apify pay-per-event for the orchestration layer, and the entry point is genuinely $0/month with Gemini's free tier.
