# AI Search Visibility Tracker (ChatGPT, Perplexity, Gemini) (`datafeed/ai-visibility-tracker`) Actor

Is your brand mentioned, ranked and cited in AI search answers? Asks OpenAI, Perplexity, Gemini and Anthropic through their official APIs with your own keys. Share of voice vs competitors; monitor mode reports only changes plus a weekly summary.

- **URL**: https://apify.com/datafeed/ai-visibility-tracker.md
- **Developed by:** [Benjamin Cojocaru](https://apify.com/datafeed) (community)
- **Categories:** SEO tools, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## AI Search Visibility Tracker (ChatGPT, Perplexity, Gemini, Claude APIs)

Find out whether AI search **mentions your brand, where it ranks it against competitors, and whether it cites your website**. You give the questions your buyers ask ("What is the best CRM for a small business?"); the Actor asks each AI engine through its **official API with your own API key** and records, per prompt and engine: brand mentioned or not, rank among the brands named, tone, cited URLs, whether your domain is cited, and which competitors appear. A summary row gives **share of voice per engine**.

Schedule it in **monitor mode** and it remembers the last result for every prompt x engine, then outputs only what changed (brand dropped from an answer, citation lost, rank moved, a new competitor showing up), plus a **weekly summary**.

**Pricing at a glance (our fee, on top of your own API cost):** `prompt-checked` per prompt x engine answered and stored (suggested $0.01), `change-detected` per monitor row with changes (suggested $0.02), `summary-generated` per summary row (suggested $0.05). Failed calls are free. Try it free first with **dry run** (no keys, no calls).

### Official APIs only, your keys

| Engine (`engines`) | API | Web grounding | Key input |
|---|---|---|---|
| `openai` | OpenAI Responses API (`/v1/responses`) | `web_search` tool | `openaiApiKey` |
| `perplexity` | Perplexity Agent API (`/v1/agent`, preset `fast` by default) | preset's web search | `perplexityApiKey` |
| `gemini` | Google Gemini API (`generateContent`) | Grounding with Google Search | `geminiApiKey` |
| `anthropic` | Anthropic Messages API | `web_search` server tool | `anthropicApiKey` |

The Actor never opens chatgpt.com, perplexity.ai or any other chat website, and never logs in anywhere. API answers are close to, but not identical with, what a person sees in the consumer apps (those add personalization, location and their own system prompts); they are consistent from run to run, which is what trend tracking needs.

**Your keys** are secret inputs (stored encrypted by Apify), sent only in a request header to that provider's API host, and removed from every error message. They are never written to the dataset, the SUMMARY or the monitor store. **You pay each provider for your own calls** (roughly $0.005-0.04 per call depending on engine and model; web-search and grounding fees apply, see each provider's pricing). `SUMMARY.apiCalls` and `SUMMARY.apiTokens` show what was used per engine so you can check your bill.

Model defaults: `gpt-5-mini`, Perplexity preset `fast`, `gemini-2.5-flash`, `claude-haiku-4-5` (change them with `openaiModel`, `perplexityModel`, `geminiModel`, `anthropicModel`). Please check that `gpt-5-mini` still exists and supports `web_search` on your account: OpenAI's guide now recommends newer models, and the defaults have not been tested with a real key. `perplexityModel` takes a preset (`fast`, `low`, `medium`, `high`, `xhigh`) or a direct `provider/model` (sent with the `web_search` tool). A `sonar*` value uses the legacy Sonar Chat Completions API, which Perplexity is retiring in favour of the Agent API (check their docs for the date). That route is kept only as a fallback while Perplexity still serves it. Anthropic web search is on by default; if an admin disabled it for your organization, the engine stops with `web_search_disabled` (free).

### Input

```json
{
  "brandName": "Acme",
  "brandAliases": ["Acme Cloud"],
  "brandDomains": ["acme.com"],
  "competitors": ["Globex | globex.io", "Initech | Initrode | initech.com"],
  "prompts": ["What is the best CRM for a small business?"],
  "seedTopics": ["CRM software"],
  "variantsPerTopic": 3,
  "engines": ["openai", "perplexity", "gemini"],
  "openaiApiKey": "<your key>",
  "perplexityApiKey": "<your key>",
  "geminiApiKey": "<your key>",
  "mode": "check",
  "confirmRuns": 2,
  "minRankChange": 2,
  "discoverBrands": false,
  "dryRun": false
}
```

