# AI Visibility Check: can ChatGPT cite your site, and does it? (`operstack/ai-visibility-check`) Actor

Two answers in one run. Scores any site out of 100 on whether AI assistants can read, quote and date it. Then asks the questions your buyers type and reads what Google AI Overview, Perplexity and ChatGPT answer, to see whether you are named or a rival is. One shareable report per run.

- **URL**: https://apify.com/operstack/ai-visibility-check.md
- **Developed by:** [Maksim Shchegolev](https://apify.com/operstack) (community)
- **Categories:** SEO tools, AI, Marketing
- **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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## AI Visibility Check: can ChatGPT cite your site, and does it?

Two questions about the same website, answered in one run.

1. **Can an AI assistant read you?** The site is scored out of 100 on whether ChatGPT, Perplexity, Claude, Copilot and Google's AI answers can reach it, understand what it is, quote it and date it.
2. **Does an AI assistant name you?** Give the questions your buyers actually type, and the actor collects the answers Google's AI Overview, Perplexity and ChatGPT give, then checks whether your brand appears in them, and which rivals appear instead.

Most tools do one or the other. On its own, neither is useful: being told "the AI never mentions you" does not say why, and being told "your llms.txt is missing" does not say whether it costs you anything. Together they say which of the two is the thing to fix first, and the actor writes that verdict in plain words on every row.

Public signals only. No login, nothing written to the sites it reads.

### Half one: can an AI read you

| Area | Out of | What it reads |
|---|---|---|
| Can AI crawlers read it | 25 | robots.txt, evaluated separately for fourteen AI crawlers and fetchers (OAI-SearchBot, ChatGPT-User, GPTBot, PerplexityBot, ClaudeBot, Claude-SearchBot, Google-Extended, Bingbot, Applebot-Extended, CCBot, DuckAssistBot and more) |
| Is there a map for agents | 15 | llms.txt: present, readable, pointing at the site's own pages |
| Is the entity clear | 20 | Organization, LocalBusiness, FAQPage and Article schema on the sampled pages |
| Is there something to quote | 25 | Answer-first opening paragraphs with a figure, page length, section structure, tables |
| Can it be dated and trusted | 15 | Publication and modification dates, phrases that name a source, a sitemap with lastmod |

Each site returns a score, a grade from A to E, the five area scores, every finding in plain words, the three fixes that move the score most, and a verdict per AI crawler.

### Half two: does an AI name you

Fill in **Questions your buyers ask** and the actor asks each engine you selected, reads the answer, and records:

- whether your brand name or your domain appears in it
- which of the rivals you named appear in it
- where your site sits in the Google top ten for the same question
- the answer text itself, so you can read what the AI actually told your buyer

| Engine | Needs a key from you | What it costs |
|---|---|---|
| Google AI Overview | No | Runs the Apify Google Search Scraper on your own account, so its charge lands on your Apify bill, roughly a third of a cent per question |
| Perplexity | Yes, your own Perplexity key | Whatever your Perplexity plan charges |
| ChatGPT (OpenAI) | Yes, your own OpenAI key | Whatever your OpenAI plan charges |

Keys are pasted into encrypted fields, used for the run and never written to the output.

### What you get back besides the numbers

- **A report page you can forward.** Every run writes a single self-contained HTML page to its storage: all the sites, their scores, the AI answers word for word, the rivals named instead of you, and the fixes in order. Open it in a browser or send it to a client as it is.
- **The change since last time.** Each run remembers one line per site, so the next one shows which way the score and the mention share moved, and when the previous check was. Turn it off with `trackChanges: false`.
- **Bot walls named for what they are.** A site behind a Cloudflare browser challenge is reported as exactly that, separately from a plain refusal, because it is the more expensive problem: an AI fetcher never solves a challenge, so every assistant is turned away from that site no matter how good its pages are.
- **Questions asked twice when Google stays silent.** Google serves an AI Overview inconsistently, so a question that comes back empty is asked once more before it is reported as unanswered, and unanswered questions are left out of the share instead of counted as a miss.

### Input

```json
{
  "urls": ["https://example.com", "competitor.com"],
  "questions": ["best property agency in phuket", "where to buy a condo in phuket"],
  "brand": "Example Group",
  "competitors": ["FazWaz", "CBRE"],
  "engines": ["google-ai-overview"],
  "countryCode": "th",
  "languageCode": "en",
  "samplePages": 3,
  "budgetSeconds": 20
}
```

- **urls**: one or more sites. A bare host works; the actor adds https and reads the homepage, robots.txt, llms.txt, the sitemap and the sampled pages.
- **questions**: leave empty and only the score is produced. Every question is asked once and the answer is reused for every site in the run, so checking yourself against five competitors costs no more than checking yourself.
- **brand**: how your company is written in ordinary text. Left empty, the domain is used, so `fazwaz.com` becomes `Fazwaz`. It applies to the first site in the list; the others are read off their own domains.
- **competitors**: names or domains. Every answer is checked for these too.
- **countryCode** and **languageCode**: the country and language the Google question is asked from. AI answers differ by country, so set the market you sell in.
- **samplePages**: content pages to read beyond the homepage (1 to 10, default 3).
- **budgetSeconds**: hard limit per site (5 to 120, default 20). Raise it for slow sites.

### Output

One dataset row per site:

```json
{
  "url": "https://example.com/",
  "host": "example.com",
  "brand": "Example Group",
  "score": 74,
  "grade": "B",
  "mentionRate": 25,
  "namedInAnswers": 1,
  "answersCollected": 4,
  "verdict": "Named in 25% of answers despite a weak site score. Fix access and structure and the share should rise.",
  "access": 25, "index": 15, "entity": 17, "content": 14, "trust": 3,
  "topFixes": ["0 of 3 sampled pages open with an answer-first paragraph...", "3 sampled pages expose no publication or modified date..."],
  "rivalsAhead": [{ "rival": "FazWaz", "answersNaming": 3 }],
  "questionsWithoutBrand": ["where to buy a condo in phuket"],
  "byEngine": { "google-ai-overview": { "asked": 2, "mentioned": 1 } },
  "answers": [{ "engine": "google-ai-overview", "query": "...", "answer": "...", "brandMentioned": false, "citedHosts": ["fazwaz.com"], "organicPosition": 4 }],
  "findings": [{ "area": "Can AI crawlers read it", "level": "pass", "text": "All 14 AI crawlers and fetchers are allowed." }],
  "crawlers": [{ "label": "ChatGPT search (OAI-SearchBot)", "kind": "search", "verdict": "allowed" }],
  "sampledPages": [{ "url": "...", "words": 875, "answerFirst": false, "dated": false, "schema": ["Organization"] }],
  "sitemap": { "found": true, "count": 41, "lastmod": true },
  "checkedAt": "2026-09-11T02:15:00.000Z"
}
```

Three ready views in the dataset tab: **Verdict** for the one-line answer per site, **Site scores** for the five areas, **AI answers** for who gets named instead of you. Export to CSV, Excel or JSON, or read it through the Apify API.

### Uses

- **Your own site, monthly.** Watch both numbers move after each fix.
- **You against your competitors.** Put every rival in `urls` and every rival name in `competitors`. One run shows who the AI can read and who it actually recommends.
- **A pitch that is hard to argue with.** Run a prospect's site on the questions their customers ask, and the output is the whole pitch: here is the answer your buyer got, here is who was named, here is why it was not you.
- **Before and after a migration.** A blocked crawler or a broken llms.txt shows up immediately.

### What it is not

It does not measure traffic, that is in your analytics. It does not log in, and sites behind a login or a bot wall cannot be scored. AI answers are not stable: the same question can be answered differently an hour later, so read a share across several questions, not a single yes or no.

### Billing

Pay per event: one charge per site scored, one per AI answer collected. Sites that are skipped or unreachable and engines that fail are never charged, and neither is a question no engine answered. The Google Search Scraper run sits on your own Apify account and is billed there.

Because the answers are collected once per run and then checked against every site in it, adding rivals costs nothing beyond their site score. Checking yourself against nine competitors on twenty questions is the price of twenty answers plus ten site scores, not of two hundred brand checks.

### Who made it

[OperStack](https://oper-stack.com): tools for content sites that have to rank, be cited and convert. The single-site version of the score runs free at [oper-stack.com/ai-visibility](https://oper-stack.com/ai-visibility/); this actor is the batch version with the citation half added. The open-source quality gates behind the fixes are at [github.com/oper-stack/gates](https://github.com/oper-stack/gates).

### Local development

```
npm install
npm run verify           # unit tests, then a real end-to-end run of the actor
apify run --input '{"urls":["https://example.com"]}'
```

`npm test` covers the scoring and the citation logic. `npm run smoke` runs `src/main.mjs` itself and checks the row, the verdict and the report page, because a unit test passes happily on an entry point that cannot execute. Run `npm run verify` before every push.

`src/ai-visibility.mjs` is vendored from `oper-stack-website/src/lib/ai-visibility.mjs`. Keep the two identical: both take `{ budgetMs, samplePages }`. `src/citations.mjs` is actor-only.

### Asking each question more than once

**In plain words.** An AI does not answer the same question the same way twice. Ask "where do I buy X"
today and it names three companies; ask again an hour later and one of them is gone. So a report built
on one answer tells you almost nothing: you could be missing from that one answer and present in the
next four. Set **How many times to ask each question** to three or five, and the report stops saying
"you are not named" and starts saying "you were named in two of five answers", which is a number you
can watch move.

**What it does.** Each question is asked `runsPerQuestion` times per engine, one round after another.
The report then shows, per question, how many runs named you and the share, plus how often the runs
agreed with each other. When the runs disagree a lot, the overall share is shaky, and the report says
so instead of hiding it behind an average. Asked once, the report says plainly that this is a snapshot
rather than a measurement.

Every round costs one more answer, so the free look starts at one and you raise it when the number
starts to matter.

# Actor input Schema

## `urls` (type: `array`):

One or more site addresses. Any public page works; the check reads the site's homepage, robots.txt, llms.txt, sitemap and up to three pages.

## `questions` (type: `array`):

Leave empty to score the site only. Add questions and the actor also collects the answers AI assistants give and checks whether your brand is named in them. Write them the way a customer would type them, for example "best property agency in Phuket".

## `runsPerQuestion` (type: `integer`):

An AI answers the same question differently each time, so a single answer is a coin toss. Ask three to five times and the report gives you the share of answers that named you, which is a number you can act on. Each extra round costs another answer, so start at 1 to look around and raise it when the number matters.

## `brand` (type: `string`):

How your company is written in ordinary text. Leave empty and the domain name is used, so "fazwaz.com" becomes "Fazwaz".

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

Names or domains. Every answer is checked for these too, so the report shows who is named when you are not.

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

Google AI Overview works out of the box. Perplexity and ChatGPT are used only if you paste the matching key.

## `countryCode` (type: `string`):

Two-letter country code for the Google search, for example us, gb, th, ae.

## `languageCode` (type: `string`):

Two-letter language code for the Google search, for example en, ru, es.

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

Your own key from perplexity.ai. Stored encrypted, used only for this run, never written to the output.

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

Your own key from platform.openai.com. Stored encrypted, used only for this run, never written to the output.

## `samplePages` (type: `integer`):

How many content pages to read beyond the homepage. More pages give a steadier content score and take longer.

## `budgetSeconds` (type: `integer`):

Hard limit for one site. Raise it for slow sites.

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

Which Perplexity model answers the questions.

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

Which OpenAI model answers the questions. Web search is requested; if the model cannot search, the answer is marked as ungrounded.

## `retryMissingAnswers` (type: `boolean`):

Google serves an AI Overview inconsistently, so a question can return one on one pass and nothing on the next. Leave this on and any question that came back empty is asked once more before it is reported as unanswered. It costs one more search for those questions only.

## `trackChanges` (type: `boolean`):

Keeps one line per site (the score and the mention share) so the next run can show which way both moved. Turn it off if you do not want anything remembered between runs.

## `htmlReport` (type: `boolean`):

Saves a single self-contained HTML page to the run's storage: every site, its score, the AI answers word for word, and the fixes in order. Open it in a browser or send it to a client as it is.

## Actor input object example

```json
{
  "urls": [
    "https://example.com"
  ],
  "questions": [],
  "runsPerQuestion": 1,
  "engines": [
    "google-ai-overview"
  ],
  "countryCode": "us",
  "languageCode": "en",
  "samplePages": 3,
  "budgetSeconds": 20,
  "perplexityModel": "sonar",
  "openaiModel": "gpt-4o-mini",
  "retryMissingAnswers": true,
  "trackChanges": true,
  "htmlReport": true
}
```

# Actor output Schema

## `report` (type: `string`):

A single self-contained HTML page: every site, its score and change, the AI answers word for word, who was named instead of you, and the fixes in order. Open it in a browser or send it to a client.

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

One row per site: score out of 100, grade, the five area scores, the share of AI answers naming the brand, the rivals named instead, the questions it is missing from, and every answer collected.

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

How many sites were scored, how many answers were collected, and which questions no engine answered.

# 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 = {
    "urls": [
        "https://example.com"
    ],
    "questions": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("operstack/ai-visibility-check").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 = {
    "urls": ["https://example.com"],
    "questions": [],
}

# Run the Actor and wait for it to finish
run = client.actor("operstack/ai-visibility-check").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 '{
  "urls": [
    "https://example.com"
  ],
  "questions": []
}' |
apify call operstack/ai-visibility-check --silent --output-dataset

```

## MCP server setup

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

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/Gkn0mzgVWzAWVTpAh/builds/o7kp6q6gWS3LSLCgU/openapi.json
