ChatGPT visibility tracker: metrics, prompt sets, sampling and a working pipeline
A ChatGPT visibility tracker runs a fixed set of buyer prompts through ChatGPT on a schedule, several times per prompt and market, and reduces each answer to a few facts: was the brand mentioned, was its site cited and where, did its products appear. Those rates, over time and against competitors, are the visibility metric. You can buy this as a platform (Peec AI, Otterly.ai, Profound, Scrunch, AthenaHQ, Semrush, Ahrefs Brand Radar) or build it on an answer API. The method comes first here, since it determines whether either approach gives numbers you can trust; then a Python pipeline on AnswerLine; then the off-the-shelf options.
Third-party facts are from the vendors’ own pages, checked 2026-09-17, with links.
Options
| Approach | Engines | What you can measure | Geo | Pricing model and entry price (checked 2026-09-17) |
|---|---|---|---|---|
| DataForSEO LLM Scraper + your own pipeline | ChatGPT, Gemini | sources, search_results, fan_out_queries, brand_entities, product items |
Location, language | From $0.0012 per results page; $50 minimum top-up |
| DataForSEO LLM Mentions | ChatGPT, Google AI Overview (prompt database) | Mentions across 280M+ prompts, not your own prompt set | Database | $0.1 per request + $0.001 per row |
| Bright Data ChatGPT Scraper + your own pipeline | ChatGPT, Perplexity, Gemini, Copilot, AI Mode | Answer text, citations, links, recommendations | Country | $1.50 per 1K records; 5K records/month free |
| AnswerLine + the pipeline below | ChatGPT, plus Perplexity, Gemini, Copilot, Grok, Google AI Mode, AI Overview | Mentions, sources[] with position, citationPills[], searchQueries[] fan-out, shoppingCards[], inlineProducts[], entities[], ads[] |
country, US state |
Credits: 5 per ChatGPT task, +2 once for fan-out, shopping, ads or raw response; free tier 500/month (pricing) |
| Peec AI | 3 of ChatGPT, AI Mode, AI Overviews, Copilot, Perplexity, Gemini | Visibility, position, sentiment, share of voice, sources | Multi-country on Advanced | Starter €85/month, 50 prompts, daily |
| Otterly.ai | ChatGPT, AI Overviews, Perplexity, Copilot (+ add-ons) | Platform reports; API on Standard and up | Platform | Lite $29/month, 15 prompts |
| Semrush AI Visibility Toolkit | ChatGPT, Google AI, Gemini, Perplexity | Prompt tracking, mentions | Platform | $99/month per domain, 25 prompts |
| Profound | Trial: ChatGPT only; Enterprise up to 9 engines | Platform reports; API on Enterprise | Enterprise: multiple regions | Free trial (10 prompts, run once); Enterprise custom |
| OpenAI API with web_search | Your configured model, not the app | Citations for your own call | user_location |
$10 per 1K search calls + tokens (pricing) |
Metrics
Define these before collecting anything. Each maps to one part of the ChatGPT answer.
| Metric | Definition | AnswerLine response field |
|---|---|---|
| Mention rate | Answers that name the brand ÷ completed answers | text (and entities[].name when present) |
| Citation rate | Answers whose sources include the brand’s domain ÷ completed answers | sources[].url |
| Average citation position | Mean best position of the domain, over answers where it is cited |
sources[].position |
| Product presence | Answers where a shopping card or inline product shows the brand ÷ completed answers | shoppingCards[].products[], inlineProducts[].product |
| Share of voice | Brand mentions ÷ mentions of all brands in the defined competitor set | text across brands |
| Fan-out coverage | Share of ChatGPT’s search queries in which your pages rank (joined with your SERP data) | searchQueries[] |
To keep them comparable over time:
- Only completed answers go in the denominator. A failed collection is not an answer without you.
- Mentions and citations stay separate. A brand can be recommended without a link and linked without a recommendation. See mentions vs citations.
- The competitor set is fixed and versioned, because share of voice changes meaning whenever the set changes. The share of voice framework covers the arithmetic.
Building the prompt set
The prompt set decides what “visibility” means for you.
- Start from buyer questions rather than keywords. Sales calls, support tickets, site search and People Also Ask are better sources than a keyword list; People Also Ask for prompt research shows one method.
- Cover the funnel: category questions (“best payroll software for small business”), comparisons (“Gusto vs ADP”), problems (“how to run payroll for contractors in two states”), and brand questions (“is Acme Payroll good for restaurants”).
- Tag every prompt with topic, funnel stage and whether it names a brand. Unbranded prompts measure discovery and branded prompts measure reputation, so report them separately.
- Write prompts the way people type: short, conversational, sometimes with context (“for a 20-person agency”). Do not name your brand in unbranded prompts.
- Size by topic. Ten to twenty prompts per topic gives a rate that no single phrasing dominates.
- Keep a stable core. Changing prompts breaks trend lines, so add new prompts as a separate cohort and fold them in after a few weeks.
Prompt set design goes deeper on balance and size.
Sampling: how many runs you need
ChatGPT’s answers vary between runs of the same prompt, so a single daily run per prompt measures that noise as much as your visibility. Treat each answer as a sample and report rates over pooled samples.
For a rate p measured over n answers, the 95% margin of error is about 1.96 × √(p(1−p)/n):
| Answers pooled (n) | Margin at p = 10% | Margin at p = 30% | Margin at p = 50% |
|---|---|---|---|
| 50 | ±8.3 pts | ±12.7 pts | ±13.9 pts |
| 200 | ±4.2 pts | ±6.4 pts | ±6.9 pts |
| 1,000 | ±1.9 pts | ±2.8 pts | ±3.1 pts |
In practice:
- Pool before you read: weekly rates per topic, not daily rates per prompt.
- More distinct prompts reduce phrasing bias; more runs per prompt reduce run-to-run noise. Three runs a day across 15 prompts in a topic gives 315 answers per topic per week.
- Keep markets separate. Pooling the US and Germany mixes two populations.
The sampling vs census post and AI answer volatility cover this in more depth.
A pipeline on AnswerLine
One Python file with three commands: submit queues today’s runs, collect fetches finished tasks and extracts brand signals, and report prints weekly rates. It uses requests and SQLite against the HTTP API, and polls for results to stay self-contained; at larger volume, use a webhook instead.
Inputs
prompts.csv:
id,topic,prompt
p001,category,best payroll software for small business
p002,category,easiest payroll software for restaurants
p003,comparison,Gusto vs ADP for a 20-person company
The script
import csv, datetime, os, re, sqlite3, sys
from urllib.parse import urlparse
import requests
API = "https://api.answerline.dev"
COUNTRIES = ["US"]
RUNS_PER_DAY = 3
BRANDS = {
"acme": {"patterns": [r"\bacme payroll\b"], "domain": "acmepayroll.com"},
"gusto": {"patterns": [r"\bgusto\b"], "domain": "gusto.com"},
"adp": {"patterns": [r"\badp\b"], "domain": "adp.com"},
}
http = requests.Session()
http.headers["Authorization"] = f"Bearer {os.environ['API_KEY']}"
db = sqlite3.connect("chatgpt_visibility.db")
db.executescript("""
create table if not exists tasks (
task_id text primary key, prompt_id text, topic text, country text, day text, run int, status text);
create table if not exists hits (
task_id text, brand text, mentioned int, cited int, best_position real, in_products int,
primary key (task_id, brand));
create table if not exists fanout (task_id text, query text);
""")
def submit():
day = datetime.date.today().isoformat()
with open("prompts.csv", newline="", encoding="utf-8") as f:
prompts = list(csv.DictReader(f))
meta, tasks = [], []
for p in prompts:
for country in COUNTRIES:
for run in range(RUNS_PER_DAY):
meta.append((p["id"], p["topic"], country, day, run))
tasks.append({
"taskType": "CHATGPT",
"payload": {
"prompt": p["prompt"],
"country": country,
"include": {"searchQueries": True, "shopping": True},
},
"idempotencyKey": f"{p['id']}-{country}-{day}-r{run}",
})
for start in range(0, len(tasks), 500):
res = http.post(f"{API}/v1/async/task/batch", json=tasks[start:start + 500], timeout=60)
res.raise_for_status()
for item in res.json()["results"]:
m = meta[start + item["index"]]
if item["success"]:
db.execute("insert or ignore into tasks values (?,?,?,?,?,?,?)",
(item["task"]["id"], *m, "QUEUED"))
else:
print("not queued:", m, item["error"]["code"])
db.commit()
def on_domain(url, domain):
host = (urlparse(url).hostname or "").lower()
return host == domain or host.endswith("." + domain)
def product_text(result):
parts = []
for card in result.get("shoppingCards", []):
for prod in card.get("products", []):
parts += [prod.get("title", ""), prod.get("merchant", "")]
for item in result.get("inlineProducts", []):
prod = item.get("product", {})
parts.append(prod.get("title", ""))
parts += [o.get("merchant_name", "") for o in prod.get("offers", [])]
return " ".join(p for p in parts if p).lower()
def extract(task_id, result):
text = result.get("text", "").lower()
products = product_text(result)
for brand, cfg in BRANDS.items():
mentioned = any(re.search(p, text) for p in cfg["patterns"])
positions = [s["position"] for s in result.get("sources", [])
if on_domain(s.get("url", ""), cfg["domain"])]
in_products = any(re.search(p, products) for p in cfg["patterns"])
db.execute("insert or replace into hits values (?,?,?,?,?,?)",
(task_id, brand, mentioned, bool(positions),
min(positions) if positions else None, in_products))
db.executemany("insert into fanout values (?,?)",
[(task_id, q) for q in result.get("searchQueries", [])])
def collect():
pending = db.execute("select task_id from tasks where status in ('QUEUED','PROCESSING')").fetchall()
for (task_id,) in pending:
res = http.get(f"{API}/v1/async/task/{task_id}", timeout=30)
res.raise_for_status()
body = res.json()
status = body["task"]["status"]
if status == "COMPLETED":
extract(task_id, body["response"]["result"])
db.execute("update tasks set status = ? where task_id = ?", (status, task_id))
db.commit()
def report():
rows = db.execute("""
select strftime('%Y-%W', t.day) as week, t.topic, h.brand,
count(*) as answers,
round(100.0 * avg(h.mentioned), 1) as mention_pct,
round(100.0 * avg(h.cited), 1) as citation_pct,
round(avg(h.best_position), 2) as avg_position,
round(100.0 * avg(h.in_products), 1) as product_pct
from hits h join tasks t using (task_id)
where t.status = 'COMPLETED'
group by week, t.topic, h.brand
order by week, t.topic, mention_pct desc""").fetchall()
for row in rows:
print(*row, sep="\t")
if __name__ == "__main__":
{"submit": submit, "collect": collect, "report": report}[sys.argv[1]]()
Run python tracker.py submit once a day, python tracker.py collect every few minutes until nothing is pending, and python tracker.py report for the table. Test it on a few prompts before scheduling the full set.
Design choices
- Idempotency keys from prompt, market, day and run. Running
submittwice on the same day creates nothing new: duplicates come back asRESOURCE_ALREADY_EXISTS, and the first submission’s tasks are already stored. See idempotency keys explained. - Batches of 500, the maximum per request. Each item succeeds or fails on its own, so the code checks every item.
include.searchQueriesandinclude.shoppingtogether. Both belong to the same add-on group, so enabling both costs 2 extra credits, not 4.shoppingCardsread with.get. The field is omitted when shopping is not requested and can be absent from answers without products.- Failed tasks are recorded but excluded from rates, so only
COMPLETEDanswers form the denominator. Failed tasks are not charged. - One row per brand per answer. A brand’s share of voice for a topic is its mention count divided by the sum across
BRANDSfor that topic and week.
Extending it
- More markets: add codes to
COUNTRIES, or US states with"state": "TX"in the payload and the state in the key. - Webhooks: add
"webhook": {"url": ...}to each task and moveextractinto the handler, readingresponse.result. Verify signatures first. - Other engines: set
taskTypetoPERPLEXITY,GEMINI,COPILOTorAIMODEand remove the ChatGPT-onlyincludeflags, which other engines reject. Thetextandsources[]fields used here exist on those responses too; shopping fields differ by engine. - Fan-out analysis: group
fanout.queryby frequency per topic to find the searches ChatGPT runs most; see query fan-out. - Mention detection: regex on brand names misses misspellings and catches homonyms. Keep an alias list per brand and review a sample of matches each month.
- Alerts: compare this week’s rates with the previous four weeks and alert when a change exceeds the margin of error in the sampling table.
Reading the weekly report
report prints one row per week, topic and brand. Read it in this order:
- Answer count. A week with far fewer completed answers than planned has wider margins; check the sampling table before comparing it with other weeks.
- Mention rate against citation rate. High mentions with low citations means ChatGPT knows the brand but cites other sites. Low mentions with some citations means your pages are retrieved but the answer recommends others. The fan-out table shows which searches produced the sources cited instead of yours.
- Average position, only when citation rate is stable. Position over a handful of cited answers swings widely.
- Product presence in commercial topics. Where ChatGPT shows shopping cards, a brand can lead the text and still be missing from the carousel.
- The answers behind any change. The script stores only extracted signals; add a table for text and sources so a moved number can be explained from the answers.
Cost of this pipeline
100 prompts, three runs a day, one market, 30 days: 9,000 ChatGPT answers a month.
| Option | Billing (checked 2026-09-17) | Monthly |
|---|---|---|
| DataForSEO LLM Scraper, standard queue | $0.0012 per results page | $10.80, collection only; $50 minimum top-up |
| Bright Data, pay as you go | $1.50 per 1K records | $13.50, collection only |
| AnswerLine | 7 credits per task (5 base + 2 add-on group), async | 63,000 credits, collection only; compare with plan allowances on /pricing |
| Otterly.ai Standard | $189/month, 100 prompts, daily, four engines | Platform, not per answer |
| Peec AI Pro | €205/month, 150 prompts, three models, daily | Platform, not per answer |
API and scraper lines cover collection, and your pipeline covers the rest. Platform lines include reports but fix run frequency and prompt allowance. AI monitoring cost planning shows how cadence and runs per prompt change the total.
Off-the-shelf ChatGPT trackers
These platforms include ChatGPT (entry details checked 2026-09-17):
- Peec AI: Starter €85 a month, 50 prompts, three models, daily; visibility, position, sentiment and share of voice; API on Enterprise.
- Otterly.ai: Lite $29 a month, 15 prompts, daily, ChatGPT, AI Overviews, Perplexity and Copilot; API from Standard.
- Semrush AI Visibility Toolkit: $99 a month per domain, 25 prompts tracked daily.
- Scrunch: Starter $300 a month month-to-month, 350 prompts.
- AthenaHQ: free Essential tier with 300 credits; Starter $295 a month.
- Ahrefs Brand Radar: custom prompts from $50 a month.
- Profound: free trial on ChatGPT (10 prompts, run once); Enterprise custom.
Before choosing, ask how many runs per prompt the platform takes and how it defines visibility; the sampling table applies to platforms too. The AI visibility tools comparison covers each in more detail.
Pitfalls
- Measuring the API instead of the app. A model API with web search is a different product; see provider APIs vs consumer answers.
- Mixing answers with and without web search in one series. An answer produced without searching has no citations. AnswerLine forces web search by default; if you set
disableWebSearch, keep those runs in a separate series. - Reading single prompts daily. Pool runs, then read.
- Changing the prompt set mid-series. Version it and add cohorts.
- Ignoring shopping blocks in product categories. See ChatGPT shopping cards.
The method behind mention measurement is in measuring brand mentions in ChatGPT, and the ChatGPT response fields are on the ChatGPT engine page.
Questions
How do you track brand visibility in ChatGPT?
Run a fixed set of prompts your buyers would ask through ChatGPT on a schedule, several times each, per market. For every answer record whether your brand is mentioned, whether your domain is cited and at what position, and whether your products appear in shopping cards, then report those rates over time against competitors.
How many times should each prompt be run?
More than once, because ChatGPT answers vary between runs. A rate from 200 answers has a 95% margin of error of roughly plus or minus 6 to 7 percentage points near 30%, so pool runs across a week or a topic before reading small changes.
Which metrics matter for ChatGPT visibility?
Mention rate, citation rate, average citation position, product presence in shopping cards, and share of voice against a defined competitor set. Keep mentions and citations separate: being named and being linked are different outcomes.
Can I use the OpenAI API to track ChatGPT visibility?
It measures something else. The OpenAI API answers with the model and settings you choose; ChatGPT users get the app's answer, with its own search, citations and shopping cards. Visibility tracking should use the app's answers.
What does the pipeline in this post cost to run on AnswerLine?
Each ChatGPT task with include.searchQueries and include.shopping costs 7 credits as an async task (5 base plus 2 for the add-on group). 100 prompts run three times a day in one market is 9,000 tasks and 63,000 credits a month; plan allowances are on the pricing page.