# Changelog of AI Brand Visibility Tracker: LLM Mentions & Share of Voice (`humble-echidna/ai-visibility`) Actor

- **URL**: https://apify.com/humble-echidna/ai-visibility/changelog.md
- **Full Actor documentation**: https://apify.com/humble-echidna/ai-visibility.md

## Changelog

Versions follow MAJOR.MINOR.PATCH (`src/version.py`); Apify shows MAJOR.MINOR from `.actor/actor.json`.
Every run logs its version and records it in the `RUN_STATS` key-value record.

> **About future changes in output:** models answer differently between runs, and a provider can change a model
> behind the same id. That's what the samples and the change since the last run are for.

### 0.1.5 (2026-10-16)

- A run that returns nothing (every input bad, private, blocked or unreachable, or an empty input) now ends
  SUCCEEDED with an empty dataset and the reason as its status message and in the log, instead of FAILED. Only an
  unexpected error still fails the run. `RUN_STATS` keeps the reason as before.
- No change to output fields or price.

### 0.1.4 (unreleased)

- **Fixed: one flagged question no longer stops the run.** When the model provider's moderation flags a question or
  answer, OpenRouter answers with HTTP 403, the same status as a refused token, and the run stopped there and
  failed. That check now gets a row with the reason ("the model provider's moderation flagged this question or
  answer (...)"), uncharged, and the other checks carry on. A refused token or account still stops the run at once.
- A model that declines to answer now says so, with its reason ("the model declined to answer: ..."), instead of
  "the model returned an empty answer". Still a row with `valid: false`, uncharged.

### 0.1.3 (2026-10-03)

- The history store is opened through the shared helper below; its name is back to `ai-visibility-history`, with
  `humble-echidna-ai-visibility-history` (0.1.2's name) as the fallback, so runs since 0.1.2 keep their history.
- A run no longer fails when the store it remembers earlier runs in can't be opened (403: the account already has
  a store of that name that this actor didn't create). It falls back to `humble-echidna-<name>`, and if that is
  refused too it runs without memory for that run. Users' existing memory is untouched: the usual name is tried first.

### 0.1.2 (2026-10-03)

- Fixed: runs by other users could fail at once with "Insufficient permissions for the key-value store" (Apify's
  daily test did, so the actor was flagged "under maintenance"). The history store now has a name of its own,
  `humble-echidna-ai-visibility-history`, and a store that still can't be opened no longer fails the run: it runs
  without the change since the last run and says so in the log and `RUN_STATS.historyUnavailable`. History from
  earlier runs starts over once.

### 0.1.1 (2026-10-02, private)

- A model left out of one run keeps its history: the next run that asks it again compares with its last known
  rate (found on Apify: a run on another model in between left `before` empty).
- **Web search tested through Apify's OpenRouter proxy, 2026-10-02:** neither OpenRouter's `web` plugin nor the
  `:online` model suffix searched (no citations, no search results in the token counts), so `webSearch` stays out
  of the input. `perplexity/sonar` does search on its own, and its citations come through as `citedUrls`.

### 0.1.0 (2026-10-02, private)

First release: whether AI models name your brand when buyers ask them questions.

- **Input:** `brand` and `competitors` (other names after `|`, whole-word, case-insensitive; a domain among them
  marks links to it), `prompts` (up to 100), `models` (up to 5 OpenRouter ids, default `openai/gpt-4.1-mini`),
  `samples` (1-5), `onlyChanges`, `maxChecks`. A prompt that names the brand itself is flagged in the log and
  `RUN_STATS.promptsNamingBrand`.
- **Checks:** every prompt × model × sample, sample round first (a capped run covers every prompt and model once
  before repeating). Each check is two calls through Apify's OpenRouter proxy with the run's own token: the question
  as a bare user message (no system prompt), the answer capped at 1,000 tokens; then a strict structured-output call
  on `openai/gpt-4.1-mini` that lists every brand the answer names, in its order, validated against the schema here.
- **Rows:** `brandMentioned`, `brandRank` (among every brand named; a tracked brand the list missed is placed where
  the text first names it), `competitorsMentioned`, `competitorRanks`, `brandsNamed`, `citedUrls` (citations plus
  links written in the answer), `brandCited`, `answer`, `answerTruncated`, `changed`, `valid`, `errors`, tokens.
- **Summary row (free):** mention rate overall and per model, each sample round's rate and their spread, average
  rank, share of voice among the tracked brands, the 20 brands named most, and `changes` against the last run of
  the same brand + prompt set (named store `ai-visibility-history` in the user's account, one record per tracker;
  adding a competitor or a model keeps the history). `onlyChanges` leaves unchanged checks out of the dataset (still
  summarised and charged).
- **Charging:** rows are free; `answer-checked` is charged once per valid check, after its row is in the dataset
  (or when it's left out as unchanged). Failed calls, empty answers and unreadable brand lists are rows with the
  reason, uncharged. `maxChecks` and the maximum cost per run are honoured before a check is asked: a slot is
  claimed per check and given back when it isn't valid. A proxy that refuses the run (401/402/403) stops it at once.
- **Web search:** the code can ask for OpenRouter's web plugin (`webSearch`), but it isn't offered in the input
  until it's confirmed to pass through Apify's proxy.
- **Sources and terms (ACTOR-CHECKLIST section 0):** nothing is scraped. The model endpoint is Apify's own,
  documented for actors (https://docs.apify.com/sdk/python/docs/guides/ai-agents, read 2026-09-29 for ai-extract:
  "The token usage is billed against the Apify account running the Actor, so no provider API key is required"). No
  credential or identity of ours is used. No Google AI Overviews and no consumer-app scraping (legal hold). Brands,
  not people: said in the README and the `brand` input description (it can't be enforced).
