To check whether ChatGPT, Perplexity and Gemini cite your site, you need a fixed set of questions, a fresh chat for every run, repeated runs, and a log you can score later. Single screenshots prove nothing, because the same question can return different sources on consecutive runs. My untimed estimate is a few hours for a first baseline and an hour or two for each re-test, depending on prompt count. It is manual on purpose: it measures what a real person sees.
I have not run it on a real site for this post. The sheet and script below are templates, and the one run of the script uses dummy rows that I typed to show the arithmetic. No result here is a measurement of any engine.
What are you actually measuring?
Pick one question per test: "when someone asks about my topic, does the answer cite my domain?" Keep the other things you could measure (brand mentions, answer accuracy, competitor share) as secondary columns. If you try to answer everything, you will answer nothing.
Be clear about what is outside the test:
- It does not prove why you were or were not cited.
- It does not measure traffic or sales.
- It samples a few dozen questions, not the whole space of questions people ask.
If you are still deciding what to change on your pages, read the GEO guide first. Do the baseline here before you edit, or you cannot tell whether an edit did anything.
How do I build the prompt set?
Write 24 to 40 prompts. Fewer than 20 gives you too little to compare.
Split them into groups so you can see where you win or lose:
| Group | Count | What it looks like | Why include it |
|---|---|---|---|
| Direct topic | 8 to 12 | "How do I [task you have a page about]?" | Pages you built to answer exactly this |
| Comparison or choice | 6 to 8 | "[A] vs [B] for [use case]" | Where engines list several sources |
| Fact or definition | 6 to 8 | "What is [term]?" | Where large reference sites tend to dominate |
| Brand or product | 4 to 6 | "Is [your brand] any good for [use]?" | Checks what is said about you, cited or not |
| Control | 3 to 4 | A topic you have no page on | Shows the baseline for a site with no relevant content |
Rules for writing them:
- Use the words a customer would type. Not your internal page titles.
- One intent per prompt. No compound questions.
- Freeze the wording. Give each prompt an ID (P01, P02, and so on) and never edit it. If a prompt is bad, retire the ID and add a new one.
- Include a few prompts you expect to lose. They keep you honest about the ceiling.
- Do not name your own domain in the prompt, except in the brand group. Naming it leads the engine.
How do I run each test so the results are comparable?
Set the conditions once and write them at the top of the sheet.
- Fresh chat for every run. Do not continue an old conversation, since earlier turns change retrieval.
- Same account state for the series. Either always logged in or always logged out, and note it. Turn memory or personalization features off where the interface lets you, or note that you could not.
- Search on. Where the app has a search or browsing toggle, make sure it is on, because without retrieval there are no citations to count. Note the setting.
- Record the app, the model name the interface shows, the date and the time. The interface labels change, so copy what you see rather than what you expect.
- Same location and language for the whole series.
- Three runs per prompt per engine, in separate fresh chats, ideally spread across the day or on two different days.
- Do not click around in the middle. Copy the answer's sources into the sheet, then close the chat.
For Gemini, note which surface you used (the Gemini app, or Google Search's AI features), because they are different products with different behaviour. I have not verified how each surface chooses sources, so treat them as separate series.
If you also want a server-side signal, check your access logs for the retrieval bots each vendor documents, covered in the crawler allow/block guide. A visit is not a citation, but a site that no bot ever fetches cannot be cited from live retrieval.
What goes in the logging sheet?
One row per run. Copy this into a spreadsheet or a CSV:
| Column | Values | Notes |
|---|---|---|
| date | 2026-10-08 | Date of the run |
| prompt_id | P01 | Frozen ID |
| engine | chatgpt, perplexity, gemini | Add the surface in a notes column |
| run | 1, 2, 3 | Fresh chat each time |
| cited | 1 or 0 | 1 only if your domain appears in the cited sources or links |
| position | integer or blank | Order of your first citation among the listed sources |
| mentioned_no_link | 1 or 0 | Your brand named in text with no link |
| accurate | yes, partly, no, blank | Does the answer about you match your page? Blank if not cited |
| competitors_cited | integer | How many other domains were cited |
| notes | free text | Model name shown, surface, odd behaviour |
Define "cited" before you start and never change it mid-series. My suggested rule: your domain (including subdomains you own) appears as a linked or listed source attached to the answer. A passing mention of your brand in the text is a separate column, since it is a different outcome.
How do I score and read the result?
Compute these per engine, and then per prompt group:
- Citation rate: cited runs divided by total runs.
- Mean position among cited runs. Treat it as rough.
- Stability: for each prompt, whether all three runs agree. Many "unstable" prompts mean your visibility is borderline, not secure.
- Accuracy rate: share of cited runs where the answer about you is right. A citation that misstates your pricing is a problem to fix, not a win.
Save this as summarise.py. It uses only the standard library. I ran it on twelve dummy rows to confirm it works.
import csv
import math
import sys
from collections import defaultdict
def wilson(k, n, z=1.96):
p = k / n
d = 1 + z * z / n
c = p + z * z / (2 * n)
m = z * math.sqrt(p * (1 - p) / n + z * z / (4 * n * n))
return (c - m) / d, (c + m) / d
rows = list(csv.DictReader(open(sys.argv[1])))
by_engine = defaultdict(list)
for r in rows:
by_engine[r["engine"]].append(r)
print(f"{'engine':12} {'runs':>4} {'cited':>5} {'rate':>5} 95% interval mean position")
for engine, rs in sorted(by_engine.items()):
n = len(rs)
k = sum(int(r["cited"]) for r in rs)
lo, hi = wilson(k, n)
pos = [int(r["position"]) for r in rs if r["position"]]
mean_pos = f"{sum(pos) / len(pos):.1f}" if pos else "n/a"
print(f"{engine:12} {n:>4} {k:>5} {k / n:>5.0%} {lo:.0%} to {hi:.0%} {mean_pos}")
by_prompt = defaultdict(list)
for r in rows:
by_prompt[(r["prompt_id"], r["engine"])].append(int(r["cited"]))
print()
for (pid, engine), v in sorted(by_prompt.items()):
label = "stable" if len(set(v)) == 1 else "unstable"
print(f"{pid} {engine:11} {sum(v)}/{len(v)} {label}")
Run it with python3 summarise.py log.csv. Output on my twelve dummy rows (two prompts, two engines, three runs each, invented for the demo):
engine runs cited rate 95% interval mean position
chatgpt 6 2 33% 10% to 70% 3.0
perplexity 6 3 50% 19% to 81% 3.0
P01 chatgpt 2/3 unstable
P01 perplexity 2/3 unstable
P02 chatgpt 0/3 stable
P02 perplexity 1/3 unstable
Look at the interval column. With six runs, 33% and 50% overlap almost completely, so the "difference" between engines is nothing. That is the main lesson of the protocol: small samples produce confident-looking noise.
The interval formula is the Wilson score interval. It assumes runs are independent, which they are not quite (same prompt, same engine), so the real uncertainty is a little wider than shown.
How big a change counts as real?
I ran the interval function on four made-up cases to show how sample size drives it:
cited 3/10 = 30%, 95% interval 11% to 60%
cited 6/10 = 60%, 95% interval 31% to 83%
cited 12/40 = 30%, 95% interval 18% to 45%
cited 24/80 = 30%, 95% interval 21% to 41%
Rough guidance from those numbers:
- At 10 runs, a jump from 30% to 60% still overlaps. Do not celebrate it.
- At 40 runs, the interval is about 27 points wide.
- At 80 runs, it is about 20 points wide.
- So for an edit to show up as a real change, you generally need either many more runs or a large effect. If your before and after intervals overlap, report "no detectable change", not "up 10 points".
Compare before and after on the same prompt IDs, the same conditions and, where possible, the same model name. If the model name shown in the interface changed between tests, say so in your notes. You cannot separate your edit from the model change.
What causes noise, and how do I limit it?
| Source of noise | What happens | What to do |
|---|---|---|
| Sampling randomness | The same prompt gives different sources run to run | Repeat runs, report intervals |
| Model or retrieval updates | Behaviour shifts with no announcement | Log date and model name, keep a control group of prompts |
| Personalization and memory | Your history changes answers | Use a clean account or logged-out state, note it |
| Location and language | Different sources by region | Fix one location for the series |
| Prompt wording | Small changes swap sources | Freeze IDs |
| Your own bias | You count borderline cases generously | Fix the "cited" rule up front, ideally have a second person score a sample |
| Time of day | Different live results | Spread runs, do not compare morning to night |
The control prompts do real work here. If your citations rise on prompts you did not touch, the engine changed, not your pages.
What is the re-test cadence?
- Week 0: baseline, before any edits. Save the CSV untouched.
- Weeks 2 to 4 after a batch of edits: re-run the same prompts. Content needs to be re-fetched before it can matter, and some vendors say crawler changes can take time to apply. For example, OpenAI's crawler page says robots.txt updates can take about 24 hours to be reflected for search results (read 2026-10-08).
- Monthly after that: same set, same conditions.
- Every quarter: review the prompt set. Retire prompts nobody asks any more and add new ones, keeping the old IDs for comparison.
Keep a change log next to the data: date, URL, what you edited. Without it, a rise six weeks later has no explanation and a fall cannot be debugged.
What do I do with the results?
- Cited but inaccurate: fix the source page first, and use the fact-checking checklist for AI answers to verify what the engine said. A citation that misrepresents you is worse than none.
- Competitors cited, you not: open the cited pages and compare. Do they answer the question in the first two sentences? Do they link primary sources? That is a content gap, not a technical one.
- Not cited and never fetched: check access. Is the bot blocked, or the page behind a login or heavy client-side rendering?
- Cited on some runs only: you are on the edge. Strengthen the page, then re-test.
- Nothing moves after real edits: accept it. Your content may not be the deciding factor for that query, and the engine may favour reference sites you cannot displace.
Write the findings up with the dates, the intervals and the caveats. If a summary says "we are up 20%" without the run counts, nobody can trust it, including you in three months.



