> ## Documentation Index
> Fetch the complete documentation index at: https://docs.shftd2.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Publishers integrate one API: GET https://api.shftd2.com/v1/fragment?partner_id=<account id>&page_url=<page URL>, called from their edge for AI-agent requests only, with the API key in the X-API-Key header, never in the query string.
> A 200 carries the card. A 204 means no card: serve the page unchanged.
> The management API (https://api.shftd2.com/v1/me, /v1/sites, /v1/cards, /v1/keys) is optional tooling for scripting setup. It takes its own management token (Authorization: Bearer ds_mgmt_…). Its limits (120 requests and 60 writes per minute per token) do not apply to the delivery API.
> Delivery logs reach doubleshift only through direct delivery logs (POST https://logs.shftd2.com/v1/beacon with a logs token; the edge library with a logsToken is its provided client, and a custom sender may use the same endpoint) or a Cloudflare Logpush job configured with doubleshift. Otherwise doubleshift sees only its own API activity.

# Verify and troubleshoot

> Prove that agents get a card and humans do not, call the fragment endpoint directly, read your Worker's outcome, test your cache in both orders, confirm that an AI assistant read the card, then match a symptom to its fix.

Run these checks on one public article of a registered site, inside your Worker's `scope` and outside
any login or consent wall, once your edge is deployed. Replace `your-domain.example/some-article` with that page.

## Checks

**1. Humans get no card, agents get one**

```bash title="1 — Count the cards" theme={"theme":{"light":"css-variables","dark":"css-variables"}}
# As a human — expect 0: no card.
curl -s https://your-domain.example/some-article | grep -o 'class="abc-card"' | wc -l

# As an AI agent — expect 1: exactly one card. Always GET, never HEAD
# (`curl -I`): HEAD gets the human variant. A page without an <article>
# answers 0 (the Worker logs no_slot).
curl -s -A "Mozilla/5.0 (compatible; GPTBot/1.0)" \
  https://your-domain.example/some-article | grep -o 'class="abc-card"' | wc -l
```

| Human | Agent | What it means |
| - | - | - |
| `0` | `1` | Agents only. Now check position and disclosure. |
| `0` | `0` | Nothing eligible for that page, or your gate never fired: step 2 tells which. |
| `1` | `1` | Your gate is not gating: humans get cards. |
| `0` | `2`+ | Several integrations enrich the page: keep one. |

```bash title="Position and disclosure, as an AI agent" theme={"theme":{"light":"css-variables","dark":"css-variables"}}
# What follows the card: expect your container's opening tag (<article…> by default),
# or, when the card is first in <main>, the content <main> started with.
curl -s -A "Mozilla/5.0 (compatible; GPTBot/1.0)" https://your-domain.example/some-article \
  | tr -d '\n' | grep -o 'class="abc-card".*' | grep -o '</article>.\{0,120\}' | head -1

# The disclosure: expect one line, inside the article, naming the card's brand.
curl -s -A "Mozilla/5.0 (compatible; GPTBot/1.0)" https://your-domain.example/some-article \
  | grep -oE 'page ?:</strong>[^<]*'
```

These greps are a rough diagnostic, not a proof: read the agent HTML around the card once
([Where the card must land](https://docs.shftd2.com/integrate.md#where-the-card-must-land)).

**2. Ask the endpoint directly**

Call `GET /v1/fragment` for the same page, bypassing your edge:

```bash title="Status code" theme={"theme":{"light":"css-variables","dark":"css-variables"}}
curl -s -o /dev/null -w '%{http_code}\n' \
  'https://api.shftd2.com/v1/fragment?partner_id=<your account id>&page_url=https://your-domain.example/some-article' \
  -H 'X-API-Key: <your key>'
```

```bash title="Headers" theme={"theme":{"light":"css-variables","dark":"css-variables"}}
curl -s -o /dev/null -D - \
  'https://api.shftd2.com/v1/fragment?partner_id=<your account id>&page_url=https://your-domain.example/some-article' \
  -H 'X-API-Key: <your key>'
```

`200`: doubleshift has a card, so a missing card is in your edge. `204`: nothing to serve, see
[symptoms](#symptoms). On a `200`, `X-ABC-Resolution` says whether a routing rule (`rule`) or the
site default (`default`) chose the fragment. Every answer carries an `X-Request-Id`: quote it to
support.

**3. Read your Worker's outcome**

On a Cloudflare Worker, open its logs, then request the article as an agent in another terminal
(CloudFront kit: [its logs](https://docs.shftd2.com/integrate/cloudfront.md#verify)):

```bash theme={"theme":{"light":"css-variables","dark":"css-variables"}}
npx wrangler tail
```

```bash theme={"theme":{"light":"css-variables","dark":"css-variables"}}
curl -sS -A "GPTBot" https://your-domain.example/some-article
```

Look for a `doubleshift_edge` record: `outcome: "served"` (card inserted), `no_fill` (the API
answered `204`: see [symptoms](#symptoms)), `no_slot` (no container in the page). Every outcome is in the
[edge library reference](https://docs.shftd2.com/api/edge-library.md).

**4. Test your cache in both orders**

```bash title="2 — Two cold fills, the warm loop, and no 304 for agents" theme={"theme":{"light":"css-variables","dark":"css-variables"}}
# All on the SAME cacheable page, card markers counted in the raw HTML.
h() { curl -s https://your-domain.example/some-article | grep -c 'abc-card'; }
a() { curl -s -A "Mozilla/5.0 (compatible; GPTBot/1.0)" https://your-domain.example/some-article | grep -c 'abc-card'; }

# Cold fill by a human: PURGE the page (below), then human first. Expect 0 then 1.
h; a

# Cold fill by an agent: PURGE the page again, then agent first. Expect 1 then 0.
a; h

# Warm cache, alternating. Expect 0 1 0 1 0 1.
h; a; h; a; h; a

# An agent sending a validator still gets the full page with the card (200, never 304):
curl -s -o /dev/null -w '%{http_code}\n' -A "Mozilla/5.0 (compatible; GPTBot/1.0)" \
  -H 'If-None-Match: "any"' -H 'If-Modified-Since: Thu, 01 Jan 2099 00:00:00 GMT' \
  https://your-domain.example/some-article
```

**Purge** means dropping that one URL from your CDN cache, so the next request is a cold fill:

* **Cloudflare**: dashboard → *Caching* → *Configuration* → *Purge Cache* → *Custom Purge*, with the page
  URL; or the API:
  `curl -X POST "https://api.cloudflare.com/client/v4/zones/<zone id>/purge_cache" -H "Authorization: Bearer <API token>" -H "Content-Type: application/json" --data '{"files":["https://your-domain.example/some-article"]}'`.
* **Any other CDN**: its purge by URL (Akamai Fast Purge, Fastly single-URL purge, the `PURGE` or ban your
  Varnish VCL accepts), or wait for the page's TTL to expire.

Expect `0` for humans and `1` for agents, whoever filled the cache. No validators on the agent
response:
`curl -s -D - -o /dev/null -A "Mozilla/5.0 (compatible; GPTBot/1.0)" https://your-domain.example/some-article | grep -i 'etag\|last-modified'`
must print nothing.

**5. Confirm that an AI assistant read the card**

> Ask an AI assistant to *consult/open* `https://your-domain.example/some-article?r=57391` and say
> which brands the page features. (Ask it to "open the page"; "read the page" can trigger a web search
> instead.)

* **A fresh URL every time** (`?r=<random>`): models cache what they fetched. The query string is not
  part of `page_url`, so card selection is unchanged.
* **A neutral article**, whose text does not already name the brand.

Check that the assistant reports facts found only in the card, not just the brand name.

## You are live when

Humans never get a card; agents get one, before the article and with its brand line, in both cache
orders; a direct call answers `200` or `204`, never a `4xx`.

## Symptoms

When you contact doubleshift, send the `X-Request-Id` of the call and, from your Worker, the record's
`record_id` and `api_rid`. Never send a key or a token.

> [accordion] The direct call answers 204
>
> A healthy no-fill: the page renders unchanged. The causes, in the order doubleshift checks them:
>
> * **Unusable `page_url`**: no host, or over 2,048 characters. Send scheme, host and path.
> * **No site of your account for that host** or its `www.` or bare alias. Register it in the key's
>   account ([Sites and fragments](https://docs.shftd2.com/sites.md)).
> * **No routing rule matches and the site has no default fragment.** Add a rule or set a default.
> * **The page's fragment is paused**, with no fallback. Activate it or route the page elsewhere.
> * **No active card is eligible on that fragment.** Activate a card, or add the fragment to its
>   eligibility ([Cards](https://docs.shftd2.com/cards.md)).
> * **The selected card's stored data is invalid**, even if its preview renders. Fix its fields under
>   **Edit** against [Card format](https://docs.shftd2.com/api/card-format.md).
> * **An unexpected error inside doubleshift**, answered as `204`. If it lasts, send the `X-Request-Id`.

> [accordion] The direct call answers 4xx or 429
>
> * `400`: `partner_id` or `page_url` is missing, or the call carries a parameter `GET /v1/fragment`
>   refuses ([the endpoint page](https://docs.shftd2.com/api/fragment.md)). Send only the parameters it documents.
> * `401`: no key in the `X-API-Key` header, or the key is unknown or revoked ([Keys and tokens](https://docs.shftd2.com/api-keys.md)).
> * `403`: the key belongs to another account than `partner_id` (**Settings → Account**).
> * `405`: a `HEAD` request (`curl -I`). Use `GET`.
> * `429`: too many calls for this key; wait for `Retry-After`.
>
> Every code is defined in the [API overview](https://docs.shftd2.com/api/overview.md).

> [accordion] Agents get no card, but the direct call answers 200
>
> The gate never matches the agent's `User-Agent`, your edge protection blocks the call to doubleshift
> (send the agent in `X-Agent-UA`, keep a benign `User-Agent`), or the card lands after `</body>` and
> is dropped. On the Worker library the outcome says where it stopped: `no_slot` (set `container`),
> `scan_limit`, `timeout`, `parse_error`, `aborted` ([edge library reference](https://docs.shftd2.com/api/edge-library.md)).
> The gate rules are in [Choose your edge](https://docs.shftd2.com/integrate.md).

> [accordion] No card and no record on a page outside scope
>
> Expected: no call, no record. A direct call to the fragment endpoint ignores `scope` and may still
> answer `200`. To cover the page, add its path to `scope` and redeploy
> ([scope](https://docs.shftd2.com/api/edge-library.md#scope)).

> [accordion] The Worker ignores scope
>
> Pages outside `scope` still get records with a `version` older than 1.1.4: download the library
> again, check its SHA-256 and redeploy. No page processed and a `scope_invalid` diagnostic: fix what
> its `reason` names ([diagnostics](https://docs.shftd2.com/api/edge-library.md#diagnostics)).

> [accordion] It worked, then stopped
>
> A key was revoked, a fragment paused, a card archived, or a rule, default or eligibility changed:
> each applies on the next call ([no cache](https://docs.shftd2.com/how-it-works.md#no-cache)). Rerun step 2.

> [accordion] Humans get the card too
>
> The gate is missing, or decided when the page enters the cache instead of on every request. Fix it
> ([Choose your edge](https://docs.shftd2.com/integrate.md)), purge the page and rerun step 4.

> [accordion] An agent gets a 304, or a cached page without the card
>
> A conditional request reached your origin, or the agent response carries `ETag` or `Last-Modified`.
> Drop `If-None-Match` and `If-Modified-Since` on the agent path and serve it without validators, as
> the Worker library does ([Choose your edge](https://docs.shftd2.com/integrate.md)).

> [accordion] The Worker logs invalid\_card
>
> A `reason` about the whole answer (`content_type`, `empty`, `too_large`, `read_failed`, `json`)
> usually means a changed `fragmentUrl` or something rewriting the answer: remove it. A field name as
> `reason`, with the default `fragmentUrl`: report the `record_id` and `api_rid`. Reasons:
> [invalid\_card](https://docs.shftd2.com/api/edge-library.md#what-it-does-with-the-page).

> [accordion] The Worker logs config\_missing or config\_invalid
>
> The page goes out without a card. `config_missing`: the API key or account id is absent; check the
> deployed secret. `config_invalid`: `field` names the invalid option. Store `logsToken` exactly as
> issued, or remove it; remove `beaconUrl` to restore the default.

> [accordion] Direct delivery logs report beacon\_failed or beacon\_skipped
>
> Each status and reason has its fix in [diagnostics](https://docs.shftd2.com/api/edge-library.md#diagnostics). Nothing at all:
> success is silent, and so is a missing token. Check the secret and the option.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.