AI

What GEO is, and how to actually measure it

Generative engine optimisation has no rank to track. A measurement design that survives non-determinism: share of answer, denominators, sample size, consensus.

8 min read
On this page

GEO — generative engine optimisation — is the practice of trying to be the source an AI assistant cites when it answers a question in your category. The name is new, the tooling is loud, and most of what is sold under it is a dashboard that samples a few prompts and renders a percentage with no denominator and no sample size attached.

The underlying problem is real, though. If a growing share of your category's questions get answered without a click, "position 3 for our head term" stops describing your visibility. What replaces it is harder to measure than rank, for a reason worth stating plainly: there is no rank to measure.

This article is about measurement design. It is deliberately more about statistics than about tools, because the failure mode in this space is not bad tooling, it is confidently reported numbers that cannot mean what they claim.

Why rank tracking does not transfer #

Classic rank tracking rests on assumptions that generative surfaces break:

assumptionstill true?
There is an ordered list of resultsNo. There is prose with some citations attached.
The same query gives the same resultNo. Sampling twice can give different answers and different sources.
Position 1 is unambiguously better than 3Unclear. Being cited mid-answer may beat being the last link.
One query = one measurementNo. The phrasing of the question changes the answer materially.
The result set is finite and observablePartly. You observe what one sample returned, not what the model would say.

Only the last one has a workaround, and it is the whole trick: stop trying to observe a state, and start estimating a distribution. You are not reading a ranking, you are sampling a random variable. Everything else follows from taking that seriously.

The metric: share of answer #

The one metric worth building on is share of answer: across the answers you sampled, what fraction of all citations went to each domain.

share_of_answer(domain) = citations_to_domain / citations_to_all_domains

Simple to compute and easy to get wrong in four specific ways.

Define the denominator, and say it out loud. Citations to all domains, or citations to domains in your competitive set? Both are defensible; they give very different numbers. Publishing one and letting people assume the other is how these dashboards mislead.

Decide whether repeats count. A domain cited three times in one answer: one citation or three? "One per answer" (presence) is more stable and usually what people mean by visibility. "Every mention" (frequency) rewards being quoted repeatedly. Pick one, write it down, never mix them in a chart.

Decide what a surface is worth. An organic result, an AI Overview citation and a Perplexity source are not equivalent, but any weighting you apply is a business judgement, not a measurement. Weight them if you must — and publish the unweighted numbers next to the weighted ones.

Count in code, not with a model. If a language model produces your percentages, the same inputs can give different numbers on different days and you can never reproduce a figure someone questions. Extract citations with a model if you like; compute arithmetic with arithmetic.

Sample size is the whole ballgame #

Because each answer is a draw from a distribution, a single sample carries almost no information. A domain that appears in one of one samples is not at 100% visibility; you have one observation.

Three rules that keep the numbers honest:

  1. Never report a percentage without its n. "42% share of answer (n=50 answers, 5 prompt variants × 10 samples)" is a finding. "42% share of answer" is a decoration.
  2. Sample prompt variants, not just repeats. The way a question is phrased changes which sources get cited, so repeating one phrasing 50 times measures that phrasing precisely and your category badly. Write 5–10 realistic phrasings per topic and sample across them.
  3. Treat small differences as noise until proven otherwise. With modest sample sizes, a change from 18% to 22% is very likely nothing. If you are going to act on differences, compute a confidence interval for a proportion; if that is too much machinery, at minimum require a change to persist across several measurement runs before you respond to it.

Keep every raw answer. When someone challenges a number months later, the only defensible response is the payloads.

Surfaces to cover #

A complete picture needs more than one engine, because they disagree — which is itself the finding. Third-party analysis from Digital Applied reports that only around 11% of domains are cited by both ChatGPT and Perplexity, and that roughly 2.1% of pages in Google's top ten also appear in ChatGPT citations. Those are their measurements, not ours. If figures anywhere near that hold on your keyword set, then measuring one engine and calling it "AI visibility" is measuring one engine.

The surfaces worth sampling:

  • Organic SERP — the baseline. Without it you cannot tell whether an AI citation gap is an AI problem or a "you do not rank anywhere" problem.
  • AI Overview — Google's in-SERP block, comparable directly against the ranking under it. Covered in reading AI Overviews programmatically.
  • AI Mode — Google's conversational surface. Extra cost, lower trigger rate; add it deliberately.
  • LLM assistants — ChatGPT, Claude, Gemini, Perplexity. Each with and without web search enabled, because those are different products with different citation behaviour.

Polling one assistant directly:

curl -X POST https://api.dataswap.io/v1/ai/llm-responses \
  -H "Authorization: Bearer $DATASWAP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "perplexity",
    "user_prompt": "What is the best CRM for a 10-person startup?",
    "web_search": true
  }'