- **Prompts:** write them as a buyer would, without your brand name. `seedTopics` turns each topic into up to 8 fixed question templates ("What are the best {topic}?", "Which {topic} have the best reviews?", ...); the same text every run so history stays comparable. Up to 100 prompts per run in total.
- **Competitors:** `Name`, or `Name | alias | domain.com`. Entries that look like domains are used to detect that competitor's citations.
- **Matching:** whole words, case-insensitive (`Acme` matches "acme" but not "Acmeville"); your domains also count as mentions when written in the answer.
- **Schedule:** use Apify Console > Schedules (e.g. daily or weekly) with `mode: monitor`. The Actor cannot schedule itself.

### Output

One row per prompt x engine (check mode, or monitor rows with changes):

```json
{
  "type": "answer",
  "promptId": "3f1c0a9b2d4e",
  "prompt": "What is the best CRM for a small business?",
  "engine": "perplexity",
  "model": "openai/gpt-6-luna",
  "brand": "Acme",
  "status": "ok",
  "brandMentioned": true,
  "brandRank": 2,
  "brandsMentionedInOrder": ["Globex", "Acme", "Initech"],
  "brandsMentionedCount": 3,
  "sentiment": "positive",
  "brandCited": true,
  "brandCitedUrls": ["https://www.acme.com/crm"],
  "citedUrls": ["https://www.acme.com/crm", "https://globex.io/pricing", "https://review-site.example/best-crm"],
  "citedDomains": ["acme.com", "globex.io", "review-site.example"],
  "competitorsMentioned": ["Globex", "Initech"],
  "competitorsCited": ["Globex"],
  "answerText": "For small businesses, Globex is the most popular ... Acme is a great choice if ...",
  "checkedAt": "2026-10-14T08:00:00+00:00",
  "error": null
}
```

