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
AI
Generative engine optimisation has no rank to track. A measurement design that survives non-determinism: share of answer, denominators, sample size, consensus.
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.
Classic rank tracking rests on assumptions that generative surfaces break:
| assumption | still true? |
|---|---|
| There is an ordered list of results | No. There is prose with some citations attached. |
| The same query gives the same result | No. Sampling twice can give different answers and different sources. |
| Position 1 is unambiguously better than 3 | Unclear. Being cited mid-answer may beat being the last link. |
| One query = one measurement | No. The phrasing of the question changes the answer materially. |
| The result set is finite and observable | Partly. 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 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_domainsSimple 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.
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:
n. "42% share of answer (n=50 answers, 5 prompt variants × 10 samples)" is a finding. "42% share of answer" is a decoration.Keep every raw answer. When someone challenges a number months later, the only defensible response is the payloads.
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:
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.
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.
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.
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.
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.
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.