Tutorial2026-07-136 min read

How to track competitor visibility in Perplexity

Tracking competitor visibility in Perplexity means running the questions your buyers actually ask and recording which rivals come back. Because Perplexity searches before it writes, you get something the chatbot surfaces can't hand you: the URL of the page each rival is winning on. One query is a browser tab. A query set, every week, with the citation list intact and last month's runs to subtract from this month's, is one POST to https://api.agentgeo.org/v1/fetches and about thirty lines of code. This guide covers the manual sweep, the API version in curl and Python, the same job handed to an agent over MCP, and the five ways competitor data quietly goes wrong.

Tutorial

To track competitor visibility in Perplexity, run your category questions and record two things per rival: whether the answer names them in answerText, and whether their domain appears in sources[]. Do it once by hand to learn the shape of your category. Do it on a schedule with one API call per query, because the finding is never a single answer — it's which names entered and left the set since last time.

The manual way (and why it stops working)

Open Perplexity and ask the question a buyer would ask — "best help desk software for a SaaS support team", not "tell me about Acme". Read the answer and write down every company it names. Then do the part that makes this engine worth checking at all: scroll to the numbered sources and note whose domains are there, in order. Repeat across three or four phrasings and you'll know both the competitive set and the pages holding it up. It costs an hour and nothing else, and it's the best hour you'll spend on this.

The trouble starts on the second pass.

  • One question per tab. A category has twenty or thirty buying questions behind it, and Perplexity's source lists run long enough that reading each one properly is slow. You'll quietly narrow the sweep to the three queries you like, which is exactly how a blind spot forms.
  • Every ask is a fresh search. Perplexity retrieves before it writes, so the cited set genuinely moves between runs. One reading is a sample of something in motion, and it can't separate a real entrance from ordinary churn.
  • No history, so no diff. The interesting question is whether Globex has appeared in four more of your queries than it did in June. Answering it needs June stored somewhere, and a browser tab stores nothing.
  • Citations lose their order in transit. Copy a source list into a sheet and the positions scramble or the URLs disappear behind titles. Cited at 1 and cited at 9 are different results, and position is the first casualty of copy-paste.
  • It never reaches a report. A hand-typed list of names isn't a chart in a client deck, a row in a table, or a job that pings you the week a new competitor enters the set.

Do it with one API call

AgentGEO fetches the answer and returns the raw record: the full text, plus every cited source with its title, URL and position. One POST, and the same envelope across all six engines — here it's just perplexity.

curl -X POST https://api.agentgeo.org/v1/fetches \
  -H "Authorization: Bearer ag_live_..." \
  -H "Content-Type: application/json" \
  -d '{"query": "best help desk software for a SaaS support team", "surfaces": ["perplexity"]}'

What comes back is a plain JSON envelope — no HTML, no scraping artifacts. Notice where the competitive intelligence actually sits: not only in the prose, but in whose pages the engine read in order to write it.

{
  "id": "run_84c1f2ab91d3",
  "query": "best help desk software for a SaaS support team",
  "surfaces": ["perplexity"],
  "status": "completed",
  "recordsDelivered": 1,
  "creditsCharged": 1,
  "answers": [
    {
      "surfaceKey": "perplexity",
      "status": "delivered",
      "answerText": "For a SaaS support team Globex is the tool most often recommended for automation depth, while Initech is favoured by smaller teams that want a shared inbox and little else ...",
      "sources": [
        { "title": "Globex Help Desk — pricing and plans", "url": "https://globex.com/pricing", "position": 1 },
        { "title": "Globex vs Initech for SaaS support", "url": "https://globex.com/compare/initech", "position": 2 },
        { "title": "Best help desk software in 2026", "url": "https://example.com/best-help-desk", "position": 3 }
      ],
      "fetchedAt": "2026-07-13T08:41:19Z",
      "latencyMs": 10420
    }
  ]
}

The scan itself is short. Keep the competitive set in one dict — display name to root domain — and record both places a rival can turn up: named in the prose, and cited underneath it.

import requests
from collections import defaultdict
from urllib.parse import urlparse

KEY = "ag_live_your_key_here"

# The competitive set you track: display name -> root domain.
RIVALS = {
    "Acme": "acme.com",
    "Globex": "globex.com",
    "Initech": "initech.com",
}

QUERIES = [
    "best help desk software for a SaaS support team",
    "help desk with a shared inbox and SLA tracking",
    "help desk software that scales past 20 agents",
]


def host(url):
    # Normalise so globex.com and www.globex.com are the same company.
    h = urlparse(url).netloc.lower()
    return h[4:] if h.startswith("www.") else h


named = defaultdict(list)   # brand -> queries whose prose named it
cited = defaultdict(list)   # brand -> (query, position, url) it was cited for

