Tutorial2026-07-156 min read

How to track competitor visibility in Gemini

Tracking competitor visibility in Gemini means asking your category question and recording which brands come back — but on this surface the recording has to happen in the prose. Gemini links pages only when it grounds an answer in a live search, so a perfectly good record can arrive with an empty sources[], and a tracker built around citations will quietly report an empty category. The names are still there. They're just in the text. POST the query to https://api.agentgeo.org/v1/fetches with "surfaces": ["gemini"], scan answerText against your list, and what you get back is the roster — the set of companies Gemini treats as the answer to your category. Watch the roster rather than your own row: a name entering or leaving it is the event worth reporting.

Tutorial

To track competitor visibility in Gemini, ask the question a buyer would ask and record every brand the answer names. One call to https://api.agentgeo.org/v1/fetches with "surfaces": ["gemini"] returns that answer as text your code can scan against a list of competitors. Run it across a query set, store each run, and the thing you're really tracking comes into view — not whether you were named on one particular morning, but which companies Gemini keeps naming together.

The manual way (and why it stops working)

Open Gemini and ask the way a buyer asks — "best email marketing platform for ecommerce", not "tell me about Acme". Read the whole answer and write down every company it names. Then push: "what are the alternatives?", "which of those is cheapest?" Each follow-up tends to surface a name the first answer left out. Three or four phrasings in, you have a rough map of the competitive set Gemini holds for your category, and you'll have learned more about how the model describes your market than any dashboard would have told you.

The map is real and worth having. It just doesn't survive a reporting cadence:

  • One question at a time. A category has twenty or thirty buying questions behind it, and each returns a slightly different cast. Typing them out and transcribing names is an afternoon — one you have to repeat identically next month, or the comparison means nothing.
  • The roster moves between runs. Ask the same thing twice and a name drops or a new one appears. A single reading can't separate genuine movement from ordinary variation, so every manual finding arrives with an asterisk you have no way to resolve.
  • Nothing is stored. The question you'll actually want answered is whether Globex has started appearing in four more of your queries than it did in June. That needs June's answers on disk, and a chat window is not a dataset.
  • There's often no citation list to lean on. Gemini links pages when it searches and simply doesn't when it doesn't. If your process is "check the sources for competitor domains", it returns nothing on every day the model answers from what it already knows.
  • Nothing downstream can consume it. Names in a notebook won't render a chart for the client deck, won't populate a column anywhere, and certainly won't message you the week an unfamiliar company turns up in the set.

Do it with one API call

AgentGEO runs the fetch on a managed-scraper engine and hands back the raw record: the full answer text, plus whatever sources the engine attached. One POST, one JSON body, the same shape across all six surfaces — here it's just gemini.

curl -X POST https://api.agentgeo.org/v1/fetches \
  -H "Authorization: Bearer ag_live_..." \
  -H "Content-Type: application/json" \
  -d '{"query": "best email marketing platform for ecommerce", "surfaces": ["gemini"], "country": "US"}'

Look closely at what comes back, because the empty array is the whole reason this guide reads differently from the ChatGPT and Perplexity versions:

{
  "id": "run_5f2a7c40be19",
  "query": "best email marketing platform for ecommerce",
  "surfaces": ["gemini"],
  "status": "completed",
  "recordsDelivered": 1,
  "creditsCharged": 1,
  "answers": [
    {
      "surfaceKey": "gemini",
      "status": "delivered",
      "answerText": "For an ecommerce store the platforms usually suggested are Globex, which has the deepest automation, Acme, which is the quickest to connect to a storefront, and Initech for teams that already run their own templates ...",
      "sources": [],
      "fetchedAt": "2026-07-15T10:22:41Z",
      "latencyMs": 24310
    }
  ]
}

status is delivered, the answer is complete, and sources is empty — because the model wrote that paragraph without going and reading anything. So the scan has to run over the prose. Keep your competitive set in one list and match each name with a word boundary:

import re
import requests
from collections import Counter

KEY = "ag_live_your_key_here"

# The roster you watch. Display names are the unit here - there may be no
# citation list to match domains against.
BRANDS = ["Acme", "Globex", "Initech"]

QUERIES = [
    "best email marketing platform for ecommerce",
    "email marketing tools with abandoned cart automation",
    "affordable email marketing software for a small online store",
]

PATTERNS = {b: re.compile(rf"\b{re.escape(b)}\b", re.I) for b in BRANDS}

roster = Counter()  # how many answers named each brand
counted = 0         # how many answers we actually got back

