AI rank tracking: what "rank" means inside an AI answer, and how to measure it
Rank tracking inside AI answers starts with a definition problem. A search results page has one ordered list, so “rank 4” is unambiguous. An AI answer has several ordered things at once: brands named in the prose, sources in a list, inline citation chips, product cards, map listings. And from one run to the next, you may not appear at all.
Below: six rank definitions, how to aggregate when you are absent, the statistics and sample sizes behind a trustworthy number, and the collection pipeline in brief. The full operational build is in the batch pipeline tutorial.
Six definitions of rank
Pick the definition that matches the business question, and name it in every report. Mixing them silently is the most common source of contradictory dashboards.
| Rank type | Definition | Question it answers | Fields in this API |
|---|---|---|---|
| Mention rank | Order of first appearance of your brand among tracked brands in the answer text | “When the answer lists options, where are we?” | text; entities[] order on ChatGPT |
| Source rank | Position of the first source on your domain in the answer’s source list | “Is our page among the first sources?” | sources[].position |
| Inline citation rank | Position of the first inline citation chip pointing to your domain | “Is our page cited inside the prose, and how early?” | citationPills[].position |
| Product rank | Position of your product in a shopping card | “Where do we appear in product recommendations?” | ChatGPT shoppingCards[].products[].position (with include.shopping), Copilot shoppingCards[].products[].position, Perplexity shopping_cards[].products[].position |
| Local rank | Position of your listing in a map or places module | “Where does our location appear for local prompts?” | map[].position on ChatGPT, Copilot and AI Mode |
| Organic rank | Position of your page in Google’s results | “Do we rank for the searches the engine ran?” | organicResults[].position on Google Search |
Notes that matter when you implement them:
- Mention rank needs a brand set. “Position 2” only means something relative to the brands you track. Fix the competitor list per prompt cluster and version it, or ranks shift when you add a competitor.
- Mention rank on non-ChatGPT engines is text order.
entities[]exists only on ChatGPT. Elsewhere, find the first match of each brand’s aliases intextwith word boundaries and sort by offset. - Source lists and inline citations differ. A source can be listed without appearing inline. Treat
citationPills[]as the more visible placement if that distinction matters to you; group entries bycitationPillIdto rebuild each chip. - Organic rank is an input to AI rank. It belongs in the same report because assistants search before answering: ChatGPT returns its searches as
searchQueries[]withinclude.searchQueries, Copilot and Grok returnsearchQueries[], and Perplexity returnssearch_model_queries. Your organic rank on those queries explains part of your source rank. See query fan-out.
The absence problem
A tracked page usually has some position in a results page. In an AI answer, a brand is either present or absent on each run. How you treat those runs decides what your metric says.
A worked example (illustrative numbers)
Twenty runs of one prompt, mention rank among tracked brands:
- Your brand appears in 12 runs, at ranks 1, 1, 2, 1, 3, 2, 1, 4, 2, 1, 1, 2.
- Competitor appears in 19 runs, always at rank 3.
| Metric | Your brand | Competitor |
|---|---|---|
| Presence rate | 12 / 20 = 60% | 19 / 20 = 95% |
| Mean rank when present | 21 / 12 = 1.75 | 3.0 |
| Top-3 rate (all runs) | 11 / 20 = 55% | 19 / 20 = 95% |
| Mean reciprocal rank (all runs) | 8.58 / 20 = 0.43 | 6.33 / 20 = 0.32 |
“Mean rank when present” says you win comfortably. Top-3 rate says the competitor is in the visible top three almost every time and you are there about half the time. Mean reciprocal rank, which weights position 1 heavily, still favours you. Which metric fits depends on the question; the error is reporting conditional rank alone.
Aggregations that handle absence
- Presence rate plus conditional rank. Always reported together. Never show conditional rank without its presence rate beside it.
- Top-k rate. Share of all runs in which you appear at position k or better. Easy to explain; choose k to match what users see (for a list of five recommendations, k = 3 is often meaningful).
- Mean reciprocal rank (MRR). Average of 1/rank over all runs, counting absence as 0. Bounded between 0 and 1, rewards top positions, and needs no arbitrary penalty.
- Penalized mean rank. Assign absent runs a fixed rank (list length + 1). Avoid it unless the list length is fixed: the penalty value drives the result.
Pick one headline metric per report and show presence rate beside it. For a broader metric set (mention share, citation share, recommendation share), see the AI share of voice framework.
Why repeated runs are required
The same prompt, engine and market can give a different answer on each run. Sampling during generation, changes in the pages the engine retrieves, and model updates all move the output; AI answer volatility covers the causes. So a rank for one prompt is a distribution, and you estimate it by sampling.
Intervals on presence and top-k rates
Presence and top-k rates are proportions. For small samples or rates near 0% or 100%, use the Wilson interval rather than the normal approximation:
from math import sqrt
def wilson(successes: int, n: int, z: float = 1.96) -> tuple[float, float]:
if n == 0:
return (0.0, 1.0)
p = successes / n
denom = 1 + z * z / n
centre = (p + z * z / (2 * n)) / denom
half = z * sqrt(p * (1 - p) / n + z * z / (4 * n * n)) / denom
return (centre - half, centre + half)
print(wilson(12, 20)) # (0.387, 0.781)
print(wilson(19, 20)) # (0.764, 0.991)
From the example: your 60% presence over 20 runs is compatible with anything from about 39% to 78%. The competitor’s 95% is 76% to 99%. Twenty runs separate these two, but they would not separate 60% from 70%.
Intervals on MRR and mean rank
MRR and conditional rank are not proportions. Bootstrap them: resample the runs with replacement a few thousand times, recompute the metric each time, and take the 2.5th and 97.5th percentiles. When you aggregate across prompts, resample prompts (with all their runs) rather than individual runs, for the reason in the next section.
import random
def bootstrap_mrr(runs_by_prompt: dict[str, list[int | None]], iters: int = 2000) -> tuple[float, float]:
prompts = list(runs_by_prompt)
stats = []
for _ in range(iters):
sample = [random.choice(prompts) for _ in prompts]
rr = [0.0 if r is None else 1.0 / r for p in sample for r in runs_by_prompt[p]]
stats.append(sum(rr) / len(rr))
stats.sort()
return (stats[int(0.025 * iters)], stats[int(0.975 * iters)])
Detecting a change between two periods
To detect a presence rate moving from p1 to p2 with a two-sided test at 5% significance and 80% power, the observations needed per period are approximately:
n ≈ (1.96 + 0.84)² × (p1(1 − p1) + p2(1 − p2)) / (p2 − p1)²
For 30% to 40%: 7.84 × (0.21 + 0.24) / 0.01 ≈ 353 observations per period. For 30% to 50%, the same formula gives about 90. Small changes need a lot of data, which is why rank reports should be aggregated over prompt clusters and weeks rather than read per prompt per day.
Runs of one prompt are not independent
Twenty runs of one prompt tell you a lot about that prompt and little about the next one. Statistically, observations cluster by prompt, and the effective sample size shrinks by the design effect:
effective n = total observations / (1 + (runs per prompt − 1) × ρ)
where ρ is the intra-prompt correlation of the outcome. With 50 prompts × 8 runs = 400 observations and an illustrative ρ of 0.5, the effective n is 400 / 4.5 ≈ 89. Estimate ρ from your own baseline. When it is high, which is common for stable prompts, adding prompts buys far more precision than adding runs. Sampling vs census covers designs that balance the two.
Comparing ranks across engines
Position numbers are not portable between engines. Engines return different numbers of sources for similar prompts and display them differently, so “source rank 3” is a stronger placement in a short list than in a long one.
Rules that keep comparisons honest:
- Compare trends within an engine, not levels across engines.
- Prefer rate metrics across engines. Presence rate and top-3 rate survive list-length differences better than mean position.
- Record list length per run. Store the count of
sources[]with each observation so you can normalize later (for example, rank divided by list length) if needed. - Keep engine as a dimension in every table. A blended AI rank hides that engines cite different sources for the same prompt.
Choosing the prompt and run design
| Decision | Recommendation | Why |
|---|---|---|
| Unit of reporting | Prompt cluster × engine × market × week | Enough observations for an interval; stable enough to trend |
| Runs per prompt | Measure ρ in a baseline, then choose | High ρ favours more prompts over more runs |
| Brand set | Fixed and versioned per cluster | Mention rank depends on it |
| Markets | country, plus US state where local |
Answers differ by market |
| Add-ons | include.searchQueries on ChatGPT when you analyse organic inputs |
Fan-out queries explain source rank |
| Failed tasks | Keep as rows with no rank | Separates collection gaps from absence |
Prompt set design covers sourcing and clustering prompts; monitoring cadence covers how often to run them.
The collection pipeline, briefly
- Plan tasks. Expand prompt × engine × market × run slot into tasks with an
idempotencyKeysuch asp042-chatgpt-US-2026-09-17-r3, so a resubmission can never double-count. - Submit in batches.
POST /v1/async/task/batchaccepts 1 to 500 tasks; each item is admitted or refused on its own. - Receive by webhook. Each finished task is posted to your
webhook.urlwithtask,creditsandresponse. Deduplicate bytask.id. - Store raw JSON. Keep the full result; you will add rank definitions later and want to recompute history.
- Extract observations. One row per task per brand per rank type:
(prompt_id, engine, market, run_at, brand, rank_type, rank or null, list_length). - Aggregate with intervals. Wilson for rates, bootstrap by prompt for MRR and conditional rank.
A submission of two tasks for one prompt on two engines:
curl -X POST https://api.answerline.dev/v1/async/task/batch \
-H "Authorization: Bearer $ANSWERLINE_API_KEY" \
-H "Content-Type: application/json" \
-d '[
{
"taskType": "CHATGPT",
"payload": { "prompt": "best accounting software for a small construction company", "country": "US", "include": { "searchQueries": true } },
"idempotencyKey": "p042-chatgpt-US-2026-09-17-r1",
"webhook": { "url": "https://hooks.example.com/answers" }
},
{
"taskType": "COPILOT",
"payload": { "prompt": "best accounting software for a small construction company", "country": "US" },
"idempotencyKey": "p042-copilot-US-2026-09-17-r1",
"webhook": { "url": "https://hooks.example.com/answers" }
}
]'
Extracting mention rank from a stored result:
import re
def mention_ranks(result: dict, engine: str, brands: dict[str, list[str]]) -> dict[str, int | None]:
"""brands maps a brand key to its aliases. Returns rank among tracked brands, or None if absent."""
if engine == "CHATGPT" and result.get("entities"):
names = [e["name"] for e in result["entities"]]
first = {}
for i, name in enumerate(names):
for key, aliases in brands.items():
if key not in first and any(a.lower() == name.lower() for a in aliases):
first[key] = i
else:
text = result.get("text", "")
first = {}
for key, aliases in brands.items():
hits = [m.start() for a in aliases for m in re.finditer(rf"\b{re.escape(a)}\b", text, re.IGNORECASE)]
if hits:
first[key] = min(hits)
order = sorted(first, key=first.get)
return {key: (order.index(key) + 1 if key in first else None) for key in brands}
Note that ChatGPT’s entities[] can include entities that are not in your brand set; the function ranks only tracked brands, by design.
What it costs
Credits per async task: ChatGPT 5 (7 with include.searchQueries, include.shopping, include.ads or include.rawResponse, charged once), Copilot 5, Grok 5, Gemini 4, Perplexity 4, AI Mode 4, Google Search 3 for the first page. Synchronous calls add 2.
A design of 60 prompts × 4 runs per week on ChatGPT with search queries and on Copilot is 60 × 4 × (7 + 5) = 2,880 credits per week. Adding a weekly organic check on 150 deduplicated fan-out queries adds 150 × 3 = 450. See cost planning and pricing to size a plan.
Pitfalls
- Conditional rank without presence. The most common way AI rank reports mislead.
- Undefined rank type. “Rank” on a dashboard must say whether it is mention, source, inline citation, product, local or organic rank.
- Per-prompt, per-day readings. The intervals are too wide; aggregate by cluster and week.
- Counting failed tasks as absent. A
FAILEDtask is a missing observation, not a zero. - Changing the brand set silently. Mention ranks shift when you add a competitor; version the set.
- Cross-engine position comparisons. Compare rates across engines and positions within one.
To start collecting observations, see the rank tracking use case or the quickstart.
Questions
Is there a rank inside an AI answer?
Not one rank but several: the order in which brands are named in the text, the position of your page in the source list, the position of an inline citation, and positions in product cards or map listings. Each has to be defined explicitly before it can be tracked.
How do you average rank when a brand is often absent?
Don't average only the runs where it appears, because a brand named once at position 1 looks better than one named every time at position 2. Report presence rate alongside conditional rank, or use a single score that counts absence, such as mean reciprocal rank or top-3 rate.
How many runs per prompt do I need?
It depends on the change you want to detect. A presence rate measured from 20 runs has a 95% interval roughly 40 points wide around 60%. Detecting a move from 30% to 40% at 80% power takes about 353 observations per period, which is usually better spread across many prompts than repeated on one.
Can I compare AI answer rank across engines?
Only with care. Engines return different numbers of sources and lay out citations differently, so position 3 on one engine is not equivalent to position 3 on another. Compare trends within an engine, or use rate-based metrics such as top-3 rate that are less sensitive to list length.
Which fields hold positions in this API?
sources[].position and citationPills[].position on the chat engines and AI Mode, entities[] order on ChatGPT, shoppingCards products position on ChatGPT (with include.shopping) and Copilot, shopping_cards on Perplexity, map[].position on ChatGPT, Copilot and AI Mode, and organicResults[].position on Google Search.