cloro
Research

Ads in ChatGPT: reverse-engineering which ones actually get shown

Ricardo Batista
Founder, cloro
8 min read
ChatGPT AdsResearchOpenAI
On this page

OpenAI’s ad payload answers one question — what did ChatGPT serve? — and quietly hides another: what did the user actually see? Those are not the same number. Ads in ChatGPT are served as candidates in a result.ads[] array, but the chat interface renders at most one visible card per answer. Everything else is served-but-not-shown: paid inventory the model considered and dropped before it reached the screen.

This post reverse-engineers the gap from our analysis of cloro’s own data on ads in ChatGPT. We measured how often a served ad is actually rendered, how often “multiple ads” is really one ad re-emitted, how often OpenAI hides an ad, and — when two advertisers genuinely compete — which one the interface picks. The last question has a clean, content-based answer.

The figures come from cloro’s own measurements across millions of ChatGPT responses, not a census of all of ChatGPT: ads concentrate in the US, Canada, Australia, and Japan, and the prompt mix skews commercial.

Served is not shown

A ChatGPT response can carry more paid inventory than the user ever sees. The result.ads[] array is OpenAI’s serve decision — the ad candidates the auction returned for that prompt. The visible card under the answer is the show decision — a client-side render that keeps at most one ad and drops the rest.

For anyone measuring ads in ChatGPT, this matters. If you count the length of result.ads[], you are counting what was served, not what was seen. Impression-weighted exposure — the number an advertiser actually cares about — needs the show decision, and the show decision is not a field in the served payload. You have to recover it from the rendered DOM.

What “served but not shown” looks like

Say you ask ChatGPT, “How do I clean Nike Air Max sneakers without damaging them?” The API response comes back with a result.ads[] array that is not empty — OpenAI served an ad for a shoe-care brand:

"ads": [
  {
    "brand": { "name": "Wolf & Shepherd", "url": "https://wolfandshepherd.com/?utm_source=chatgpt.com" },
    "cards": [{ "title": "Sneaker Shoe Care Kit", "image": "https://bzrcdn.openai.com/…webp" }],
    "visibility": "allowed",
    "adsResponseIndex": 0,
    "rendered": false
  }
]

The ad sits right there in the payload. But read the answer the user actually saw, and there is no Sponsored card in it — not for Wolf & Shepherd, not for anyone. The rendered: false flag says it plainly: OpenAI served the ad, the interface never drew it. Count the response as an ad served, not an ad seen.

This happens even when the ad fits the question. A shoe-care ad against a shoe-cleaning prompt is a close match, and it still didn’t show. Serving is the auction’s call; showing is a separate decision the interface makes as it builds the answer.

Now the opposite. Ask “what’s the best outbound sales platform right now?” and the response serves JustCall — and this time the answer displays its Sponsored card, so the ad carries rendered: true. Same result.ads[] shape, opposite outcome for the reader. The one field that tells the two apart is rendered.

How we read visibility and rendered

Two fields on each ad carry the story, and they come from different places.

visibility is OpenAI’s own call, passed straight through. Every served ad in the SSE stream carries a visibility.status of allowed or hiddenhidden meaning OpenAI’s integrity checks filtered the ad. cloro copies that value onto the ad; it does not decide it. In practice the value is almost always allowed.

rendered is cloro’s own measurement, because the payload never says which ad the page drew. cloro reads the rendered HTML for a data-testid="image-card-v2-elevated-brand" element — the tag that sits only on a real Sponsored card, not on the served-ad JSON embedded elsewhere in the page, and that holds whether the visible label reads “Sponsored” or “광고”. Its text is the advertiser name. An ad is rendered: true when its advertiser matches one of those cards, and false when it does not. The read is time-bounded, so a stalled page marks nothing rendered rather than hanging the scrape.

So visibility answers “did OpenAI allow the ad?” and rendered answers “did the user see it?” — separate questions, which is why an ad can be allowed and still rendered: false.

Finding 1 — served, but not always shown

87% of ad-bearing ChatGPT responses render a visible ad card; the other 13% are served but never shown

Across 3.25M ad-bearing responses, about 87% rendered a visible ad card and roughly 13% were served but never shown. When a single ad is served — most cases — it renders ~87% of the time; the rest are counts no user ever saw. That ~13% gap is the single most important correction to make when you measure ads in ChatGPT: roughly one in eight served ads is never seen.

A Sponsored ad card rendered inside a ChatGPT response, with a brand logo, headline, and one-line description

