When ChatGPT answers with web search, it does not have to search your prompt word for word. It can run several searches, narrower, rephrased or with added context, and its cited sources come from what those searches return. That set of searches is the query fan-out. ChatGPT does not show it to users, and on the ChatGPT answers this API serves today the searchQueries field comes back empty. What you can see is the result of the fan-out: the pages cited, in order, and the answer text. This post covers how to reason about the fan-out from those.
Google uses the same term for AI Mode: its announcement describes AI Mode breaking a question into subtopics and running many searches at once with a "query fan-out technique" (Google, May 20, 2025, checked 2026-09-17).
Definitions
- Query fan-out: the searches an AI assistant runs to answer one prompt.
-
Fan-out query: one search in that set. The API field for them is
result.searchQueries, empty on the ChatGPT answers served today. - Source frequency: the number of answers in a prompt set that cite a given domain.
- Inferred search: a search you work out from the cited pages and the answer text. It is a hypothesis, not returned data.
A single request carrying one prompt fans out into several web searches, which ChatGPT runs but does not return today. The pages those searches return feed one answer, which comes back as result.text together with sources and citationPills, each entry carrying a url and a label.
POST /v1/monitor/chatgpt
one prompt
one request
search 1
search 2
search 3
search 4
result.text
sources[]
url, label
citationPills[]
url, domain
searches ChatGPT ran: not returned today
The searches in the middle column are what the answer was actually built from, so they, not the prompt's own wording, are the queries a page has to win. For ChatGPT you can't see them today; the right-hand column is what you can see.
Why the fan-out matters
A prompt such as "iPhone 17 or Galaxy S26?" is one question to the user and several retrieval problems to the assistant: specs, camera comparisons, battery tests, prices, maybe reviews from a specific year. The pages that win those narrower searches are the ones the answer can cite. Three consequences for anyone working on AI visibility:
- Your target keyword is not the retrieval query. A page optimized for the prompt's wording can miss the searches actually run.
- Fan-out queries can repeat across prompts. When different prompts in one category trigger overlapping searches, one page that serves that search is relevant to many answers.
- The fan-out leaves traces. ChatGPT does not return its searches, but the cited sources are what those searches found. Which domains get cited, and in what order, is the measurable side of the fan-out.
What the API returns
{
"prompt": "iPhone 17 or Galaxy S26?",
"country": "US"
}
Send it to POST /v1/monitor/chatgpt or as the payload of a CHATGPT task. The result carries text, sources[] in the order ChatGPT shows them, and citationPills[]. The contract also has result.searchQueries for the fan-out, requested with include.searchQueries: true at no extra cost, but it comes back empty on the ChatGPT answers served today.
Two request details decide whether you get useful data:
-
Web search is forced by default. Leave
disableWebSearchunset. Withtrue, you get the answer ChatGPT gives on its own, which searches only when it decides to, sosourcesandcitationPillsare often empty. That mode is useful for a different question (what does the model say without retrieval?), not for studying retrieval. -
Market matters.
countryis required. ChatGPT answers from 213 countries; US-state targeting is coming soon. Run each market as its own series and don't pool markets before you have compared them.
curl -X POST https://api.answerline.dev/v1/monitor/chatgpt \
-H "Authorization: Bearer $ANSWERLINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "best CRM for a 10-person B2B sales team",
"country": "US"
}'
Aggregate cited sources across a prompt set
One answer's sources say little; across 50 or 200 prompts they give a distribution. Count each cited domain once per answer that cited it:
from collections import Counter
from urllib.parse import urlparse
def domain(url: str) -> str:
return (urlparse(url).hostname or "").removeprefix("www.")
def source_report(results: list[dict]) -> list[tuple[str, int]]:
"""Cited domains with the number of answers that cited them, most frequent first."""
counts = Counter()
for result in results:
counts.update({domain(s["url"]) for s in result.get("sources", []) if s.get("url")})
return counts.most_common()
results are result objects: from synchronous responses, or response.result of completed tasks.
Counting per answer, not per occurrence, keeps one answer that cites a domain several times from inflating it. Also report coverage: the share of answers that cited any sources at all. If only 40% did, your counts describe that 40%, and you should say so next to the numbers.
Reason about the searches you can't see
Without the searches, work backwards from what was cited and what was said:
-
Read the cited pages. Each source's
labelanddescriptionsay what the page is about, and so what the search that found it must have matched. A page about camera tests for two phones was found by a camera search, not by the prompt as typed. - Read the answer text for subtopics. An answer that covers specs, price, battery and reviews in turn most likely searched for each. Each subtopic is a candidate query for your content plan.
- Group prompts by shared sources. Prompts whose answers cite the same domains probably rely on overlapping searches, and one page can serve all of them.
from collections import defaultdict
def prompts_by_domain(rows: list[tuple[str, dict]]) -> dict[str, set[str]]:
"""rows are (prompt_id, result) pairs. Maps each cited domain to the prompts whose answers cite it."""
m: dict[str, set[str]] = defaultdict(set)
for prompt_id, result in rows:
for s in result.get("sources", []):
if s.get("url"):
m[domain(s["url"])].add(prompt_id)
return m
This is inference, not returned data: a cited page tells you a search found it, not which search. Label inferred searches as such in reports. How AI engines choose citations covers the source side in more depth.
Turn it into work
- Rank topics by how many answers cite pages on them across the prompt set.
- Map each topic to a page. Note the page on your site that should answer it, or that none does. A missing page for a frequently cited topic is the clearest content gap you will find.
- Check the organic side. Inferred searches are still searches, so run the top ones through a Google Search request and see who ranks. A domain that ranks organically and is cited in answers holds both positions; one that ranks but is never cited suggests the page doesn't answer the narrower question well. Structured SERP data shows the request.
-
Split by market. Run the set with each
countryyou sell in; fan-out can differ by market, and so do the cited domains. - Keep the set fixed. Re-run the same prompts on a schedule so changes in the cited sources are visible, and version the prompt set when you change it. Prompt set design covers versioning.
- Feed new prompts back. Subtopics that recur across answers are good candidates for new monitoring prompts, because they reflect how the engine decomposes your category.
A report that people act on
A report content teams can act on has four columns per topic:
| Topic (inferred from cited pages) | Answers citing it | Top cited domains | Our page |
|---|---|---|---|
| best crm small business | 34 of 150 | review site A, vendor B, publisher C | /crm-for-small-teams (not cited) |
| crm pricing comparison | 21 of 150 | vendor B, publisher D | none |
| crm with email automation | 12 of 150 | vendor E, our domain | /features/email (cited 5 times) |
How to read it:
- High frequency, no page: a content gap. Write the page that answers the narrower question directly.
- High frequency, page exists, not cited: the page is not what the engine retrieves or uses for that topic. Compare it with the cited pages: scope, freshness, structure, and whether it answers the exact question the topic implies.
- Page cited: protect it. Keep it current and watch the topic in the next runs.
Add a fifth column with a change indicator against the previous run of the same prompt set version, and the report doubles as a trend view. Keep the denominator ("of 150") visible, and state that topics are inferred from cited pages, not from returned searches.
Fan-out on other engines
Some engines return their searches, with different field names:
| Engine | Field | How to get it | Base credits |
|---|---|---|---|
| ChatGPT | result.searchQueries |
include.searchQueries: true (free); empty on the answers served today |
5 |
| Copilot | result.searchQueries |
Coming; not returned yet | 5 |
| Grok | result.searchQueries |
Returned in the response | 4 |
| Perplexity | result.search_model_queries |
Returned in the response | 4 |
Grok and Perplexity are paused for now and coming back soon. Perplexity also returns related_queries, its suggested follow-up searches. Those are suggestions for the user, not searches it ran, so keep them in a separate table. The contract also defines product.generatedProductQuery on ChatGPT inline products, a product-level cousin of the fan-out, but inlineProducts comes back empty on the answers served today; see ChatGPT shopping cards for the product data you get.
Where an engine returns searches, count each normalized search once per answer that ran it, and group near-duplicates ("best crm small business 2026", "best CRM for small businesses") before counting. Normalize every engine's queries the same way so you can see which query groups every engine runs and which are engine-specific.
Run it at scale
For a prompt set, send async tasks in batches (up to 500 per request) with a webhook:
[
{ "taskType": "CHATGPT", "idempotencyKey": "fanout-v3-2026-w38-crm-012-US",
"payload": { "prompt": "best CRM for a 10-person B2B sales team", "country": "US" },
"webhook": { "url": "https://your-app.example/hooks/answerline" } }
]
Put the prompt set version in the idempotency key, as above, so a re-run under a new version never collides with an old one. Sync, async and webhooks explains the flow.
Cost, from the current credit table (/pricing has credit prices): 150 prompts × 1 market × weekly (about 4.3 runs a month) × 5 credits for a ChatGPT task ≠3,200 credits a month. include.searchQueries is free, so turning it on does not change that.
Pitfalls
-
Reading an empty
searchQueriesas "no search". On ChatGPT today it means the searches were not exposed, not that none ran; checksources. - Counting occurrences instead of answers. One answer citing a domain several times should count once.
- Treating inferred searches as data. A search worked out from cited pages is a hypothesis; label it that way.
-
Disabling web search by accident.
disableWebSearch: truechanges the question you are measuring. - Pooling markets early. Compare markets before merging them.
- Judging one run. Answers and their sources vary between runs; trend over repeated runs. AI answer volatility covers sampling.
- Comparing across prompt set versions. A reworded prompt produces a different fan-out. Compare runs only within one version, and when you promote a new version, run old and new side by side once so the change in counts can be attributed to the wording rather than to the engine.
See the full ChatGPT field list on the ChatGPT engine page, or start with the quickstart.
If you want ChatGPT's answer and cited sources as JSON instead of reading browser traffic: POST /v1/monitor/chatgpt returns them in one call. Free tier, no card: https://answerline.dev
Top comments (0)