Skip to content
Blog

How to Track Brand Mentions in ChatGPT, Gemini, Copilot and Google AI Mode with Python

Carmine Cella18 min read

How to Track Brand Mentions in ChatGPT, Gemini, Copilot and Google AI Mode with Python

To track brand mentions in ChatGPT and other AI assistants, send a fixed set of buyer questions to each engine on a schedule, from the country you sell in, and record whether your brand is named, where it appears in the answer and which sources are cited. This is LLM visibility tracking. One check in your own chat window measures nothing. You need the same prompts, asked the same way, many times.

This tutorial builds that loop in one Python file using the LLM Scraper API. To show real output, we ran it on the CRM market: 25 buyer questions, each asked three times on four engines, for 292 answers in total.

Tested on 13 October 2026 with API v1.

What AI visibility tracking measures actually

Classic rank tracking gives you a position from 1 to 100. AI answers don't work that way. An assistant writes a paragraph or a short list. Your brand is in it or not, near the top or at the end, recommended or just named.

So AI visibility tracking comes down to five signals:

  1. Mention rate. Out of N answers, how many name your brand.
  2. Position in the answer. Is it the first brand named, or the fifth?
  3. Citations. Which URLs the engine shows as sources. If your pages are cited, you have a direct lever: improve those pages.
  4. Share of voice. Your mentions divided by the mentions of every brand you track in the same answers.
  5. Framing. Is your brand described the way you describe it? Wrong prices or old features show up here.

The first four are counts. Code handles them well. The fifth needs a person reading a sample of answers each week.

Why LLM visibility tracking beats asking ChatGPT yourself

Three things make manual checks misleading:

  • Personalization. A logged-in session carries your history. Your buyers don't share it.
  • Location. Engines answer differently depending on where the question comes from.
  • Variance. The same prompt can return a different list five minutes later. One answer is an anecdote. Many answers make a trend.

The fix is boring and reliable: clean sessions, a fixed country per run, a fixed prompt set, and enough repetitions to average out the noise.

Prerequisites

  • Python 3.10 or newer.
  • pip install requests pandas
  • A Datafuel API key from the dashboard, exported as DATAFUEL_API_KEY. Authentication is described in the docs.

Step 1: Write a prompt set your buyers would actually type

Start with 20 to 50 prompts. Mix three kinds. For a CRM vendor they look like this:

  • Category prompts: "best CRM for small business", "best free CRM for startups".
  • Problem prompts: "how do I stop losing track of leads in spreadsheets", "which CRM integrates best with Gmail and Google Workspace".
  • Brand prompts: "HubSpot vs Salesforce for a B2B startup", "is Pipedrive good for real estate agents".

Good sources are sales call notes, support tickets, Google's "People also ask" boxes and the question keywords in your SEO tool. Keep the wording natural. People write to assistants in full sentences.

Put one prompt per line in prompts.txt. Change the set only on purpose, and note the date when you do. Otherwise your trend line measures your edits, not the engines.

Step 2: Send each prompt to every engine

The LLM Scraper API captures the answer a real user sees in ChatGPT, Gemini, Google AI Mode, Perplexity or Copilot, from the country you choose, with the sources the interface cites. Each request costs 100 credits on any engine, and failed captures are free.

One request looks like this:

curl -X POST https://scraping-api.datafuel.ai/api/v1/task \
  -H "X-API-Key: $DATAFUEL_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 2026-10-13-best-crm-small-business-gemini-us" \
  -d '{
    "type": "llm_scraping",
    "proxy_country": "us",
    "attributes": {
      "prompt": "best CRM for small business",
      "engine": "gemini"
    }
  }'

The parameters that matter for tracking:

  • engine: openai (ChatGPT), gemini, google_ai_mode, perplexity or copilot.
  • proxy_country: the country the answer is captured from. Run each market as a separate pass.
  • websearch: for ChatGPT, true lets it browse while it answers, which is closer to what most users see. The other engines search the web on their own and return citations either way.
  • Idempotency-Key: makes retries safe. Resend a timed-out request with the same key and it attaches to the running task instead of charging twice.

The answer comes back as JSON: the text, with [n] markers, and a list of numbered citations. Here is a real response from our run, shortened:

{
  "id": "f94ff4a1-8278-426d-abe4-9ee8637a60e7",
  "status": "completed",
  "credits_used": 100,
  "result": {
    "data": {
      "text": "Choosing the \"best\" CRM for a small business depends heavily on your budget, technical comfort, and primary goals ...",
      "citations": [
        {"kind": "source", "n": 1,
         "title": "The 6 Best CRMs for Small Business in 2026 - Osmos Cloud",
         "url": "https://www.osmoscloud.com/blog/the-6-best-crm-for-small-business-in-2024/"},
        {"kind": "source", "n": 2,
         "title": "The Best CRM Software We've Tested for 2026 | PCMag",
         "url": "https://www.pcmag.com/picks/the-best-crm-software"}
      ]
    }
  }
}

Engines can be switched off for maintenance. GET /api/v1/config/capabilities needs no key and lists each engine with enabled: true or false. The script checks it first and skips anything that isn't accepting work.

Step 3: Run the prompt set as a job

For a list of prompts, don't loop over single calls. Send a job per engine with a prompts array (at least two), poll it until it leaves pending or processing, then read the results. A job ends as completed, completed_with_errors when some prompts failed, or failed or cancelled. Failed prompts are refunded and left out of the scores.

Answers vary from one run to the next, so the script sends every prompt several times (REPEATS = 3) and averages. Results come back in the same order as the prompts. The script checks that the count matches before it pairs them and stops if it doesn't.

Step 4: Match brands with word boundaries and aliases

Matching looks simple until you meet brand names that are also English words. Close, Copper, Nutshell, Capsule and Monday are all CRMs. They are also words that show up in any sales answer: "expected close date", "copper wire", "in a nutshell", "Monday morning follow-ups".

Search for the bare word close and you'll count every deal-stage explanation as a mention of Close. In our data, the bare word "close" appeared in 49 of 292 answers. The aliases close crm, close.com and close.io matched 14. A typical false hit: "every open deal has an owner, an estimated value, an expected close date..."

So give each brand a list of specific aliases, and match them with word boundaries (\b), so "hubspot" doesn't match inside a longer word:

BRANDS = {
    "Close": ["close crm", "close.com", "close.io"],
    "Copper": ["copper crm", "copper.com"],
    "Nutshell": ["nutshell crm", "nutshell.com"],
    "Capsule": ["capsule crm", "capsulecrm"],
    "monday.com": ["monday.com", "monday sales crm", "monday crm"],
}

Strict aliases have the opposite risk: they miss answers that write just "Nutshell" in a list of CRMs. Check a sample of raw answers for the bare word, and add aliases when you see real misses.

Step 5: Score, schedule and act

For each answer the script records which tracked brands appear, the order of their first mention, and every cited domain. It prints three tables per engine: mention rate, share of voice and average position.

Average position is the position among the brands you track, not among every brand in the answer. If an answer names HubSpot, then two CRMs that aren't on your list, then Pipedrive, Pipedrive is position 2. Add the brands that matter to you, or the number tells you less than it seems.

Run the script weekly with cron or any scheduler. Weekly is enough for most teams. Daily runs mostly add noise, unless you're watching a launch.

The full script

Save this as tracker.py and run python tracker.py prompts.txt. Edit the settings block at the top: engines, country, repeats, your domain and the brands you track. Perplexity is in ENGINES; when an engine isn't available, the script skips it and carries on.

"""Track brand mentions in ChatGPT, Gemini, Perplexity, Copilot and Google AI Mode.

Requires Python 3.10+ and:  pip install requests pandas
Set DATAFUEL_API_KEY in your environment (create a key in the Datafuel dashboard).
Usage: python tracker.py prompts.txt
"""
import hashlib
import json
import os
import re
import sys
import time
from urllib.parse import urlparse

import pandas as pd
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