Finding 2 — the surface is single-ad, but not strictly

ChatGPT’s ad surface is single-slot by design, and the data agrees: only ~4.2% of ad-bearing responses serve more than one distinct advertiser. Three advertisers show up in about 1 in 4,000 responses, and four is a genuine rarity — we found four in a week of traffic.

“At most one shown” is the right intuition but slightly too strong. Among two-advertiser responses:

Cards renderedShare of two-advertiser responses
One card82.9%
Zero cards12.7%
Both cards4.4%

Two-advertiser ChatGPT responses: 83% render one card, 13% render none, 4% render both

So the interface usually shows one, sometimes shows none, and occasionally shows two. Treating result.ads[] as a one-per-response impression log is wrong in both directions — it can overstate (served, not shown) and, rarely, understate (two rendered).

Finding 3 — one ad, counted many times

Here is what makes naive ad counting wrong. ChatGPT re-emits the same ad several times as the answer streams, each copy carrying a fresh ads_response_index. A response that shows one ad can contain three identical type:"ads" events with indices 0, 1, and 2.

In cloro’s data, 5.1% of ad-bearing responses re-emit a single advertiser this way. If you de-duplicate on ads_response_index, you keep all three copies and report three ads where the user saw one.

This is a real trap. A parser that keys on ads_response_index — which differs across the re-emissions — reports three identical entries in result.ads[] for one placement. De-duplicate on the advertiser and creative content instead — the brand and its cards — and treat ads_response_index as a re-emission counter, not an ad identifier. A single ad, streamed a few times, is still a single ad.

Finding 4 — hidden ads barely exist in the stream

Each served ad carries a visibility.status. The intuition is that hidden — an ad OpenAI’s integrity checks filtered — might explain the served-but-not-shown gap. It doesn’t. Over 60 days, not one of 8,590 ad-bearing responses in cloro’s data carried a hidden ad event. Every served ad we saw was marked allowed.

That is an important negative result. The gap between served and shown is not OpenAI flagging ads as ineligible in the payload — those almost never reach the response. It is a rendering decision made after the ad is served and marked allowed. If you were planning to infer exposure from visibility.status, don’t — it is nearly always the same value.

Finding 5 — the shown ad is the one ChatGPT names

Now the interesting case. When ChatGPT serves two distinct advertisers and shows one, which does it pick? We tested every field in the payload — ad format (image_card_v2 vs product_card_v2), card count, image presence, visibility, even adsResponseIndex — and the strongest signal isn’t in the ad payload at all. It’s in the answer.

The rendered advertiser’s brand appears in ChatGPT’s generated answer 61% of the time; the dropped advertiser’s brand appears just 4% of the time. The interface shows the ad for the brand the model actually talked about.

Served adBrand named in the answer
The one that rendered61%
The one that was dropped4%

61% of shown ChatGPT ads are named in the answer text, versus 4% for the ads it drops

That resolves into a clean two-part rule:

  • When the answer names exactly one of the two advertisers — 60% of two-ad responses — that ad renders 96% of the time (43 of 45).
  • When it names neither (39%), the interface falls back to serving order: the later-served ad (higher adsResponseIndex) renders 86% of the time.

The exceptions to the naive order rule give it away. In the ~10% of responses where the earlier-served ad wins, it is consistently the one the answer is about: Expedia rendered over a running-shoe brand for a “Starbucks in Seattle” location query; Gong.io rendered over a lead-gen tool for “best sales outreach tool”; a migration tool rendered for “how to move your Netflix profile.” Order didn’t pick those — relevance did.

So the selection process is contextual match between the ad and the generated answer, not a fixed slot. adsResponseIndex predicts the winner ~90% only because OpenAI tends to serve the answer-relevant ad later; it is a proxy, not the mechanism. When you need the true answer, the shown ad is the one whose advertiser the model surfaced in its own text — and the rendered DOM is the ground truth for the rest.

What it means for measuring ads in ChatGPT

Three rules for measurement

Three practical takeaways for anyone tracking ads in ChatGPT:

  1. Exposure is not ads[].length. About 87% of ad-bearing responses render a card; the served array overstates what users saw by roughly 13%. Measure the rendered ad, not the served list.
  2. De-duplicate on the advertiser, not the index. Re-emission — 5.1% of responses — inflates counts if you trust ads_response_index as an identifier. One placement is one advertiser plus one creative, however many times it streams.
  3. The shown ad is the one the answer names. For the ~4% of responses with two advertisers, the ad whose brand appears in the generated answer renders 96% of the time when the answer names exactly one of them. visibility, format, and card count show no effect; serving order (adsResponseIndex) is only a ~90% proxy for the underlying content match.