The response carries message_content, input_tokens / output_tokens, and web_search_results (title, url, domain) when web search ran. provider accepts chat_gpt, claude, gemini or perplexity, and model_name pins a specific model — which you should always do, for reasons in the pitfalls below.

Composing the whole landscape in one call #

Running organic, AI Overview, AI Mode and several assistants for one query, then normalising domains and computing shares, is a few hundred lines of glue and a lot of edge cases. POST /v1/geo/answer-landscape is that composition:

import { Dataswap } from 'dataswap';

const dataswap = new Dataswap({ apiKey: process.env.DATASWAP_API_KEY });

const land = await dataswap.geo.answerLandscape({
  query: 'best crm for startups',
  engines: ['gpt-4o-mini', 'gemini-2.0-flash'],
  include_ai_mode: false,
  gl: 'us',
  hl: 'en',
});

for (const row of land.share_of_answer) {
  console.log(row.domain, row.share_pct, row.surfaces.join('+'));
}

console.log('consensus', land.consensus.score, land.consensus.agreed_domains);
console.log('disagreement', land.divergence);

if (land.partial) console.warn('surfaces missing:', land.unavailable);
land = client.geo_answer_landscape(
    "best crm for startups",
    engines=["gpt-4o-mini", "gemini-2.0-flash"],
    gl="us",
)
for row in land["share_of_answer"]:
    print(row["domain"], row["share_pct"], row["surfaces"])

Three parts of the response matter more than the headline number:

  • consensus.score (0–1) — how much the surfaces cite the same domains. Low consensus means your category has no settled answer yet, which is both an opportunity and a warning that any single-engine number is unrepresentative.
  • divergence — where the surfaces disagree. Often more actionable than the share itself: it names the claims that are still contested.
  • surfaces — the raw organic results, AI Overview, AI Mode and LLM answers everything was computed from. Without the raw material you cannot audit a share figure, and an unauditable metric is one you should not act on.

engines: [] is valid and is the cheap mode — Google surfaces only, no paid assistant calls. Omit the field entirely to get the defaults instead; the empty array means zero, not "default". If a surface is unavailable the call still succeeds with partial: true and an unavailable list, rather than silently returning a share computed over fewer surfaces than you asked for. Read partial before quoting any percentage — a share over three surfaces is not comparable with a share over five.

Cost scales with the number of engines, since each is a real call. Intelligence-layer routes reserve a ceiling and seal at the actual cost, and credits_used in the response is what was charged.

Limits and failure modes #

Model versions drift under you. "GPT-4o" in March and in September are not the same weights. Always pin model_name, record it with every data point, and treat a version change as a break in the series — not a trend. A visibility "drop" that coincides with a provider's model update is a measurement artefact until proven otherwise.

Personalisation and geography move the answer. Assistant answers vary with location and account context. You are measuring one configuration. State it.

Citation is not recommendation. Being cited as a source is not the same as being recommended, and being named in prose without a link is invisible to citation-based counting but very visible to a reader. If reputation is what you care about, you need sentiment and mention extraction as well as citation counting — they answer different questions.

Web search on/off is two different products. An assistant with retrieval cites live pages; the same assistant without it answers from training data and may cite nothing. Never pool the two.

Prompt phrasing is a confounder, not a detail. Small rewordings shift citation sets. This is why variant sampling is mandatory rather than a refinement.

The category is young and the vendors are loud. Estimates of the GEO market's size circulate widely; they are analyst projections, not measurements of your funnel. The only number that should change your roadmap is one you computed from your own keyword set, with an n next to it.

No tool observes the model, only its outputs. Everything here — ours included — samples answers. Nobody has a rank table for a generative engine, and any product implying otherwise is describing a sampling process without saying so.

The short version #

Treat AI visibility as an estimation problem. Define the denominator, count presence or frequency but never both, sample multiple phrasings, always report n, pin model versions, keep raw payloads, and compute the arithmetic in code. Do that and you have a metric you can defend in a meeting. Skip it and you have a percentage.

If you want the surfaces without building the composition, /v1/geo/answer-landscape returns share of answer, consensus, divergence and the raw surfaces in one call — see AI search visibility. The docs cover the credit envelope and how partial results are reported.

More reading

3 min read

AI crawlers read your HTML, not your JavaScript

Before you measure whether an AI assistant cites you, check whether it can read you at all. The test is one curl command, and the result is often uncomfortable.

  • ai-visibility
  • crawling
  • geo
8 min read

Grounding an LLM agent with search data

Search results are the wrong shape for a prompt. How to turn a SERP into grounding context an agent can use: dedupe, rerank, budget tokens, keep citations.

  • grounding
  • rag
  • llm-agents
  • search-api
  • context-window