# --- settings -------------------------------------------------------------
API = "https://scraping-api.datafuel.ai/api/v1"
ENGINES = ["openai", "gemini", "google_ai_mode", "perplexity", "copilot"]
COUNTRY = "us"                 # country the answers are captured from
WEBSEARCH = True               # let ChatGPT browse while answering
REPEATS = 3                    # ask every prompt this many times per engine
OWN_DOMAIN = "yourbrand.com"   # your site, to count when it is cited
BRANDS = {                     # brand -> lowercase aliases
    "HubSpot": ["hubspot"],
    "Salesforce": ["salesforce"],
    "Pipedrive": ["pipedrive"],
    "Zoho CRM": ["zoho crm", "zoho"],
    "monday.com": ["monday.com", "monday sales crm", "monday crm"],
    "Freshsales": ["freshsales", "freshworks crm"],
    "Close": ["close crm", "close.com", "close.io"],
    "Copper": ["copper crm", "copper.com"],
    "Attio": ["attio"],
    "Microsoft Dynamics 365": ["dynamics 365", "microsoft dynamics"],
    "Insightly": ["insightly"],
    "Keap": ["keap"],
    "Nutshell": ["nutshell crm", "nutshell.com"],
    "Capsule": ["capsule crm", "capsulecrm"],
    "folk": ["folk crm", "folk.app"],
}
MAX_WAIT = 2400                # seconds per job before giving up
POLL_EVERY = 10
RUN_ID = time.strftime("%Y-%m-%d")
HEADERS = {"X-API-Key": os.environ["DATAFUEL_API_KEY"],
           "Content-Type": "application/json"}

# Retry network errors and 429/5xx on every call (POSTs are safe: Idempotency-Key).
http = requests.Session()
http.mount("https://", HTTPAdapter(max_retries=Retry(
    total=5, backoff_factor=2, status_forcelist=[429, 500, 502, 503, 504],
    allowed_methods=None)))

PATTERNS = {brand: [re.compile(rf"\b{re.escape(a)}\b") for a in aliases]
            for brand, aliases in BRANDS.items()}


# --- API calls -----------------------------------------------------------
def enabled_engines() -> set[str]:
    caps = http.get(f"{API}/config/capabilities", timeout=30).json()
    return {e["name"] for e in caps["engines"] if e["enabled"]}


def start_job(engine: str, prompts: list[str], repeat: int) -> str:
    body = {"type": "llm_scraping", "proxy_country": COUNTRY,
            "attributes": {"prompts": prompts, "engine": engine,
                           "websearch": WEBSEARCH}}
    # Same day + engine + prompts + repeat = same key, so a re-run never pays twice.
    digest = hashlib.sha1("\n".join(prompts).encode()).hexdigest()[:12]
    key = f"{RUN_ID}-{engine}-{COUNTRY}-{digest}-r{repeat}"
    r = http.post(f"{API}/job", json=body,
                  headers={**HEADERS, "Idempotency-Key": key}, timeout=60)
    r.raise_for_status()
    return r.json()["id"]


def wait(job_id: str, max_wait: int = MAX_WAIT) -> dict:
    deadline = time.time() + max_wait
    while time.time() < deadline:
        s = http.get(f"{API}/job/{job_id}", headers=HEADERS, timeout=30).json()
        if s["status"] not in ("pending", "processing"):
            return s           # completed, completed_with_errors, failed or cancelled
        time.sleep(POLL_EVERY)
    raise TimeoutError(f"job {job_id} still running after {max_wait} s")


def job_results(job_id: str) -> list[dict]:
    r = http.get(f"{API}/job/{job_id}/results", headers=HEADERS, timeout=120)
    r.raise_for_status()
    return r.json()["tasks_result"]


# --- scoring -------------------------------------------------------------
def answer_text(task: dict) -> str:
    data = (task.get("result") or {}).get("data") or {}
    return data.get("text") or data.get("answer") or ""


def citations(task: dict) -> list[dict]:
    data = (task.get("result") or {}).get("data") or {}
    return data.get("citations") or data.get("sources") or []


def domain(url: str) -> str:
    return urlparse(url).netloc.lower().removeprefix("www.")


def score(task: dict) -> dict:
    text = answer_text(task).lower()
    first_hit = {}
    for brand, patterns in PATTERNS.items():
        positions = [m.start() for p in patterns for m in p.finditer(text)]
        if positions:
            first_hit[brand] = min(positions)
    order = sorted(first_hit, key=first_hit.get)   # among tracked brands only
    return {"mentioned": set(first_hit),
            "rank": {b: i + 1 for i, b in enumerate(order)},
            "cited": [c.get("url", "") for c in citations(task)]}