- `brandRank`: position of your brand among the tracked brands (yours plus competitors), by first mention in the answer. `null` when not mentioned.
- `sentiment` (optional, `analyzeSentiment`): `positive`, `negative`, `mixed` or `neutral`, from a small word list applied to the sentences that mention your brand ("not reliable" counts as negative). It is a hint, not a verdict, and costs no extra API call.
- Citations: OpenAI `url_citation` annotations, the Perplexity `search_results` items referenced by `[n]` markers in the answer (all of them if the answer has no markers), Gemini grounding chunks, Anthropic cited search results, plus plain URLs in the answer. Gemini cites through `vertexaisearch.cloud.google.com` redirect links whose title is the source domain; that domain is used for `citedDomains` and `brandCited`.
- With `discoverBrands: true`, the summary row also has `discoveredBrands`: the top 15 capitalised, brand-like names that answers mentioned but you did not track, with the number of answers and engines (a heuristic for English text: common words and all-caps acronyms are skipped, so expect some noise). Use it to find competitors to add. No extra charge; never in the per-answer rows.
- A summary row (`"type": "summary"`) ends each check run: per engine `answers`, `brandMentionRate`, `brandCitationRate`, `averageRank`, `shareOfVoice` (your mentions / all tracked-brand mentions) and `shareOfVoiceByBrand`.
- Failed checks get `status: "error"` with `error.code` (`api_key_rejected`, `api_no_credit`, `bad_model`, `web_search_disabled`, `truncated_answer`, `empty_answer`, `rate_limited`, `api_unavailable`, `invalid_request`, ...) and are free. A rejected key (including Gemini's HTTP 400 "API key not valid"), no credit, an unknown model or preset, a disabled web search, or 3 empty answers in a row stops that engine for the rest of the run (the others continue). Only rate limits, 5xx errors and network failures are retried (twice, honouring `Retry-After`). Empty or malformed answers are not retried, so you never pay the provider 3 times for one error. An answer the provider cut off (OpenAI `incomplete`, Anthropic `max_tokens`/`pause_turn`, Gemini `MAX_TOKENS`, Perplexity not `completed`) is never counted as a real answer: it is a free `truncated_answer` row, so a cut-off answer cannot create a false "brand lost" alert.

**Run-summary row (free).** When a run produces no other row (for example a monitor run where nothing changed, or a run stopped at once by your maximum charge or the run timeout), the Actor adds one row `{"type": "run_summary", "status": "run_summary", "message": "...", "stopReason": ..., "warnings": [...], "summary": {...}}` (`summary` is the same as the `SUMMARY` record), so the dataset is never empty. It is never charged. Skip it with `type != "run_summary"`.

### Monitor mode (the reason to schedule it)

Set `"mode": "monitor"` and create a schedule. Each run:

1. asks every prompt x engine as above;
2. reads `state-<engine>-<promptId>` from the **named** key-value store `monitorStoreName` (default `ai-visibility-tracker`), compares, and writes the new state. The state is small: mentioned, rank, cited, competitor names, tone, cited domains. **No answer text is stored.**
3. appends to `history-<engine>-<promptId>` (last 100 runs: time, mentioned, rank, cited) for charts;
4. outputs a row only when something changed (and on the first run, as a baseline). `emitUnchanged: true` gives a row every time.

Each row then has `monitor`: `firstRun`, `previousCheckAt`, `previousRank`, `alertLevel` (`none`, `info`, `warning`, `critical`) and `changes`, most severe first:

| Change kind | Alert |
|---|---|
| `mention_lost` (brand no longer in the answer) | critical |
| `citation_lost`, `rank_down`, `competitor_appeared`, `sentiment_turned_negative` | warning |
| `mention_gained`, `citation_gained`, `rank_up` | info |

**You do not pay for flicker.** AI answers vary between runs, so by default a mention, citation, rank or new-competitor change is reported (and charged as `change-detected`) only once it is seen on **2 consecutive runs** (`confirmRuns`, 1-5; 1 = report at once). On the first sighting nothing is output; if the row is output anyway (`emitUnchanged`) the field is listed in `monitor.pendingConfirmation`. A change that does not repeat is dropped silently, and the stored state keeps the last confirmed value. Rank moves smaller than `minRankChange` places (default 2) are ignored, and a rank move must keep the same direction on both runs. A competitor newly appearing goes through the same check (it must appear on 2 runs in a row; a competitor that disappears is dropped silently and never charged). A tone turning negative goes through the same check too, so a tone that alternates between neutral and negative is never charged. `history-*` always records what was actually observed on each run, flicker included, for your own charts.

**Weekly summary:** on the first run and then every `summaryEveryDays` (default 7) the run ends with a summary row, which also has `changeSincePreviousSummary` (difference in mention rate, citation rate, share of voice and average rank per engine) and `changesSincePreviousSummary` (number of changed checks in between).

**One schedule per monitor store.** A run takes a `LOCK` record and a second run on the same store fails at once while the lock is fresh (older than 6 hours = crashed run, taken over). Use one `monitorStoreName` per brand or client.

#### Alerts (Apify integrations; nothing is sent by this Actor)

Actor > Integrations > HTTP webhook (Slack, Zapier, Make) or email on `ACTOR.RUN.SUCCEEDED`, then read the run's dataset and filter on `monitor.alertLevel`. In monitor mode an empty dataset means nothing changed.

### Dry run

`"dryRun": true` (pre-ticked in the Console form) needs no keys, makes no API call, charges nothing and never writes the monitor store: built-in sample answers in each API's real response format go through the full analysis, so you see exactly what the output looks like. Untick it after adding your keys; a warning appears in SUMMARY if keys were given while dry run was on.

### Pricing

Pay per event, our fee only; your AI-provider API usage is billed to you by the provider.

- `prompt-checked`: once per prompt x engine answer that was stored (a row in check mode; the state record in monitor mode, charged after it was saved).
- `change-detected`: once per monitor row with changes, charged with that row (in addition to `prompt-checked`).
- `summary-generated`: once per summary row.
- Free: failed calls, skipped engines, error rows, dry runs. Before every API call the Actor checks that your maximum charge per run still covers that check (and the change and summary it may produce), so your API money is never spent on an answer that cannot be delivered.

**Example:** 25 prompts x 3 engines (75 checks per run).

| Schedule | Checks | Confirmed changes (typical, after the 2-run filter) | Summaries | Our fee per month |
|---|---|---|---|---|
| daily | 75 x 30 = $22.50 | about 1-3 per day = $0.60-1.80 | 4-5 x $0.05 | about $23-25 |
| weekly | 75 x 4.3 = $3.23 | about 2-5 per week = $0.17-0.43 | 4-5 x $0.05 | about $3.60-3.90 |

Without the flicker filter (`confirmRuns: 1`, `minRankChange: 1`) a daily run of 75 noisy answers can easily produce 10+ change rows per day (about $6/month more), most of them noise. Plus your own API cost in both cases.

**Why this price.** Dedicated AI-visibility SaaS tools cost far more and are subscription-locked: Profound is sales-led with an entry price around $499/month, Peec AI starts around EUR 89/month for a small prompt set, and Ahrefs Brand Radar's AI coverage is a paid add-on to an Ahrefs plan (figures as last seen, check their sites). The existing Apify option, `apify/chatgpt-search-scraper` (rated 3.7 with about 249 users in 30 days at the time of writing; not re-checked), scrapes the ChatGPT website, which breaks when the UI changes and covers one engine. This Actor uses four official APIs, pays per check with no subscription, and keeps your data in your own Apify account.

### Good behaviour and privacy

- Calls go only to the four official API hosts above, one at a time, with `delayBetweenCallsMs` (default 500 ms) between them.
- No personal data is collected: the input is brand names, domains and questions; the output is AI answers about brands.
- Your keys never appear in URLs, logs, rows, SUMMARY or the store.

### Limits

100 prompts, 25 competitors and 10 aliases/domains per run. Rank and mention use exact names: misspellings or brand names that are ordinary words (e.g. "Notion", "Monday") can over- or under-count; add aliases, or use a distinctive form ("monday.com"). Sentiment is rule-based English only. API answers can differ from the consumer apps. Provider request/response formats were implemented from their public documentation; if a provider changes its API, that engine returns error rows (free) until the Actor is updated.

# Actor input Schema

## `brandName` (type: `string`):

The brand to track, exactly as people write it (e.g. Acme). Matched as a whole word, case-insensitive.

## `brandAliases` (type: `array`):

Other names of the brand or its products (up to 10), e.g. Acme Cloud.

## `brandDomains` (type: `array`):

Your own domains (up to 10), e.g. acme.com. Used to detect when an AI answer cites your site (subdomains count).

## `competitors` (type: `array`):

Up to 25. One per line: 'Name' or 'Name | alias | competitor.com' (entries that look like domains are also used to detect citations).

## `prompts` (type: `array`):

Questions buyers ask AI search, without naming your brand, e.g. 'What is the best web scraping platform?'. Up to 100 prompts per run in total (with generated ones).

## `seedTopics` (type: `array`):

Optional. Each topic (e.g. 'web scraping tools') is turned into variantsPerTopic fixed question templates, the same every run so history stays comparable.

## `variantsPerTopic` (type: `integer`):

How many template questions per seed topic.

## `engines` (type: `array`):

Each is asked through its official API with your own key: openai (Responses API + web search), perplexity (Agent API), gemini (Gemini API + Google Search grounding), anthropic (Messages API + web search).

## `openaiApiKey` (type: `string`):

Your own key (platform.openai.com). Needed when 'openai' is selected. Secret input: sent only in a request header to api.openai.com, never stored or shown. You pay OpenAI for the calls.

## `perplexityApiKey` (type: `string`):

Your own key (perplexity.ai API settings). Needed when 'perplexity' is selected. Secret input, sent only to api.perplexity.ai.

## `geminiApiKey` (type: `string`):

Your own key (Google AI Studio). Needed when 'gemini' is selected. Secret input, sent only to generativelanguage.googleapis.com. Grounding with Google Search may be billed by Google.

## `anthropicApiKey` (type: `string`):

Your own key (console.anthropic.com). Needed when 'anthropic' is selected. Web search is on unless an admin disabled it for your organization. Secret input, sent only to api.anthropic.com.

## `openaiModel` (type: `string`):

A model that supports the web_search tool. Default: gpt-5-mini (check that it is still offered on your account).

## `perplexityModel` (type: `string`):

An Agent API preset (fast, low, medium, high, xhigh) or a direct provider/model such as openai/gpt-5.5. Default: fast (the docs map sonar to fast). sonar\* values use the legacy Sonar API (being retired), fallback only.

## `geminiModel` (type: `string`):

Default: gemini-2.5-flash.

## `anthropicModel` (type: `string`):

Default: claude-haiku-4-5.

## `mode` (type: `string`):

check = one row per prompt x engine plus a share-of-voice summary row. monitor = compare with the previous run (named key-value store) and output only changes, plus a summary row every summaryEveryDays. Schedule monitor runs in Apify Console, one schedule per store.

## `analyzeSentiment` (type: `boolean`):

Tag the tone of the sentences that mention the brand (positive, negative, mixed, neutral) with a small word list. No extra API call.

## `includeAnswerText` (type: `boolean`):

Add the AI answer (first 4,000 characters) to each row. Never stored in the monitor store.

## `discoverBrands` (type: `boolean`):

List the capitalised brand-like names that AI answers mention but you did not track (top 15, heuristic) in the summary row. No extra charge.

## `minRankChange` (type: `integer`):

AI answers vary from run to run: a rank move smaller than this is ignored (default 2 places).

## `confirmRuns` (type: `integer`):

A mention, citation or rank change is reported (and charged) only after it was seen on this many consecutive runs; a one-off flicker is ignored. 1 = report at once.

## `summaryEveryDays` (type: `integer`):

A summary row (share of voice, mention and citation rates per engine, change since the previous summary) is output on the first run and then once per this many days.

## `emitUnchanged` (type: `boolean`):

Off: unchanged checks produce no dataset row (their state and history are still updated).

## `monitorStoreName` (type: `string`):

Named store that keeps the state between runs. Use one per brand/project and one schedule per store.

## `delayBetweenCallsMs` (type: `integer`):

Calls are made one at a time; this pause keeps you under low rate limits.

## `dryRun` (type: `boolean`):

Uses built-in sample answers instead of calling any API, charges nothing and never writes the monitor store. Use it to see the output format. Pre-ticked in the Console form so a first try is free: untick it after adding your keys.

## Actor input object example

```json
{
  "brandName": "Apify",
  "brandDomains": [
    "apify.com"
  ],
  "competitors": [
    "Bright Data | brightdata.com",
    "ScrapingBee | scrapingbee.com"
  ],
  "prompts": [
    "What is the best web scraping platform?"
  ],
  "variantsPerTopic": 3,
  "engines": [
    "openai",
    "perplexity",
    "gemini"
  ],
  "mode": "check",
  "analyzeSentiment": true,
  "includeAnswerText": true,
  "discoverBrands": false,
  "minRankChange": 2,
  "confirmRuns": 2,
  "summaryEveryDays": 7,
  "emitUnchanged": false,
  "monitorStoreName": "ai-visibility-tracker",
  "delayBetweenCallsMs": 500,
  "dryRun": true
}
```

# Actor output Schema

## `results` (type: `string`):

One row per result (see the dataset schema for fields).

## `summary` (type: `string`):

SUMMARY record: counts, warnings, stop reason and what was charged.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "brandName": "Apify",
    "brandDomains": [
        "apify.com"
    ],
    "competitors": [
        "Bright Data | brightdata.com",
        "ScrapingBee | scrapingbee.com"
    ],
    "prompts": [
        "What is the best web scraping platform?"
    ],
    "dryRun": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("datafeed/ai-visibility-tracker").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "brandName": "Apify",
    "brandDomains": ["apify.com"],
    "competitors": [
        "Bright Data | brightdata.com",
        "ScrapingBee | scrapingbee.com",
    ],
    "prompts": ["What is the best web scraping platform?"],
    "dryRun": True,
}

# Run the Actor and wait for it to finish
run = client.actor("datafeed/ai-visibility-tracker").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "brandName": "Apify",
  "brandDomains": [
    "apify.com"
  ],
  "competitors": [
    "Bright Data | brightdata.com",
    "ScrapingBee | scrapingbee.com"
  ],
  "prompts": [
    "What is the best web scraping platform?"
  ],
  "dryRun": true
}' |
apify call datafeed/ai-visibility-tracker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,datafeed/ai-visibility-tracker"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/Q7YPlnWXnJgrCFkPR/builds/STT5QupvL5bbfefOe/openapi.json
