AnswerLine Start free

, Rank Tracking · Analytics

AI rank tracking: what rank means inside an AI answer

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
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 inlineProducts[].product.position (with include.shopping; shoppingCards[] comes back empty today); Copilot’s shopping cards are coming
Local rank Position of your listing in a map or places module “Where does our location appear for local prompts?” map[].position on ChatGPT and AI Mode; Copilot’s map results are coming
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:

Rank is a choice of list, then a count over runs A single answer holds several ordered lists at once: the order brands appear in the text, sources by position, inline citation chips by position, and products inside shopping cards. Each gives a different rank for the same run, so the definition has to be named. Repeating the same prompt turns rank into a distribution over runs in which the brand is sometimes absent, which is why presence rate is reported beside any conditional rank. one run, four ordered lists, four different ranks mention rank: order in text source rank: sources[].position citation rank: citationPills[] product rank: inlineProducts[] filled cell: where the brand sits in that list 20 runs of one prompt, filled where the brand appeared presence rate 12/20, mean rank when present 1.75 top-3 rate 11/20, mean reciprocal rank 0.43 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4

The run strip is the reason a single conditional average misleads: the empty cells are real observations, and any headline number has to account for them.

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:

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 favors you. Which metric fits depends on the question; the error is reporting conditional rank alone.

Aggregations that handle absence

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
    center = (p + z * z / (2 * n)) / denom
    half = z * sqrt(p * (1 - p) / n + z * z / (4 * n * n)) / denom
    return (center - half, center + 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:

  1. Compare trends within an engine, not levels across engines.
  2. Prefer rate metrics across engines. Presence rate and top-3 rate survive list-length differences better than mean position.
  3. 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.
  4. 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 ρ favors more prompts over more runs
Brand set Fixed and versioned per cluster Mention rank depends on it
Markets country (213 countries on ChatGPT and Gemini, any country on Copilot, Perplexity and Grok; US state on Gemini) Answers differ by market
Include flags include.shopping on ChatGPT when you track product rank Product cards carry their own positions; ChatGPT’s come back empty today
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

  1. Plan tasks. Expand prompt × engine × market × run slot into tasks with an idempotencyKey such as p042-chatgpt-US-2026-09-17-r3, so a resubmission can never double-count.
  2. Submit in batches. POST /v1/async/task/batch accepts 1 to 500 tasks; each item is admitted or refused on its own.
  3. Receive by webhook. Each finished task is posted to your webhook.url with task, credits and response. Deduplicate by task.id.
  4. Store raw JSON. Keep the full result; you will add rank definitions later and want to recompute history.
  5. 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).
  6. 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" },
      "idempotencyKey": "p042-chatgpt-US-2026-09-17-r1",
      "webhook": { "url": "https://hooks.example.com/answers" }
    },
    {
      "taskType": "GEMINI",
      "payload": { "prompt": "best accounting software for a small construction company", "country": "US" },
      "idempotencyKey": "p042-gemini-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}

On ChatGPT the function ranks by the order of entities[]; on the other engines, and on a ChatGPT answer that names no brands, it falls back to the answer text. It ranks only tracked brands, by design.

What it costs

Price per request at the Starter tier: ChatGPT $0.0006 (every include flag free), Gemini $0.0006, AI Mode $0.00206, Google Search $0.00103 and Google News $0.0002 for the first page.

A design of 50 prompts × 3 runs per week on ChatGPT and on Gemini is 50 × 3 × ($0.0006 + $0.0006) = $0.18 per week. A weekly organic check on 150 deduplicated target queries would add 150 × $0.00103 = $0.1545 on Google Search. See cost planning and pricing to size a budget.

Pitfalls

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.

At what level should AI rank be reported?

Over prompt clusters and weeks rather than per prompt per day. Detecting a presence rate moving from 30% to 40% takes about 353 observations per period at 80% power, and repeated runs of one prompt are correlated, so they add less precision than the same number of observations spread across prompts.

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 ChatGPT, Gemini, Perplexity and Copilot, inlineProducts[].product.position on ChatGPT (with include.shopping; shoppingCards comes back empty today), and map[].position on ChatGPT and AI Mode, and organicResults[].position on Google Search. Mention order comes from the answer text, or on ChatGPT from entities[], the brands and products the answer names.

Try it on your own prompts

100 free requests on any engine, no card. One POST returns the answer, sources and citations as JSON.

Keep reading