def main(prompt_file: str) -> None:
    with open(prompt_file) as f:
        prompts = [line.strip() for line in f if line.strip()]
    if len(prompts) < 2:
        sys.exit("A job needs at least 2 prompts.")

    live = enabled_engines()
    jobs = []                  # start everything first, then wait
    for engine in ENGINES:
        if engine not in live:
            print(f"skip {engine}: not accepting requests right now")
            continue
        for repeat in range(1, REPEATS + 1):
            jobs.append((engine, repeat, start_job(engine, prompts, repeat)))

    raw, rows, cites = {}, [], []
    for engine, repeat, job_id in jobs:
        status = wait(job_id)
        if status["status"] in ("failed", "cancelled"):
            print(f"{engine} run {repeat}: job {status['status']}, skipped")
            continue
        tasks = job_results(job_id)
        print(f"{engine} run {repeat}: job {status['status']}, "
              f"{sum(t.get('credits_used') or 0 for t in tasks)} credits")
        # Results come back in prompt order; stop if the count doesn't match.
        if len(tasks) != len(prompts):
            sys.exit(f"{engine}: {len(tasks)} results for {len(prompts)} prompts")
        raw.setdefault(engine, []).extend(
            {"prompt": p, "repeat": repeat, **t} for p, t in zip(prompts, tasks))
        for prompt, task in zip(prompts, tasks):
            if task.get("status") != "completed":
                continue       # failed tasks are refunded; retry next run
            s = score(task)
            for url in s["cited"]:
                cites.append({"engine": engine, "repeat": repeat,
                              "prompt": prompt, "domain": domain(url)})
            own_cited = any(domain(u).endswith(OWN_DOMAIN) for u in s["cited"])
            for brand in BRANDS:
                rows.append({"run": RUN_ID, "country": COUNTRY, "engine": engine,
                             "repeat": repeat, "prompt": prompt, "brand": brand,
                             "mentioned": brand in s["mentioned"],
                             "rank": s["rank"].get(brand),
                             "own_domain_cited": own_cited})

    if not rows:
        sys.exit("No answers collected: every engine was skipped or failed.")

    with open(f"answers-{RUN_ID}-{COUNTRY}.json", "w") as f:
        json.dump(raw, f)          # keep raw answers to re-score later
    df = pd.DataFrame(rows)
    df.to_csv(f"visibility-{RUN_ID}-{COUNTRY}.csv", index=False)
    pd.DataFrame(cites).to_csv(f"citations-{RUN_ID}-{COUNTRY}.csv", index=False)

    answers = df.drop_duplicates(["engine", "repeat", "prompt"]).groupby("engine").size()
    mention_rate = df.groupby(["engine", "brand"])["mentioned"].mean().unstack().round(2)
    counts = df[df.mentioned].groupby(["engine", "brand"]).size()
    share_of_voice = (counts / counts.groupby(level="engine").transform("sum")).unstack().round(2)
    avg_rank = df.dropna(subset=["rank"]).groupby(["engine", "brand"])["rank"].mean().unstack().round(1)

    pd.set_option("display.width", 200)
    print("\nAnswers per engine (n)\n", answers.to_string(), sep="")
    print("\nMention rate\n", mention_rate.T, sep="")
    print("\nShare of voice\n", share_of_voice.T, sep="")
    print("\nAverage position among tracked brands\n", avg_rank.T, sep="")


if __name__ == "__main__":
    main(sys.argv[1] if len(sys.argv) > 1 else "prompts.txt")

It writes three files per run: the raw answers (answers-*.json), one row per answer and brand (visibility-*.csv), and the cited domains (citations-*.csv). Keep the raw JSON. When you change your aliases later, you can re-score old runs without paying for them again.

The script prints the number of answers per engine and three tables. An excerpt from our run:

Answers per engine (n)
engine
copilot           70
gemini            75
google_ai_mode    74
openai            73

