What We Did
OpenAI began rolling out ads in ChatGPT on February 9, 2026. Ads appear on the Free and Go subscription tiers for US-based users. The pilot has been a massive success so far, with OpenAI recently announcing that ChatGPT ads hit $100M in revenue in just six weeks. But what exactly happens under the hood when an ad gets served?
We tested this using our own LLM scraper, intercepting every XHR request and SSE stream while sending shopping-intent queries to trigger the ad system. Across roughly 20 queries, one returned a live ad unit from WSJ Buy Side (The Wall Street Journal's research and commerce team). That single response gave us everything we needed to map the full ad pipeline.
The Query and the Response
We sent: "What are the best deals at Target right now?"
Here is the step-by-step flow of what happened, with screenshots at each stage.
Step 1: ChatGPT Home (Free Tier, Not Logged In)

The free tier interface, no login required. Ads are served to Free and Go tier users.
Step 2: Query Typed

The query is typed into the prompt box. At this point, the browser has already completed the sentinel handshake (prepare, finalize) and is ready to fire the conversation request.
Step 3: Response with Organic Product Cards

ChatGPT returns the answer with inline product cards (Beats Solo Buds at $79.99, JBL Clip 5 at $59.95, Keurig K-Mini Go at $69.99). These are organic shopping results served via the /backend-api/search/product_update endpoint. They are not ads.
Step 4: Full Response with Ad at Bottom

The full-page view. At the very bottom, below the "Sources" button, an ad card appears from WSJ Buy Side. The ad is clearly labeled "WSJ Buy Side · Sponsored", with a card titled "The Best Deals Now", a description ("See which sales are actually worth your time."), and product images. This is the single_advertiser_ad_unit format with a carousel_cards array.
Now let's look at what happened in the network tab.
The Conversation Stream XHR
When you send a message to ChatGPT, the browser makes a POST request to /backend-anon/f/conversation. The response is a Server-Sent Events (SSE) stream with text/event-stream content type. This single stream carries everything: the system prompt, the streamed text tokens, content references, and the ad payload.
The stream totaled ~200 KB for this query and contained 48 SSE events. Each event is a data: line containing JSON. Here are the key headers the browser sends on this request:
| Header | Purpose |
|---|---|
| oai-device-id | Unique device fingerprint (UUID format) |
| oai-session-id | Session identifier for the current browser session |
| openai-sentinel-chat-requirements-token | Auth token from the sentinel finalize handshake |
| openai-sentinel-proof-token | Proof-of-work token (anti-automation) |
| openai-sentinel-turnstile-token | Cloudflare Turnstile challenge response |
| openai-sentinel-so-token | Additional anti-automation token |
For anonymous users, the persona field in the sentinel response is set to chatgpt-noauth. The token expires after 540 seconds.
The model handling this query was gpt-5-3, identified via the resolved_model_slug field in the message metadata.
The Product Update Endpoint (Shopping Cards)
Separately from the main conversation stream, ChatGPT fires POST /backend-api/search/product_update requests. These are not ads. They are the organic shopping product cards that appear inline in the response (Beats earbuds at $79.99, JBL speaker at $59.95, and so on).
Each product_update is also an SSE stream returning product_entity events. For this query there were three of them, one per product card. Here is the structure of a single product entity:
{
"type": "product_entity",
"product": {
"id": "16406220612788342393",
"title": "Beats Solo Buds True Wireless Earbuds",
"url": "https://www.beatsbydre.com/...?utm_source=chatgpt.com",
"price": "$79.99",
"rating": 3.9,
"num_reviews": 4440,
"merchants": "Beats by Dre + others",
"offers": [
{
"merchant_name": "Target",
"price": "$69.99",
"provider": "p2",
"tag": { "text": "Best price" },
"debug_info": { "source": "p2", "p": "101" }
},
{
"merchant_name": "Best Buy",
"price": "$69.99",
"provider": "p3",
"debug_info": {
"source": "p3",
"feed_id": "openai_best_buy"
}
}
],
"show_price_disclosure": false,
"checkoutable": false,
"checkout_payload": null
}
}Two things stand out here. First, the provider field differentiates organic product data (p2) from direct feed partner data (p3). The debug_info.feed_id reveals the partner: openai_best_buy and openai_walmart were the two feed IDs we captured.
Second, the checkoutable and checkout_payload fields are present but set to false / null. OpenAI has announced native checkout for Q3 2026, and the plumbing is already in the schema waiting to be turned on.
These product cards are important context, but they are not ads. The actual ad is a completely separate SSE event inside the main conversation stream.
The Ad Snippet
At the end of the conversation SSE stream, after all the text tokens and product entities have been sent, ChatGPT sends one final event with "type": "ads". This is the actual ad. Here is the complete payload:
{
"type": "ads",
"content": {
"ads_request_id": "069cc2f2-43ca-71ec-8000-d1563c0625ad",
"ads_spam_integrity_payload": "gAAAAABpzC8n...",
"type": "single_advertiser_ad_unit",
"preamble": "",
"advertiser_brand": {
"name": "WSJ Buy Side",
"url": "www.wsj.com/buyside",
"favicon_url": "https://bzrcdn.openai.com/307ebaa6f26198d5.ico",
"id": "adacct_69a60e8a254c819880317b0f30f9e9da"
},
"carousel_cards": [
{
"title": "The Best Deals Now",
"body": "See which sales are actually worth your time.",
"target": {
"type": "url",
"value": "https://www.wsj.com/buyside/...?utm_source=chatgpt&utm_medium=cpc&oppref=...&olref=...",
"open_externally": false
},
"image_url": "https://bzrcdn.openai.com/8b041d28a5266e0c.jpg",
"ad_data_token": "eyJwYXlsb2FkIjoi..."
}
]
},
"visibility": {
"status": "allowed",
"reason": null
},
"debug_info": null
}Let's walk through every field.
| Field | What It Does |
|---|---|
| type: "ads" | Identifies this SSE event as an ad unit, separate from text tokens or product entities |
| ads_request_id | Unique UUID for this specific ad impression. Used for deduplication and billing |
| ads_spam_integrity_payload | A Fernet-encrypted token for anti-fraud verification. Prevents spoofed impression reporting |
| content.type | The ad format. We saw single_advertiser_ad_unit. The JS bundle also references multi_advertiser_ad_unit |
| advertiser_brand.id | Advertiser account ID with the adacct_ prefix |
| advertiser_brand.favicon_url | Brand icon served from bzrcdn.openai.com, OpenAI's dedicated ad asset CDN |
| carousel_cards[] | Array of ad cards. Supports multiple cards in a scrollable carousel |
| target.value | Click URL with utm_source=chatgpt and utm_medium=cpc, confirming CPC billing |
| target.open_externally | When false, the link opens inside the ChatGPT webview instead of the system browser |
| oppref / olref | Encrypted click-tracking tokens appended to the target URL |
| ad_data_token | Base64-encoded JSON containing a Fernet-encrypted payload and version: 2. Sent back on click/impression events |
| visibility.status | allowed or hidden. Controls whether the ad renders. The JS code checks this and suppresses the ad if set to hidden |
The preamble field is an empty string in our capture, but it is clearly designed to hold an optional intro line above the ad card (something like "You might also like").
Ad Tracking: The Bazaar Endpoint
Immediately after the ad renders, the browser fires two POST requests to /backend-anon/bazaar/event. Both return an empty {} response. The tracking data lives in the request body (POST payload), not the response.
The two calls correspond to two separate events: one for the ad impression (the ad was rendered in the viewport) and one for ad viewability (the user scrolled far enough to actually see it). These carry the ads_request_id, the ad_data_token, and the ads_spam_integrity_payload back to OpenAI for billing and fraud detection.
The ad asset CDN domain is bzrcdn.openai.com (short for "bazaar CDN"), which serves both the brand favicon and the ad card images. This is separate from OpenAI's main CDN and is purpose-built for the advertising system.
We also found a rejection mechanism in the frontend JavaScript bundle. The code checks whether the ad's messageId matches the latest user message. If the user sends a new message before the ad loads, the ad is suppressed with a rejection reason of CHATGPT_ADS_PLACEMENT_REJECTION_REASON_NOT_LATEST_PROMPT. This prevents stale ads from appearing on old responses.
Full Ad Schema Reference
Here is a consolidated view of every ad-related field, endpoint, and infrastructure component we found:
Endpoints
| Endpoint | Method | Purpose |
|---|---|---|
| /backend-anon/f/conversation | POST | Main conversation SSE stream. Contains the ad payload as a type: "ads" event |
| /backend-api/search/product_update | POST | Organic shopping product cards (not ads). SSE stream with product_entity events |
| /backend-anon/bazaar/event | POST | Ad impression and viewability tracking. Fires twice per ad render |
| /backend-anon/sentinel/* | POST | Anti-bot handshake (prepare, finalize, ping). Required before any conversation request |
Ad Unit Types
| Type | Description |
|---|---|
| single_advertiser_ad_unit | One brand, one or more carousel cards. This is the format we captured |
| multi_advertiser_ad_unit | Multiple brands in a single ad block. Referenced in the JS bundle but not observed in our testing |
Infrastructure
| Component | Details |
|---|---|
| bzrcdn.openai.com | Dedicated ad CDN serving favicons and card images |
| Billing model | CPC (confirmed by utm_medium=cpc in click URLs) |
| Encryption | Fernet tokens for integrity payloads and ad data tokens |
| Advertiser ID format | adacct_ prefix followed by a 32-character hex string |
What This Means for Brands
ChatGPT's advertising system is cleanly separated from its answer generation. The conversation model generates the response first. The ad is appended as a distinct SSE event at the end of the stream. There is no evidence that the ad influences the content of the response itself.
The fill rate is still very low. Across roughly 20 shopping-intent queries, only one returned a ad. OpenAI is being conservative during the pilot phase, currently limited to Free and Go tier users in the US with a $200,000 minimum commitment for advertisers.
But the infrastructure is clearly built for scale. The multi_advertiser_ad_unit type exists in the code. The carousel_cards array supports multiple cards per brand. The checkoutable field on product entities is waiting for native checkout to go live. And OpenAI plans to launch self-serve ad tools in April 2026, which will remove the minimum spend requirement.
For brands tracking their AI visibility, the takeaway is clear: the organic product cards and the ads are two completely separate systems. Getting your products into the product_entity pipeline (via data feeds like openai_best_buy or openai_walmart) is a feed optimization problem. Getting into the ads event is a media buying problem. Both matter, and both are now measurable.
OpenAI is also expanding its ad pilot beyond the US, with launches planned in Canada, Australia, and New Zealand in the coming weeks, and more markets throughout 2026. Self-serve advertising tools are expected to go live in April 2026, removing the current $200,000 minimum commitment and opening the door for a much wider range of advertisers.
At Rankly, we are building an Ad Intelligence module that will track and analyze these ad placements across AI answer engines. As ChatGPT ads scale globally, brands will need visibility into which competitors are buying placements, what queries trigger ads, and how the ad landscape evolves over time. Stay tuned.
