classification
The five checks, cheapest first
Detection is pure compute: no network calls, no crypto, just a handful of header string scans well under 1ms. Every check that fires is recorded in signals; the highest-precedence one classifies.
Accept: text/markdown(explicit in the list, q-values honored,q=0excluded) →agent,acceptsMarkdown: true, high confidence. Claude Code and OpenCode send this. Platform attribution comes from Signature-Agent or UA if present, elseunknown.- Web Bot Auth headers:
Signature-Agent(e.g. "https://chatgpt.com" →chatgpt) →agent, high confidence; a bareSignature+Signature-Inputpair →agent/unknown, medium confidence. Parsed only bydetect();verifiedstaysfalsehere because cryptographic verification is the separate asyncverifyWebBotAuth()step (see below). - Protocol route on the URL pathname:
/.well-known/ucp,/.well-known/acp,/mcp,/acp/*(each also matching subpaths) →protocol. - UA heuristics: a fallback only, capped at medium confidence. Known agent UAs:
claude-code,opencode,ChatGPT-User,OAI-SearchBot,GPTBot,PerplexityBot,Perplexity-User,Gemini/Google-Extended,anthropic-ai/claude-web(plusClaudeBot/Claude-User),meta-externalagent/meta-externalfetcher(Meta),Amazonbot,DuckAssistBotandMistralAI-User. Crawler UAs:Googlebot→crawler/googlebot,bingbotandadidxbot→crawler/bingbot,MicrosoftPreview→crawler/microsoft-preview,Applebot→crawler/applebot.Applebot-Extendedis a robots.txt token that never crawls, so it has no pattern. - Default →
human. High confidence when a browserish Accept (text/html) or UA (Mozilla/) is present; low otherwise (empty or garbage headers).
From detection to serving path
The gateway maps the detection onto a serving path (a Response means markdown was served; null means your HTML pipeline runs as if the gateway weren’t there):
| Detection | Path | Behavior |
|---|---|---|
Agent with Accept: text/markdown or an identified platform |
markdown |
Render from sources; Response returned |
| Agent (signed/identified) on a protocol route, no markdown Accept | protocol |
Routed to the protocols hook, since signed protocol clients must reach the protocol handler |
Agent, unidentified, no markdown Accept (e.g. bare Signature pair) |
html |
null, because the gateway never guesses a format nobody asked for |
| Human | html |
null; your HTML pipeline runs as if we weren’t there |
| Crawler (Googlebot, bingbot, Applebot) | html |
null, always; see the guardrail below |
| Protocol route | protocol |
config.protocols hook when wired; null otherwise; the event records the demand either way |
The Googlebot guardrail
Known search crawlers receive canonical HTML. The gateway renders the same source offer in markdown for matching agent requests; a user-agent claim does not authorize access to private terms.
Confidence semantics
DetectionResultCopy
const result = detect({ headers: req.headers, url: req.url, method: req.method })
// {
// kind: 'agent' | 'human' | 'crawler' | 'protocol',
// platform: 'claude-code' | 'opencode' | 'claude' | 'chatgpt' | 'gemini'
// | 'perplexity' | 'meta' | 'amazon' | 'duckduckgo' | 'mistral'
// | 'googlebot' | 'bingbot' | 'microsoft-preview' | 'applebot'
// | 'unknown' | null,
// verified: boolean, // ALWAYS false from detect(); verification is the
// // separate async verifyWebBotAuth() step
// acceptsMarkdown: boolean, // Accept explicitly lists text/markdown with q > 0
// signals: string[], // ordered checks that fired, e.g. ['accept:text/markdown', 'ua:claude-code']
// confidence: 'high' | 'medium' | 'low',
// }
- high: an explicit negotiation signal (
Accept: text/markdown, or an identifiedSignature-Agent). Also the browserish-human default. - medium: UA heuristics and bare signature pairs. These identify a claimed requester, not verified authority.
- low: the default
humanclassification with empty or garbage headers. - Authorize private actions through the appropriate verified credentials and scopes. Web Bot Auth verification is separate from a parsed platform claim.
Fixture-driven testing
Detection rules are tested against versioned header fixtures. Add a regression fixture when introducing a new rule, and keep verified identity separate from heuristic classification.
Verification (Web Bot Auth)
Verification is a separate async step from detection: detect() stays synchronous, parse-only, and its verified is always false. verifyWebBotAuth() cryptographically verifies a Web Bot Auth signature chain (RFC 9421 Ed25519 message signatures) against a caller-injected key registry. It is pure crypto over ambient WebCrypto with zero network, and it never throws: every malformed or adversarial header resolves to { verified: false, reason }.