Why it matters to advertisers

For an advertiser, the distinction is money. A brand in result.ads[] won the ad auction; a brand in the rendered card was actually shown. When two advertisers share a response, the one served first is the one dropped about 90% of the time. Exposure counts that stop at the served array over-credit the ads that never rendered.

cloro bakes all three corrections into its ChatGPT results: result.ads[] is de-duplicated on the advertiser and creative, each ad carries its visibility and adsResponseIndex, and a rendered flag marks the ad the interface actually drew. That turns “what was served” into “what was seen” — the number that matters for exposure.

Winning the ad slot in ChatGPT

If the shown ad is the one ChatGPT names in its answer, the task for advertisers is clear: become the brand the model names for your target prompts. Two ads can win the auction for the same query, but only the one named in the answer is shown. So performance on ads in ChatGPT is a visibility problem, not just a bidding one.

Three things matter:

  • Be worth citing for the query. The advertisers that render are the ones the model already treats as a good answer — brands with real authority on the topic and fresh, structured content that AI engines cite. If ChatGPT never names you on its own, buying the slot leaves the choice to serving order.
  • Match the product to the intent, not the keyword. A charger brand served against a “best rural carrier” query loses to any advertiser the answer actually discusses. Relevance is judged against the generated answer, which is narrower than the raw prompt.
  • Measure the impression, not the serve. Track the rendered flag, not result.ads[] length, and watch whether your brand appears in the answer text alongside the card. That overlap is the early sign that your ads in ChatGPT are being seen.

None of this shows up in a served-count report. It surfaces only when you join the ad payload to the answer content — which is the whole reason to measure ads in ChatGPT from the rendered response rather than the served list.

Reproduce it

Everything above about ads in ChatGPT is reproducible. The detection logic is public: check the result.ads[] array for served ads and the image-card-v2-elevated-brand card test id for the rendered one. The ChatGPT ads monitoring guide walks through the detection signals end to end. The ChatGPT ads penetration study tracks how often the surface fires by country. And if you want to place ads rather than measure them, see how to advertise on ChatGPT.

Ricardo Batista

About the author

Founder, cloro

Ricardo is one of the founders and engineers behind its SERP and AI-search scraping infrastructure. Before cloro he scaled a financial comparison site to $7M ARR and ran the full-country operations of a unicorn to $65M ARR, then went back to building. He writes about search engine scraping, generative-engine optimization, and turning live search and AI-answer data into something teams can act on.

Frequently asked questions

Does ChatGPT show every ad it serves?+

No. Ads in ChatGPT are served in the `result.ads[]` array, but the interface renders at most one visible card most of the time. Across 3M+ responses, about 87% of ad-bearing responses actually rendered a card; the other ~13% were served but never shown. Counting the length of `ads[]` overstates real user exposure.

How many ads does ChatGPT show at once?+

Usually one. Among two-advertiser responses, 83% show exactly one card, 13% show none, and about 4% show both. ChatGPT serves more than one advertiser in only ~4% of ad-bearing responses (rarely three, almost never four), so the practical answer is one ad per response.

When ChatGPT serves two ads, which one does it show?+

The one it talks about. The rendered advertiser's brand appears in ChatGPT's generated answer 61% of the time versus 4% for the dropped ad. When the answer names exactly one of the two advertisers, that ad renders 96% of the time; when it names neither, the later-served ad (higher `adsResponseIndex`) renders about 86%. Serving order predicts the winner ~90% as a proxy, but the real driver is contextual match to the answer.

Why does a ChatGPT response list the same ad multiple times?+

It re-emits one ad several times as the answer streams, each copy carrying a higher `ads_response_index`. In cloro's data, 5.1% of ad-bearing responses re-emit a single advertiser this way. De-duplicate on the advertiser and creative, not on `ads_response_index`, or you will double-count one placement.

How can I measure real ChatGPT ad exposure instead of served counts?+

Count the ad the interface actually rendered, not the length of `result.ads[]`. cloro detects the rendered card from the response DOM (the `image-card-v2-elevated-brand` card test id, which is locale-independent) and exposes a `rendered` flag per ad. See the technical monitoring guide for the detection logic.