for query in QUERIES:
    resp = requests.post(
        "https://api.agentgeo.org/v1/fetches",
        json={"query": query, "surfaces": ["perplexity"]},
        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")
            continue

        text = (answer.get("answerText") or "").lower()
        sources = answer.get("sources") or []

        for brand, domain in RIVALS.items():
            if brand.lower() in text:
                named[brand].append(query)
            for src in sources:
                h = host(src["url"])
                if h == domain or h.endswith("." + domain):
                    cited[brand].append((query, src["position"], src["url"]))

for brand in RIVALS:
    print(f"\n{brand}")
    print(f"  named in {len(named[brand])}/{len(QUERIES)} answers")
    if not cited[brand]:
        print("  cited: never")
        continue
    print(f"  cited {len(cited[brand])} time(s) - the pages doing the work:")
    for query, position, url in sorted(cited[brand], key=lambda row: row[1]):
        print(f"    #{position}  {url}   ({query})")

Run that over your query set and you get a per-rival summary: how many answers named them, and the exact pages that carried them there. Each delivered record costs one credit, and records that come back failed cost nothing — so a three-query sweep against one surface is three credits at most. While you're shaping the parser, an ag_test_ key returns clearly labelled demo records at zero credits.

The URLs are the point. Read the column and each entry tells you something different about how you're losing the query:

The page Perplexity citedWhat it tells youThe move
globex.com/pricingThe engine is using a rival's own pricing page as the reference for what this category costs.Publish pricing a model can quote — tiers, limits and numbers in text, not behind a sales call.
globex.com/compare/initechA competitor wrote the head-to-head, and now owns how the whole category gets framed.Write the comparison they haven't: yours against theirs, on your own domain.
A third-party roundupNo vendor is winning this query — an editorial list is doing the work instead.Get into that roundup, or out-answer it with a page narrower and more specific than a list of ten.
A forum or community threadThe engine fell back on opinion because nobody published a real answer.The gap is written out for you in the thread's question. Answer it somewhere you control.
Your own domain, at position 7You're inside the retrieval set but doing very little of the writing.A position problem, not a coverage one — compare your page against whatever sits at 1 to 3.

This is what makes competitor tracking on Perplexity different in kind rather than just in volume. Because it searches before it writes, the answer arrives with its evidence attached: you don't only learn that Globex owns the query, you get the address of the page that won it. The citation list is a ranked shortlist of the content beating you — in order, specific, and small enough to act on this week. On a chatbot surface you'd be guessing at brand strength; here you're reading a document.

Let your agent do it

Citations point at pages, and pages are something an agent can go and read — which is why this job belongs in an agent rather than a spreadsheet. Wire the server into Claude Code once:

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, with query and surfaces required and country defaulting to "US". After that the sweep is a sentence:

Run our four help desk queries through Perplexity. For each of Acme, Globex and Initech tell me whether the answer names them and whether their domain is cited. Then open the top-cited competitor page and tell me what it covers that ours doesn't.

The records arrive raw. Whether Globex turning up in two extra queries is a trend or a wobble is a judgment your agent makes with your context — AgentGEO ranks nothing, scores nothing and draws no conclusions, by design. If you'd rather script it, the Python and curl paths hit the identical endpoint.

Common problems

Five things that make competitor data read wrong, all fixable in the parser or the schedule:

  • Match domains in sources[], brand names in answerText. A third-party review titled "Globex vs Initech" matches a title search for both and was written by neither. Compare the host in url for citations, and use a word-boundary regex — re.search(r"\bglobex\b", text, re.I) — for mentions.
  • One run is a sample. Perplexity retrieves fresh on every ask, so the cited set moves on its own. Fetch the same query twice, or on two consecutive days, before you report that a rival entered or left the set.
  • Decide early how a domain cited twice counts. Perplexity often cites several pages from one site. Counting them once per answer and counting each appearance are both defensible; switching convention mid-quarter silently rewrites every number you already reported.
  • Phrasing changes the retrieval, not just the wording. "Best help desk software" and "help desk tools for support teams" can pull different pages and a different competitive set. Freeze the query set, and treat any edit to it as the start of a new baseline rather than a continuation of the old one.
  • Long scrapes come back failed. A request waits up to 180 seconds; a scrape that runs past that budget returns status: "failed" at zero credits with a providerFields.snapshot_id. A follow-up call with that id and the same single surface redeems the finished answer instead of paying to scrape it again.

Summary

For a one-off read on who Perplexity favours, ask it yourself — the source list is right there and it costs nothing. When the sweep has to cover a query set, repeat weekly, and produce something you can subtract last month from, fetch it instead: one call per query returns the prose and the ordered citation list, thirty lines of Python turn that into a per-rival grid, and the URLs in that grid are the real output — a short list of pages you can go and beat.

See which rivals Perplexity names — and which of their pages it reads. Start free — no credit card and run your first sweep in a couple of minutes.

Start for free

Frequently asked questions

How do I see which competitors Perplexity recommends?
Ask Perplexity the category question your buyers ask — "best help desk software for a SaaS support team" rather than a branded query — and write down every company it names, plus the domains in the numbered source list. For repeatable tracking, POST the same query to https://api.agentgeo.org/v1/fetches with "surfaces": ["perplexity"] and scan answerText for each rival's name and sources[] for each rival's domain.
Can I track competitor mentions in Perplexity automatically?
Yes. One HTTP call returns the answer and its citation list as JSON, so a short script can loop your query set, test every competitor's name and domain, and write a grid to a table on a schedule. You can also connect the agentgeo-mcp server and have an AI agent run the same sweep from a plain-language instruction, with no parser to maintain.
Does Perplexity show which pages it used for an answer?
Effectively always. Perplexity is search-grounded — it retrieves, then writes from what it found — so a numbered source list accompanies essentially every answer. AgentGEO returns those as a structured sources[] array with title, url and position on each entry, which means you can see which competitor page fed the answer rather than only which competitor was named.
Is competitor tracking in Perplexity different from ChatGPT?
The scan is identical, but the evidence is much denser. A chatbot only links pages when it happens to browse, so competitor citations there are sparse; Perplexity cites on nearly every answer, which turns a brand-level finding into a page-level one. The surfaces array takes one to six engine keys, so you can run both and keep the citation columns in separate tables.
How much does it cost to track competitors across a query set?
One delivered record costs one credit, so a twenty-query sweep against a single engine is twenty credits, and failed records cost nothing. The free tier needs no credit card, paid usage is billed by consumption with a spend cap rather than per seat, and ag_test_ keys return labelled demo records at zero credits while you build the script.

Keep reading