for query in QUERIES:
    resp = requests.post(
        "https://api.agentgeo.org/v1/fetches",
        # Gemini takes `country`. A `language` field is rejected by the chatbot
        # scraper, so it is deliberately not forwarded on this surface.
        json={"query": query, "surfaces": ["gemini"], "country": "US"},
        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(f"{query!r}: no record this run (0 credits)")
            continue

        counted += 1
        text = answer.get("answerText") or ""
        named = [b for b in BRANDS if PATTERNS[b].search(text)]
        roster.update(named)

        print(f"\n{query!r}")
        print(f"  grounded: {bool(answer.get('sources'))}")
        print(f"  named:    {', '.join(named) or 'nobody on the list'}")

print(f"\nroster across {counted} answers")
for brand in BRANDS:
    print(f"  {brand:<10} {roster[brand]}/{counted}")

The output is a small roster: who Gemini named, in how many of your queries. Keep the grounded flag next to it — an answer with sources is one the model assembled from pages it just read, and an answer without them is one it produced from what it already knows. Those two are worth reading differently even when the brand list is identical. Three queries against one surface is three delivered records, so three credits; anything that comes back failed costs nothing.

Track the set, not your own row. Whether Gemini named Acme in one answer on one morning is close to a coin flip and there's nothing you can do with it. Which companies it names together, across your whole query set, is a stable picture of how the model has learned to answer your category — and on a surface where sources[] is frequently empty, that prose roster is the only competitive signal available. Keep every run on disk, and let the reportable event be a name arriving in the set or falling out of it.

What changed in the rosterWhat it usually meansWhat to do next
A name you don't track appears in several queriesA new entrant has reached the model's answer for your categoryAdd it to BRANDS, then read the answer text to see how it's being described
A tracked name enters one query, then leavesOrdinary run-to-run variation, not movementRe-run before you report it — one answer is a sample
A competitor appears in four more queries than last monthReal drift in the competitive setLook at what changed on the web about them; the model is downstream of that
You're in the roster but never described firstYou're in the consideration set without owning itGive the model one crisp, quotable differentiator to reach for
The roster is stable and you're not in itA coverage gap, not a tracking bugPublish the page that answers this exact question and wait a cycle

Let your agent do it

The endpoint is reachable over MCP as well, which means an agent that already has your competitor list in context can carry out the sweep without a parser existing anywhere. One command puts it in Claude Code:

claude mcp add agentgeo -- npx -y agentgeo-mcp --key ag_live_...

agentgeo-mcp runs on stdio, carries no npm dependencies whatsoever, and asks only for Node.js 18+. A single tool sits behind it, fetch_raw_answers, returning records exactly as they arrived. After that, the check is a sentence:

Run our three ecommerce email queries through gemini. For each answer, list every company it names, then tell me which names appear in all three — and flag anyone who isn't already Acme, Globex or Initech.

That last clause is the part a fixed script can't do: a name-matching scan only ever finds names you already knew about, while a model reading the prose will notice the entrant nobody has added to the tracker yet. The records themselves arrive untouched. Nothing is ranked, nothing is scored, and no conclusion is drawn on your behalf — that's the design, not a missing feature. Whether Globex turning up in two extra queries deserves anyone's attention is a call your agent makes with context AgentGEO doesn't have.

Common problems

Most bad Gemini competitor data comes from five places, and all five are fixable in the parser or the schedule:

  • An empty sources[] is not a failed fetch. Gemini cites only when it grounds an answer in a live search; when it answers from what it already knows, a complete delivered record carries no citations at all. Decide whether a run counted by reading status, never by measuring the length of the source list.
  • language is not forwarded on this surface. The chatbot scraper behind Gemini accepts prompt and country only, and rejects a language field outright, so market targeting here happens through country. On google_ai_overview it's the opposite — language drives the search hl= parameter and genuinely changes the answer you get.
  • The web_search toggle does nothing here. Only chatgpt honours it. Setting it on a Gemini call will not force the model to go and browse, so don't build a process that assumes you can switch citations on when you need them.
  • Your list is the ceiling. A name scan can only find names you thought of. Every few runs, read two or three raw answerText values end to end — new entrants appear in the prose long before anyone remembers to add them to the tracker. Use word boundaries too: a bare substring test for a short brand fires on unrelated words and on companies that merely share a prefix.
  • A failed record is sometimes just a slow one. Calls are held for up to 180 seconds; a scrape still running when that expires comes back failed, costs nothing, and carries a providerFields.snapshot_id. Treat that id as a claim ticket — reissue the call with it and the same single surface, and the completed answer arrives without a second scrape being paid for.

Summary

For a one-off gut check, ask Gemini yourself and write the names down — it's free and it takes a minute. The moment the check has to repeat, span a query set, or land in a report, fetch the answers as data: one call per query, a word-boundary scan of answerText, and stored runs. Because this surface so often answers without citing anything, the roster in the prose is the competitive signal, and a name entering or leaving it is the only movement worth acting on.

See which competitors Gemini names alongside you. Start free — no credit card and record your first roster today.

Start for free

Frequently asked questions

How do I see which competitors Gemini recommends?
Ask the category question rather than a branded one — "best email marketing platform for ecommerce", not "tell me about Acme" — and write down the companies it lists. To make that repeatable, send the query to https://api.agentgeo.org/v1/fetches with "surfaces": ["gemini"] and run each name from your competitive set against the returned answerText, anchored to word boundaries so short brands don't over-report.
Can I track Gemini brand mentions automatically?
Yes. A single HTTP call hands back the answer as JSON, so twenty lines of script can walk the query set, look for each competitor and drop the roster into a table on whatever cadence suits you. If you'd rather not own a parser at all, connect agentgeo-mcp and let an agent run the same sweep from an instruction written in English.
Why doesn't Gemini show sources for its answers?
Because it only cites when it grounds the answer in a live search. When it replies from what it already absorbed, there is nothing to link, and the record legitimately comes back delivered with an empty sources[]. That's why competitor tracking on this surface has to read the prose: the brand list is in the text whether or not any pages were cited.
Why does Gemini name different competitors each time I ask?
Generated answers vary between runs, and the model may or may not search on any given call, so the shortlist genuinely moves. Treat one fetch as a sample rather than a measurement: run the same query repeatedly, store every run, and judge drift across the whole query set instead of reacting to a single answer.
How much does it cost to track competitors in Gemini?
Credits are charged per delivered record — one each — so twenty queries against Gemini costs at most twenty credits, and any that fail are free. Getting started needs no credit card. Past the free tier you pay for what you consume against a cap you set yourself, never per seat, and ag_test_ keys return obviously-labelled demo records at no cost while the script is still being written.

Keep reading