Mention rate
engine                  copilot  gemini  google_ai_mode  openai
brand
HubSpot                    0.64    0.77            0.73    0.79
Pipedrive                  0.47    0.52            0.50    0.47
Salesforce                 0.34    0.37            0.32    0.30
Zoho CRM                   0.36    0.32            0.45    0.48

Average position among tracked brands
engine                  copilot  gemini  google_ai_mode  openai
brand
HubSpot                     1.5     1.5             1.6     1.3
Pipedrive                   2.4     2.4             2.0     2.3

What we found: 25 CRM prompts on four engines

We ran the script on 13 October 2026. Settings: US, websearch: true, every prompt asked 3 times on ChatGPT, Gemini, Google AI Mode and Copilot. That's 300 requests; 292 returned an answer (ChatGPT 73, Gemini 75, Google AI Mode 74, Copilot 70). The 8 that failed were refunded.

Mention rate is the share of answers that name the brand at least once, averaged over the three repetitions:

BrandChatGPT (n=73)Gemini (n=75)Google AI Mode (n=74)Copilot (n=70)All (n=292)
HubSpot79%77%73%64%74%
Pipedrive47%52%50%47%49%
Zoho CRM48%32%45%36%40%
Salesforce30%37%32%34%34%
Freshsales16%11%14%17%14%
monday.com8%15%12%21%14%
Attio12%8%7%3%8%
Copper7%5%7%4%6%
Close5%4%5%4%5%
Insightly0%3%0%13%4%
Microsoft Dynamics 3650%8%0%3%3%

Folk, Capsule, Nutshell and Keap were named in 2% of answers or fewer.

Three things stood out:

  1. The top four hold on every engine. HubSpot, Pipedrive, Zoho CRM and Salesforce lead on all four engines. HubSpot appears in roughly three answers out of four.
  2. Smaller brands depend on the engine. Insightly shows up in 13% of Copilot answers and in none from ChatGPT or Google AI Mode. With about 70 answers per engine, each answer is worth about 1.4 points, so treat gaps of a few points as noise. A gap of this size is worth watching.
  3. Assistants read comparison content, often written by vendors. Gemini cited about 6 sources per answer, Google AI Mode about 9 and Copilot about 11. ChatGPT almost never returned citations. Among the most cited domains were vendor blogs (monday.com, folk.app, nethunt.com, larksuite.com, nutshell.com) next to Forbes, Zapier, G2 and PCMag. Some vendors are cited as sources far more often than they're named as an answer.

How to read the results: if you were Pipedrive

Say you're on the Pipedrive marketing team. Pipedrive is named in 49% of answers, second only to HubSpot. The useful part is where it's missing:

  • Prompts where competitors appear and you don't. Pipedrive was absent from every answer to "best CRM for nonprofits", and in about 10% of answers to "best free CRM for startups", where HubSpot appeared in all of them. It was also nearly absent from "which CRM integrates best with Gmail and Google Workspace".
  • Sources to cover. For the prompts where Pipedrive was weak, engines cited folk.app, copper.com, nethunt.com and nutshell.com most often. Those comparison pages are where engines learn the shortlist.
  • What to write. A clear page on Pipedrive for nonprofits, an honest free-plan comparison, and a Google Workspace integration guide, each with facts an engine can quote: prices, limits, setup steps.

Then run the same prompts next week and see whether those rows move.

Build or buy

If you want a ready dashboard, GEO tools such as Profound, Peec AI and Otterly do this work for you: prompt management, charts, alerts. For many marketing teams that's the right choice.

Build it yourself when you need the raw answers and citations, full control over prompts, engines and countries, or when you're building a GEO tool or an agency service of your own. In that case the LLM Scraper API is the data layer, and the script above is a working starting point.

What it costs

LLM scraping costs 100 credits per request on every engine, and only successful requests are billed. Our run was 25 prompts × 4 engines × 3 repetitions = 300 requests, or 30,000 credits at most (29,200 were billed). A lighter weekly run of 25 prompts × 4 engines, asked once, is 100 requests, or 10,000 credits.

