ChatGPT serves ads. Here's the full attribution loop

Published: (April 28, 2026 at 07:54 PM EDT)
4 min read

Source: Hacker News

Ad delivery mechanism

OpenAI’s ad platform has two halves. On the ChatGPT side, the backend injects structured single_advertiser_ad_unit objects into the conversation SSE stream while the model is responding. On the merchant side, a tracking SDK called OAIQ runs in the visitor’s browser and reports product views back to OpenAI. The two are tied together by Fernet‑encrypted click tokens, four of them per ad.

I captured both halves on a consented mobile‑traffic research fleet. Everything below comes from observed traffic.

When you send a message to ChatGPT, the backend opens an SSE response at https://chatgpt.com/backend-api/f/conversation. Most events in that stream are model output; some are ad units. They look like this:

event: delta
data: {
  "type": "single_advertiser_ad_unit",
  "ads_request_id": "069e89b3-c038-7764-8000-6e5a193e5f69",
  "ads_spam_integrity_payload": "gAAAAABp6Js_",
  "preamble": "",
  "advertiser_brand": {
    "name": "Grubhub",
    "url": "www.grubhub.com",
    "favicon_url": "https://bzrcdn.openai.com/cabfae7ead26b03d.png",
    "id": "adacct_6984ed0ba55481a29894bb192f7773b4"
  },
  "carousel_cards": [{
    "title": "Get Chinese Food Delivered",
    "body": "Satisfy Your Cravings with Grubhub Delivery.",
    "image_url": "https://bzrcdn.openai.com/cabfae7ead26b03d.png",
    "target": {
      "type": "url",
      "value": "https://www.grubhub.com/?utm_source=chatgptpilot&utm_medium=paid&utm_campaign=diner_gh_search_chatgpt_kw_traffic_nb_x_nat_x&utm_content=nbchinese&oppref=gAAAA&olref=gAAAA",
      "open_externally": false
    },
    "ad_data_token": "eyJwYXlsb2"
  }]
}

Notes

  • single_advertiser_ad_unit is a typed schema; the naming implies sibling types (e.g., multi‑advertiser).
  • advertiser_brand.id follows the pattern adacct_ – a stable per‑merchant account identifier.
  • Brand favicon and ad image both load from bzrcdn.openai.com. OpenAI hosts the advertiser’s creative, not the merchant.
  • target.open_externally: false opens the link in ChatGPT’s in‑app webview, so OpenAI observes the post‑click navigation on top of any pixel signal.
  • Four Fernet tokens per ad: ads_spam_integrity_payload, oppref, olref, and a base64‑wrapped ad_data_token. Each is AES‑128‑CBC under a server‑only key with HMAC‑SHA256 integrity.

How ads get selected

A single account in the panel received six different ads across six conversations on six different topics. The targeting is contextual to the chat:

Conversation topicAdvertiser delivered
Beijing trip planning (Great Wall, Forbidden City)Grubhub – “Get Chinese Food Delivered”
Beijing tour bookingsGetYourGuide – Great Wall tour (ad_id=beijing003)
Beijing flightsAxel – utm_term=vflight_beijing_03
NBA playoffsGametime – utm_campaign=nba&utm_content=playoffs
Spring fashion/trendsAritzia – utm_campaign=chatgptpilot_trav3
Productivity / slidesCanva – utm_campaign=…link-clicks_products

The same account, different topic, different brand. I didn’t find evidence one way or the other on whether targeting also incorporates prior conversation history.

The four‑token attribution chain

Every ad ships with four distinct Fernet‑encrypted blobs. Their roles, based on where they appear:

  • ads_spam_integrity_payload – sent inside the SSE data, never on the click URL. Used for server‑side integrity checks against forged ad clicks.
  • oppref – present on the click URL and copied verbatim by the OAIQ pixel into the cookie __oppref (TTL 720 hours ≈ 30 days). The forward attribution token; travels with every subsequent merchant pixel event.
  • olref – paired with oppref on the click URL but not stored by the SDK we observed. Likely impression‑side / outbound‑link‑reference logging on OpenAI’s servers.
  • ad_data_token – base64‑wrapped JSON containing yet another Fernet token. Carried in the SSE payload, presumably reconciled server‑side at click time.

Fernet’s first nine bytes are public: version byte 0x80 plus an 8‑byte big‑endian Unix timestamp. So the mint time of any of these tokens is recoverable without OpenAI’s key:

import base64, struct, datetime

b = base64.urlsafe_b64decode("gAAAAABp7fdA" + "==")
print(datetime.datetime.utcfromtimestamp(struct.unpack(">Q", b[1:9])[0]))
# → 2026-04-26 11:30:08 UTC

The Home Depot click URL I captured was minted at 11:30:08; the browser fetched the merchant page at 11:31:43. Click latency: 95 seconds.

How the loop closes on the merchant side

User taps the card. Browser opens a URL such as:

https://www.grubhub.com/?utm_source=chatgptpilot&...&oppref=gAAAA&olref=gAAAA

The merchant page loads the OAIQ SDK:

<script src="https://bzrcdn.openai.com/oaiq.min.js"></script>
<script>
  oaiq('init',    { pid: '' });
  oaiq('measure', 'contents_viewed', { /* … */ });
</script>

oaiq.min.js is at version 0.1.3. On init it reads ?oppref= from window.location, writes it into the first‑party cookie __oppref with a 720‑hour TTL, and sets a probe cookie __oaiq_domain_probe. Every subsequent measure call POSTs JSON to:

POST https://bzr.openai.com/v1/sdk/events?pid=&st=oaiq-web&sv=0.1.3

Two domains to add to your filter list if you want to block ChatGPT ad events: bzrcdn.openai.com, bzr.openai.com.
Two cookie names to inspect after any ChatGPT‑recommended click: __oppref, __oaiq_domain_probe.

0 views
Back to Blog

Related posts

Read more »