From the server to the page
How the server half sends the request's own verdict to the page in a Server-Timing entry, and when.
The transport sends the request's own verdict from the server half to the page half. An adapter adds one Server-Timing entry to the response, and the page half reads the entry when it starts.
Server-Timing: botscent;desc="1;chatgpt;1759300000123;signer.web-bot-auth.verified"
Cache-Control: no-storeWhy the page needs the entry
Some agents show their evidence only in the request. For example, a hosted agent can sign the navigation and nothing else. The page half cannot see that signature. With the entry, the page verdict holds the evidence of both halves, so a single page sees both.
When there is no entry, the page half has no server evidence. It does not count the absence as evidence of a person.
When an adapter writes the entry
An adapter writes the entry only when both of these are true:
- The request's own verdict is an agent.
- The request is a document navigation. That is
Sec-Fetch-Dest: document, or, without fetch metadata, aGETthat acceptstext/htmlfrom a user agent that begins withMozilla/.
Link previewers, crawlers that name themselves and HTTP clients never run the page half. So they get no entry and no no-store. People's responses never carry an entry.
The adapter also does 3 things to keep the entry away from people:
- It writes the entry after the shared-cache lookup, at most once for each response.
- It adds
Cache-Control: no-storeto each response with an entry, so that no cache stores the agent's entry. - On every response it passes, it removes each
botscententry that the response already has. It keeps all otherServer-Timingentries.
The format of the entry
The entry has 4 fields, separated by ;:
| Field | Example | Meaning |
|---|---|---|
| Version | 1 | The major version of the format, and an optional minor version. |
| Name | chatgpt | The agent name. It is empty when the verdict has no name. |
| Time | 1759300000123 | When the server received the request, in milliseconds since the epoch. |
| Reasons | signer.web-bot-auth.verified | The reasons, separated by commas. |
An entry is at most 256 bytes. A reader discards an entry that does not match the format, that is longer, or that has an unknown major version. A reader keeps reasons that it does not know.
What the page accepts
The page half accepts one entry whose time is between 10 s before and 120 s after the page's own navigation start. The page half ignores the entry in these cases:
- The response has more than one
botscententry. - The entry does not match the format.
- The entry's time is outside the window.
An accepted entry adds its reasons to the page verdict. Its name counts as a declaration. The browser shows Server-Timing to a page only in a secure context, that is on https or on localhost.
diagnostics().transport tells you what happened to the entry: pending, received, absent, unsupported or rejected.
The transport option
Every TypeScript adapter has a transport option, with these values:
'auto': the adapter sends the entry only where it runs for each request in front of the cache.'always': you state that no shared cache stores your HTML.'never': the adapter never sends the entry.
In Python, the adapters take transport=True. In Django, set BOTSCENT_TRANSPORT = True in the settings. The Express and Astro adapters take 'always' or 'never'.
The default depends on where the adapter runs:
| Adapter | Transport by default |
|---|---|
botscent/next | On when the app runs on Vercel. Off when it is self-hosted. |
botscent/vercel | On. |
botscent/workers, for Cloudflare Workers and Netlify Edge Functions | On. Behind Workers Cache, cached pages carry no entry. |
botscent/hono | On when the app runs on Cloudflare Workers. Off on Node.js. |
botscent/express | Off. |
botscent/astro | Off. |
botscent.asgi, botscent.django, botscent.flask | Off. |
Why the transport is off at an origin
An origin server, such as Express or Django, cannot see whether a CDN in front of it stores HTML. A cache can ignore Cache-Control: no-store. Then the cache serves an agent's entry to a person, and the person's page reports an agent.
The page half's time window limits how long a stored entry can be replayed. It cannot show that the entry was written for this response. In a test, nginx ignored Cache-Control and replayed an agent's entry to a person within the same minute.
So origin adapters leave the transport off. With the transport off, your server code still gets the request's own verdict. Only the page half does not get the server's evidence.
Warning: Set
transport: 'always'only when no shared cache stores your HTML. A cache that ignoresno-storeserves an agent's entry to people, and their pages then report an agent. After the change, runnpx botscent checkand read thepersonline.
Limits
- The Next.js proxy cannot see headers that the route sets later. On the agent navigations it changes, its
Server-Timingreplaces one that the route set. People's responses do not change. - Vercel Routing Middleware runs before the response exists. It cannot remove an entry that the response itself carries, and Vercel appends the middleware's
Server-Timingto the response's own. - CloudFront with a minimum TTL above zero served cache hits without the origin's
Server-Timingin a test. - The Cache API in a service worker does not follow
Cache-Control. If a service worker caches pages, keep navigations with abotscententry out of its cache. As an alternative, leave the transport off.
Related
- Verify your install: the
entry,no-store,person,cacheandtransportchecks test the transport. - The trust model: why the page verdict is not fit to decide access.
- Server API (TypeScript): the options of
inspectand of each adapter.