> ## 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.

# doubleshift-edge reference

> Reference of doubleshift-edge, the Cloudflare Worker library: version and SHA-256, every option with its default, the scope of the pages it works on, the outcome of each agent page, the delivery record fields, direct delivery logs and their diagnostics.

`doubleshift-edge.v1.js`, one file with no dependency, for a Cloudflare Worker: version `1.1.4`,
SHA-256 `d4db890cbf27a2da1977d3ba76e0585ab3bf7cc78111eaddc71019527114e9d1`, served at `https://api.shftd2.com/edge/doubleshift-edge.v1.js`. Install it with
the [quickstart](https://docs.shftd2.com/quickstart.md); earlier versions are in the [changelog](https://docs.shftd2.com/changelog.md).

It exports `enrichWithDoubleshift(request, response, options, ctx)`, which returns the page to send, and
`prepareOriginRequest(request, config)`, which drops the conditional headers from an AI agent's `GET`.
`config` is optional: with a custom `agentPattern`, pass `{ agentPattern }` with the same pattern to both
functions, or an agent the enrichment recognises still sends its conditional headers to your origin.

## Options

| Option | Default | What it does |
| - | - | - |
| `apiKey` | — | Required. Your API key, sent in `X-API-Key` only. |
| `partnerId` | — | Required. Your account id (`acc_…`). |
| `container` | `article` | CSS selector: the card before its first match, the disclosure line inside it. |
| `cachePolicy` | `private, no-store` | `Cache-Control` of an enriched page. |
| `canonicalOrigin` | the request's origin | Bare `https` origin of `page_url` when the Worker sees another host. |
| `language` | `auto` | English if `<html lang>` starts with `en`, else French; or `fr`, `en`. |
| `timeoutMs` | `800` | Budget of the call, in milliseconds. |
| `agentPattern` | the [agent list](https://docs.shftd2.com/api/agents.md) | RegExp of the `User-Agent`s that get a card; its first capture group is the marker. |
| `fragmentUrl` | `https://api.shftd2.com/v1/fragment` | Change it for a test endpoint only. |
| `log` | `console` | Receives records and diagnostics ([below](#your-own-log-function)). |
| `deliveryCode` | `false` | Prints the [delivery code](https://docs.shftd2.com/api/card-format.md#what-your-integration-adds) on the card and disclosure line. |
| `logsToken` | not set | Turns on [direct delivery logs](#direct-delivery-logs): `ds_logs_` + 40 lowercase hex characters. Missing or empty: off. |
| `beaconUrl` | `https://logs.shftd2.com/v1/beacon` | Absolute `https`, without credentials, query, fragment or secret; checked even without `logsToken`. |
| `quietInfo` | `false` | With direct delivery logs and a usable `ctx`, `served` and `no_fill` reach `log` only if sending fails. Not under Logpush. |
| `skipIfEnriched` | `false` | Skips a page already carrying `article.abc-card[data-ds-delivery]` before or on the container: no call, no record, an `already_enriched` diagnostic. Second Worker only. |
| `scope` | not set | From 1.1.4. The paths this configuration works on ([scope](#scope)). Not set: every page. |
| `enable_request_signal` | off | A Worker compatibility flag, not an option: an agent that hangs up during the call cuts it short (`aborted`). |

An invalid value gives `config_invalid` with its `field`, before anything is read, and the page goes out
unchanged. An unusable `scope` gives `scope_invalid` instead ([scope](#scope)).

## Scope

`scope` is checked first, on every request. Out of scope, the Worker returns the origin's response as
it came: no call, no record, no sending.

```js theme={"theme":{"light":"css-variables","dark":"css-variables"}}
scope: {
  include: ["/articles/*"],
  exclude: ["/articles/private/*"],
}
```

* **Patterns**: a path starting with `/`, exact (`/articles/geely.html`) or a section ending in `/*`:
  `/articles/*` covers `/articles`, `/articles/` and every path under it, never `/articles-other`. `/`
  is the home page only, `/*` every page. No host, query, hash, whitespace, backslash, `.` segment,
  `..` anywhere, malformed `%` sequence, `//`, or `*` elsewhere; at most 200 patterns per list.
* **Which pages**: no `exclude` pattern matches and, if `include` is set, one of its patterns does.
  `exclude` always wins; `include: []` takes no page.
* **Comparison**: as [routing](https://docs.shftd2.com/how-it-works.md#resolution) reads the path, query, hash, trailing slash and
  ASCII case ignored. The request's own path, on any host; `canonicalOrigin` only changes `page_url`.
* **In scope**: routing and card eligibility decide. A path in scope does not guarantee a card.
* **A scope that cannot be used** never means every page: nothing is processed, and one
  `scope_invalid` [diagnostic](#diagnostics) is written per isolate and per reason.

A copy older than 1.1.4 ignores `scope` silently: download the file again and check its SHA-256.

## What it does with the page

Only an AI agent's `GET` answered `200 text/html` is touched. Nothing reaches the agent before the
decision, taken at the first container: one call, `GET /v1/fragment` with `format=json`, `X-Agent-UA` and
the page's `record_id` in `X-DS-Record-Id`, redirects not followed. `page_url` never carries the query
string. No container in the first 256 KiB, or within 800 ms of the origin's headers: no call.

With a card, the enriched page loses its validators, `Age`, `Expires`, length, encoding and origin cache
directives, takes `cachePolicy` and adds `Vary: User-Agent`. Otherwise the page goes out as the origin
sent it.

| `outcome` | Meaning |
| - | - |
| `served` | Card inserted, and the enriched page's stream ended normally. |
| `no_slot` | No container in the page; no call. |
| `scan_limit` | No container in time (`limit`: `bytes` or `time`); no call. |
| `parse_error` | A refused charset (`reason: charset`), or markup the parser fails on. |
| `origin_error` | The origin's stream broke before the decision. |
| `no_fill` | `204`: nothing to serve. |
| `upstream_status`, `timeout`, `fetch_failed` | Another status, no full answer within `timeoutMs`, or none. |
| `invalid_card` | Not a usable card (`reason`). |
| `aborted` | The agent left before or during the call (with `enable_request_signal`), or while reading. |
| `stream_error` | The page broke after the decision to insert the card; `card_written` says whether the card's end was handed to the runtime. |
| `config_missing`, `config_invalid` | No key or account id; an invalid option. |
| `body_unavailable` | Your code read the body before the call. |
| `internal_error` | An unexpected failure inside the library. |

`invalid_card` reasons: `content_type`, `empty`, `too_large` (over 64 KiB), `read_failed`, `json`, or the
first refused field, from `payload` to `source.newsroom_url` ([card format](https://docs.shftd2.com/api/card-format.md#json-fields)).

## Delivery records and logs

Each agent page in scope normally writes one record, `event: "doubleshift_edge"`; a fallback `internal_error` may
follow with the same `record_id`. Fields: `version`; `level` (`info` for `served` and `no_fill`, `error`
for configuration, body, origin, stream and internal failures, else `warn`); `outcome`; `agent`, the marker
found; `page_url`; `record_id`, a UUIDv7 sent in `X-DS-Record-Id`; `ts`, ISO 8601 UTC; `duration_ms` until
the record. After an answer: `status`, `api_rid` (its `X-Request-Id`), `api_ms` (the call alone). With a
card: `brand` (120 characters at most), `resolution` (`rule`, `default`, `id` or `other`), `card_written`
(the runtime took the card's end for sending, not a network acknowledgement). `reason`, `field`, `limit`
and `error` are fixed codes.

### Direct delivery logs

With `logsToken`, each record also goes to doubleshift through `ctx.waitUntil`, the token in
`Authorization` only, 10 s per attempt. A timeout, a network error or a `503` gets one more attempt 2 s
later; nothing is queued. An invalid `agentPattern`, `logsToken` or `beaconUrl` is checked before the
sender exists, so its record stays local; with a valid token and a usable `ctx`, any other
`config_missing` or `config_invalid` record is sent like the others. Records carry no article HTML or cookies, and both secrets are redacted. Contract: [direct delivery logs API](https://docs.shftd2.com/api/beacon.md); which transport:
[delivery logs](https://docs.shftd2.com/delivery-logs.md).

#### Diagnostics

`event: "doubleshift_edge_diag"` with a `kind`, written through `log`, never sent.

```json title="A sending diagnostic" theme={"theme":{"light":"css-variables","dark":"css-variables"}}
{"event":"doubleshift_edge_diag","version":"1.1.4","level":"warn","kind":"beacon_failed","record_id":"0192f3a4-5b6c-7d8e-9f01-23456789abcd","status":503,"attempts":2}
```

| Diagnostic | Meaning and fix |
| - | - |
| `beacon_failed`, `status: 401` | Token refused: check the deployed secret, ask doubleshift if it is revoked. |
| `beacon_failed`, `status: 403` | A logs token for Logpush (`wrong_transport`), or a cookie: use the token for direct sending. |
| `beacon_failed`, `status: 400` or `413` | Record refused or over 8 KiB: send doubleshift the `record_id`, version and status. |
| `beacon_failed`, `status: 429` | The token's limit; not retried. |
| `beacon_failed`, `status: 503`, `error: "TimeoutError"` or `"TypeError"` | Failed after the retry. Report recurring ones. |
| `beacon_failed`, `error: "AbortError"` or `"error"` | Another failure; not retried. |
| `beacon_failed`, any other status | Not `202`, redirects included: remove any `beaconUrl` override. |
| `beacon_skipped` | `reason: no_ctx` or `ctx_refused`: pass your handler's `ctx` as the fourth argument. |
| `already_enriched` | `skipIfEnriched` found a numbered card; this Worker made no call and no record. |
| `scope_invalid` | No page is processed. `reason`: `not_object`, `empty` (no list), `unknown_key`, `not_array`, `too_many` (over 200), `not_string`, `too_long` (over 512 characters, or 200 normalised) or `pattern`. Fix `scope`. |

Success is silent, and so is a missing or empty token.

### Your own log function

`log(record)` replaces console output for records and diagnostics. Forward records with every field:
`record_id` links a diagnostic that carries one to the page, `api_rid` to the delivery. Send only `doubleshift_edge` records
to doubleshift. With `logsToken`, your changes do not reach what is sent. A function that stops writing to
the console also removes the records from what Logpush exports.


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