Skip to main content
Your job is small: for AI-agent requests only, call the doubleshift fragment endpoint and place the card right before the article content — never at the end of <body>. Humans get the page unchanged and never trigger a card request. Read Where the card must land first — placement decides whether the model actually reads the card — then pick the path for your CDN.
Edge integration: the ESI tag or Worker sits between visitor and origin; an agent User-Agent gate decides whether to call doubleshift

The ESI tag / Worker sits between visitor and origin. Only an AI-agent User-Agent opens the gate to GET /fragment; humans pass straight through.

Where the card must land

AI agents do not read your page — they read what a content extractor (think Readability) keeps of it: the single densest text node, everything else discarded as boilerplate. Placement therefore decides everything. Measured on two independent stacks: Two rules follow:
  1. The card, intact, goes right before the article element — the first autonomous block of <main>, never after the footer. Do not nest it inside the editorial node either: the card carries role="complementary", and extractors drop such sub-regions.
  2. Put a short plain-text brand line inside the article content itself — e.g. <p><strong>Featured advertiser on this page:</strong> <brand>.</p> as the article’s first element, derived from the card you just fetched and served only when a card is served. Measured on both stacks: this line is what models actually cite, and it doubles as the ad disclosure your editorial team wants anyway. (The Worker snippet below derives it.)
If your templates make the article element hard to target, have the template render an empty anchor where the card should go (e.g. <div class="ds-anchor"></div> immediately before the article body) and inject on it — an anchor beats guessing someone else’s DOM. Place it so the brand line lands inside the extracted content, never after it.

The fragment endpoint URL

The base is:
Fragment endpoint
Fill each parameter:
Report the agent via X-Agent-UASend the visiting agent’s User-Agent in the X-Agent-UA header (or the agent_ua query parameter) and keep a benign User-Agent on the request itself, because WAFs commonly block bot User-Agents outright, and the endpoint’s own edge protection is no exception. The value only feeds per-agent reporting; the card never depends on it. Prefer the header: it stays out of the cache key, while agent_ua is part of the URL and gives every distinct UA string its own CDN cache entry. Requests that send neither fall back to the plain User-Agent header.
Normalize the path before any pathname-keyed lookupIf your edge derives ctx.* values (or lang) from the URL path via a lookup table — a catalogue, a manifest, a switch — normalize the pathname the way your origin does before the lookup: decodeURIComponent under try/catch, collapse duplicate slashes. An encoded variant of a targeted path (/societe/e%2Dbike-…) otherwise misses the table while the origin happily serves the page — which then goes out without its targeting, silently, and anyone posting the encoded URL bypasses it. Found by an adversarial verification pass on a production integration (2026-08-26).
Keep your API key server-sideThe api_key authenticates your account. Put it in an ESI tag rendered at the edge or in an edge worker and never in client-side JavaScript. It must match the partner_id account, or the endpoint returns .

ESI: Akamai · Fastly · Varnish

The simplest path: one tag in your page template, resolved by your CDN’s native Edge Side Includes. No code to deploy. $(HTTP_HOST)$(REQUEST_PATH) are standard ESI variables that Akamai and Fastly fill in to build page_url. Open-source Varnish expands no ESI variables, so on Varnish page_url must be rebuilt in VCL (snippet below).
ESI tag
ESI splices the card exactly where the tag sits — the placement rules above are entirely in your template. The in-content brand line needs code to derive the brand; on a pure-ESI stack, either have the template render it from your own ad-ops data, or use the Worker path. Then enable ESI on your text/html responses, gated on the AI-agent User-Agent so the include resolves only for agents:
  • Akamai: Property Manager → add a match on User-Agent (the agent list) → behavior Edge Side Includes → Enable.
  • Fastly: set beresp.do_esi = true for agent requests:
Fastly VCL
  • Varnish (self-hosted): vcl_backend_response enables ESI, and vcl_recv rebuilds page_url + reports the agent (no ESI variables on Varnish). Validated end-to-end on Varnish 7.6:
