How to track competitor visibility in ChatGPT
Tracking competitor visibility in ChatGPT means asking the question your buyers ask and recording which brands come back. You can do it by hand - open ChatGPT, type "best CRM for small business", note who gets listed. To do it every week, across a query set, with citations attached, POST the query to https://api.agentgeo.org/v1/fetches and scan answerText and sources[] for every name on your list.
The unit worth tracking isn't your own brand. It's the set - the handful of companies ChatGPT treats as the answer to your category question. This guide covers the manual check, the one-call version in curl and Python, the same job handed to an agent over MCP, and the traps that make competitor data read wrong.
Read this page with an AI
To track competitor visibility in ChatGPT, ask the model your category question and record which brands come back - then repeat it on a schedule so you can watch the set change. Doing that once takes thirty seconds in the ChatGPT window. Doing it every week, across twenty queries, with citations attached and history you can diff, takes one API call and about twenty lines of code.
The manual way (and why it stops working)
Start here, honestly - it costs nothing and it teaches you what these answers actually look like. Open ChatGPT and ask the question a buyer would ask ("best CRM for small business", not "tell me about Acme"). Read the answer end to end and write down every company it names, in the order it names them. Then ask the obvious follow-up - "what are the alternatives?" - and note which names are new. Do that across three or four phrasings of the same intent and you have a rough picture of the competitive set ChatGPT holds for your category.
That picture is real and worth having. It just doesn't survive contact with a reporting cadence:
- One query at a time. A category has twenty or thirty buying questions behind it. Typing each one and transcribing the names is an afternoon - an afternoon you have to spend again next month.
- The answer moves. Ask the same question twice and you often get a different shortlist: different order, a name dropped, a new one added. A single reading can't tell you which change is real.
- No history to diff. The chat window shows you today. The interesting question - has Globex started appearing in three more of our queries than it did in June? - needs June's answers stored somewhere.
- Citations get lost. When ChatGPT browses, it links the pages it used. Copying those links out by hand, with their order intact, is tedious and the order is the first thing to go.
- It can't go anywhere. A hand-typed list isn't a chart in a client report, a row in a table, or a job that pings Slack the week a new competitor enters the set.
Do it with one API call
AgentGEO returns the raw record behind that answer - the full text plus every source the engine cited, with positions. One POST, one JSON body, and the same shape across all six engines; here it's just chatgpt.
curl -X POST https://api.agentgeo.org/v1/fetches \
-H "Authorization: Bearer ag_live_..." \
-H "Content-Type: application/json" \
-d '{"query": "best CRM for small business", "surfaces": ["chatgpt"]}'What comes back is a plain envelope - no HTML, no scraping artifacts, and identical every run, which is what makes the scan easy to write:
{
"id": "run_84c1f2ab91d3",
"query": "best CRM for small business",
"surfaces": ["chatgpt"],
"status": "completed",
"recordsDelivered": 1,
"creditsCharged": 1,
"answers": [
{
"surfaceKey": "chatgpt",
"status": "delivered",
"answerText": "For a small business the tools most often suggested are Globex, which leads on automation, and Acme, which is the fastest to set up ...",
"sources": [
{ "title": "Best CRM software in 2026", "url": "https://example.com/best-crm", "position": 1 },
{ "title": "Globex CRM pricing", "url": "https://globex.com/pricing", "position": 2 }
],
"fetchedAt": "2026-07-08T09:14:22Z",
"latencyMs": 11840
}
]
}The scan is the whole job. Keep your competitive set in one dict - display name to root domain - and check both places a competitor can turn up: named in the prose, and cited as a source underneath it.
import requests
from urllib.parse import urlparse
KEY = "ag_live_your_key_here"
QUERY = "best CRM for small business"
# The competitive set you care about: display name -> root domain
BRANDS = {
"Acme": "acme.com",
"Globex": "globex.com",
"Initech": "initech.com",
}
def host(url):
# Normalise to a bare host so acme.com and www.acme.com match.
h = urlparse(url).netloc.lower()
return h[4:] if h.startswith("www.") else h
resp = requests.post(
"https://api.agentgeo.org/v1/fetches",
json={"query": QUERY, "surfaces": ["chatgpt"]},
headers={"Authorization": f"Bearer {KEY}"},
timeout=200, # the API waits up to 180s for live surfaces
)
resp.raise_for_status()
for answer in resp.json()["answers"]:
if answer.get("status") == "failed":
print("no record this run - retry later")
continue
text = (answer.get("answerText") or "").lower()
hosts = {host(s["url"]) for s in answer.get("sources", [])}
print(f"\n{answer['surfaceKey']} - {QUERY}")
for brand, domain in BRANDS.items():
named = brand.lower() in text
cited = any(h == domain or h.endswith("." + domain) for h in hosts)
print(f" {brand:<10} named={str(named):<5} cited={cited}")
missing = [b for b in BRANDS if b.lower() not in text]
print(f" not named at all: {', '.join(missing) or 'none'}")Run it and you get a small grid: who ChatGPT named, who it cited, and the column that usually matters most - who is on your list and appears in neither. Wrap the whole thing in a loop over your query set and you have a weekly competitor report. Each delivered record costs one credit; records that come back failed cost nothing.
| Checking by hand | One API call | |
|---|---|---|
| Queries per pass | As many as you'll type | Your whole query set, in a loop |
| Competitor set | Whoever you remember to look for | An explicit list, checked identically every run |
| Citations | Copied by hand, order usually lost | sources[] with title, URL and position |
| History | Screenshots in a folder | Stored runs you can subtract from each other |
| Where it ends up | A doc someone reads once | A report, a table, a cron job, an alert |
Track the set, not your own row. Whether ChatGPT named Acme on a given Tuesday is close to a coin flip and you can't act on it. Whether the set of brands it returns across your twenty category queries has drifted over six weeks is a trend, and it points at something you can fix. Store every run and treat a name entering or leaving the set as the event worth reporting - that's the signal a single snapshot can never give you.
Let your agent do it
The same endpoint is exposed over MCP, so an agent that already knows your competitors can run the sweep itself - no parser to write. One command wires it into Claude Code:
claude mcp add agentgeo -- npx -y agentgeo-mcp --key ag_live_...agentgeo-mcp speaks MCP over stdio, ships with zero npm dependencies and needs Node.js 18+. It exposes exactly one tool, fetch_raw_answers: query and surfaces are required, and country defaults to "US". (language defaults to "en" but only reaches Google AI Overviews - ChatGPT's scraper takes a country and no language field.) After that you just describe the check:
- "Fetch what ChatGPT answers for 'best CRM for small business' and list every company it names, in the order it names them."
- "Run our five category queries through ChatGPT and tell me which of Acme, Globex and Initech show up in each - named, cited, or neither."
- "Compare today's competitor set against the one in geo-log.md and tell me who entered and who dropped out."
The records arrive raw. Whether Globex appearing in two more queries actually matters is a judgment your agent makes with your context - AgentGEO computes no scores, ranks nothing and draws no conclusions, by design.
Common problems
Most bad competitor data comes from five places. All five are fixable in the parser or the schedule.
- Brand names collide with English. A substring test for a short name fires on unrelated words and on other companies that share a prefix. Match on a word boundary -
re.search(r"\bacme\b", text, re.I)- and keep an explicit list of the collisions you already know about. - One run is a sample, not a reading. Chatbot answers vary between identical calls. Before you report that a competitor vanished, fetch the same query two or three times, or on two consecutive days, and see whether it holds.
- Phrasing changes the set. "Best CRM for small business" and "top CRM tools for small teams" can return different companies. Freeze a query set that mirrors how your buyers actually phrase it, and change it deliberately rather than casually - a query edit invalidates the comparison to last month.
- Named in the text is not cited in the sources. A competitor in
answerTextwith nothing matching insources[]usually means the model knows them without reading them today. Track the two columns separately; they call for completely different responses. - Some chatbot fetches run long. A request waits up to 180 seconds, and a scrape that exceeds that budget comes back with
status: "failed"at zero credits and aproviderFields.snapshot_id. A follow-up call with that id and the same single surface redeems the finished answer rather than paying to scrape it again.
Summary
For a one-off gut check, open ChatGPT and read the answer - it's free and it's fast. The moment the check has to repeat, cover a query set, or land in a report, hand it to the API or to an agent: one call returns the answer text and its sources, twenty lines of Python turn that into a competitor grid, and stored runs turn the grid into the only thing that really matters here - the trend.
See which competitors ChatGPT names for your category. Get a free audit - no card and a real person runs your prompts and sends the answers back.
Get my free audit