The Free plan includes 1,000 trial credits, granted once per account and valid for one year, with no card required. That's enough for 10 prompts to test the flow. Paid options are on the pricing page. If you also want classic Google results for the same queries, the Search API returns them as JSON for 50 credits per search, from the same balance.

Limits worth knowing

  • Answers vary. Treat a single answer as a sample. Repeat prompts and look at rates over many weeks.
  • Engines change. Models and interfaces update without notice. When a number jumps, read the raw text before you celebrate or panic.
  • Matching is literal. Brands with common-word names need careful aliases and a manual check of a sample.
  • This measures, it doesn't optimize. Tracking tells you where you stand. Improving it is content, documentation and accurate facts across the web: clear pages, structured data and an llms.txt file. Google explains how its AI features use web content in its Search Central documentation.

For the business side of the same problem, see our GEO and AI visibility use case.

The 25 prompts we used

Copy them to repeat the test:

  1. best CRM for small business
  1. best free CRM for startups
  2. cheapest CRM for a 5-person sales team
  3. best CRM with email automation
  4. best CRM for B2B SaaS companies
  5. best CRM for real estate agents
  6. best CRM for nonprofits
  7. best simple CRM for freelancers
  8. best CRM for agencies managing many clients
  9. best CRM with a good mobile app
  10. how do I stop losing track of leads in spreadsheets
  11. which CRM is easiest to set up without an admin
  12. what CRM should I use to manage a sales pipeline for a small team
  13. how can I track customer emails and follow-ups in one place
  14. which CRM integrates best with Gmail and Google Workspace
  15. what is a good CRM for tracking deals on WhatsApp
  16. how do I migrate from spreadsheets to a CRM
  17. HubSpot vs Salesforce for a B2B startup
  18. is Pipedrive good for real estate agents
  19. Zoho CRM vs HubSpot for small business
  20. is Attio a good alternative to Salesforce
  21. monday sales CRM vs Pipedrive
  22. is Close CRM good for outbound sales teams
  23. Copper CRM vs HubSpot for Google Workspace users
  24. Freshsales vs Zoho CRM pricing

FAQ

What is AI visibility?

AI visibility is how often, and how favorably, AI assistants like ChatGPT, Gemini, Copilot and Google AI Mode mention your brand when people ask questions in your category. It's the AI-answer equivalent of search rankings.

How do you track AI visibility?

Send a fixed set of buyer prompts to each engine on a schedule, from the countries you sell in, and record mentions, position and cited sources. Same prompts, same settings, repeated over time.

How do I track brand mentions in ChatGPT?

Capture ChatGPT answers from clean sessions through an API such as the openai engine of the LLM Scraper API, then search each answer for your brand and its aliases with word boundaries. Count the share of answers where you appear and compare it week over week.

How do I measure LLM visibility across countries?

Run the same prompt set once per country by changing proxy_country. Keep each country as its own series. Mixing them hides real differences.

How much does LLM visibility tracking cost?

With the LLM Scraper API, each answer costs 100 credits on any engine and failed requests are free. 25 prompts on 4 engines, asked 3 times, is 30,000 credits per run; asked once, 10,000.

How often should I track AI visibility?

Weekly works for most brands. It's frequent enough to see changes after you publish or fix content, and repeating each prompt keeps the noise of single answers under control. Run daily only around launches.

What are the best tools for tracking LLM visibility?

If you want a dashboard, GEO platforms such as Profound, Peec AI and Otterly are built for it. If you want raw data and full control, an API plus a short script like the one above does the job.


Run it on your own brand. Get an API key: the free tier needs no credit card. Want help with your prompt set? Talk to an engineer.

LLM Scraper APIMeasure your AI visibility, engine by engineCapture what ChatGPT, Gemini, Perplexity and Copilot actually answer, by country, with citations.
Start freeSee the LLM Scraper API pagefrom $2.91 per 1k requests · failed requests free
Carmine CellaFounder & CEO

About the author

Founder & CEO at Datafuel · 6+ years building proxy infrastructure. Owned networks, ethically sourced IPs, and scraping APIs for AI companies, e-commerce, and enterprises

Get started

Ready to build?

Start with the free tier and scale as your project grows. No credit card, no sales call.

Talk to an engineer, not a chatbot.