Varnish VCL
ESI includes forward the visitor’s User-AgentAn ESI subrequest carries the original (bot) User-Agent, and the fragment endpoint sits behind edge protection that blocks bot UAs, so a bot-gated include can be dropped before it reaches doubleshift. With onerror="continue" that failure is silent: no card, and no event either. The include subrequest therefore needs both: a benign User-Agent, and the real agent reported out-of-band. On Akamai: Property Manager → Modify Outgoing Request Header (scoped to requests to the fragment endpoint) to replace User-Agent, and append &agent_ua=$url_encode($(HTTP_USER_AGENT)) to the include src (note: agent_ua is part of the URL, so each distinct UA string gets its own CDN cache entry). On Fastly/Varnish: rewrite the include subrequest in VCL: benign User-Agent, real agent in X-Agent-UA (the Varnish snippet above does this). If your CDN cannot rewrite the subrequest headers, use the Worker path below instead.Two Varnish-specific notes (validated on Varnish 7.6): its native ESI is gzip-aware, with no analog of the Worker path’s compression pitfall, but not brotli-aware; and human visitors keep the inert literal <esi:include> element in the HTML (ESI only runs for gated agent requests), so the human page is not byte-identical to the original.
Varnish hidden behind another CDNIf your public CDN is Cloudflare or CloudFront but you run a Varnish underneath, resolving ESI in that Varnish makes the public CDN cache the already-composed HTML and the card stops refreshing. On those stacks, use the Worker / edge-function path instead so each request reaches the endpoint.

Cloudflare Worker

Cloudflare has no native ESI, so a small Worker plays the same role: classify the request by User-Agent and, for AI agents only, fetch the card and inline it with HTMLRewriter. Humans get the page unchanged. Deploy with npx wrangler deploy.
cloudflare-worker.js
The Worker sets page_url from the live request, so you do not bake it into FRAGMENT_URL. Replace <your account id>, <your fragment id>, <lang> and the API key with your real values.
Strict or case-insensitive brand match? The snippet folds case (never diacritics) because a hand-entered ctx.brand legitimately differs in case from the card’s brand_surface. If your ctx.brand values are generated from the same source of truth as the cards, prefer strict equality (after NFKC): any difference then signals drift between your manifest and the account, and you want it to fail loudly.

The page response: cache contract

GET /fragment ships its own cache headers (see Reference) — that covers the card. The response of your page to the agent is yours, and it is where human/agent isolation lives. The contract, established across CDN and serverless stacks:
  • Never let the canonical (human) render vary by visitor. The cached human page must not read the User-Agent nor call /fragment; classification runs in your edge code, on every request, in front of the cache — that execution, not a cache header, is what guarantees isolation.
  • Serve the agent response with Cache-Control: public, max-age=0, must-revalidate and no validators — no ETag, no Last-Modified, so no 304 can ever cross the human/agent line. Build these headers from scratch; never copy the origin’s s-maxage or CDN-specific headers onto the composed response.
  • Compose only 200 text/html. Reproduce every other status — redirects included — untouched, and exclude HEAD (a composed HEAD consumes a fill nothing ever reads) and your framework’s data/prefetch requests.
  • After injecting, drop Content-Length/Content-Encoding if your path buffers the body (HTMLRewriter and native ESI handle this for you).
  • Verify in both orders — human→agent→human and agent→human→agent, cold cache and warm — by counting card markers in the raw HTML.

Timeout budget

The reference snippets budget 800 ms for the fragment call — a same-network figure (CDN edge to Cloudflare). On a serverless platform (Vercel, Netlify functions, …) a cold start alone can blow that budget: use ~2500 ms there. And because the integration fails open (the page always renders), an expired call is invisible by default — log it distinctly, so “no card: not eligible” and “no card: the call timed out” stop looking identical.

Verify

Presence first — then readability, which presence does not prove.
1 — Presence and position
A 0 then a 1 means humans are untouched and the card sits before the footer. That still only proves the card is in the HTML — not that a model reads it. The proof is a probe:
Ask an AI assistant to consult/open https://your-domain.example/some-article?r=57391 and say which brands the page features. (Phrase it as “open the page”; a bare “read the page” can drift into web search instead of fetching your URL.)
Three rules make the probe meaningful:
  • A fresh URL every time (?r=<random>): models cache what they have fetched — a probe on an already-seen URL measures the model’s memory, not your site. The query string is stripped from page_url, so card selection is unchanged.
  • A neutral article: probing a page whose editorial text already names the brand proves nothing.
  • The model citing your in-content brand line is the pass signal — that line, not the card markup, is what models quote back.