How Telltale works
Telltale watches how a signed-in page is used and tells you whether a person or an AI agent did it, with how it knows, so your own application can decide what to allow. It runs the live classifier heuristic-v4.33.
For developers, the path is: browser SDK, then session token, then your authenticated backend, then verification, then your own authorization decision.
The session
- The hosted tag starts recording on page load. A site with a consent banner holds it (data-telltale-wait-for-consent, or waitForConsent: true), and then the SDK does nothing until your consent manager permits collection. Once collecting it opens a session with the Telltale server and observes coarse interaction and environment signals: pointer, keyboard timing, scrolling, focus, navigation and automation traces. It never records typed values, cookies, credentials or request-body values.
- The Telltale server keeps the evidence server-side and classifies the session as human, agent or unknown. Legacy scores (1, 99 and 50) are categorical labels, not probabilities. Separate weighted evidence groups correlated observations, keeps human support distinct and explains the result; it is not a calibrated probability. Unknown means the evidence is insufficient; it is not counted as human and never treated as human.
- client.identify() may attach user and account IDs from the page (browser-supplied). They label telemetry and are not verified.
- client.getSessionToken() returns an opaque, signed, expiring reference to the session. It contains no evidence, identity or verdict.
- Your backend sends that token with the server key to POST /api/v1/verify immediately before the protected action and receives the verdict, freshness, the optional agent name with how we know it (attribution) and the decision from the action's policy (allow, review or deny).
- Optionally, your backend calls POST /api/v1/identify to attach identity from your server (server-associated), derived from its own session.
Analytics and consequential decisions
What backs an AI agent verdict is its agentEvidence, and its confidence band follows from it (a strength level, never a probability). High confidence: Verified (a valid Web Bot Auth signature), an automation trace (webdriver flag, driver-defined trace or headless browser), input injected by software (keys with no hardware delay), sustained behaviour (enough consistent behaviour evidence on those pages, two independent signals agreeing) a behaviour change after a person (a handover), or several behaviour families (keyboard timing, pointer and clicks, text insertion) agreeing on one page when the session shows no physical-device input. Medium confidence: behaviour patterns alone, or several families when the session shows physical-device input. High confidence follows the action's AI agent rule, Deny included, and is chargeable. Medium confidence is held for review (step-up) by default, the site owner may switch it to block, and it is never charged. A session that has shown a person on a physical device is not turned into an AI agent by a brief odd moment, only by proof or a sustained change. Related pointer and timing observations count as one behavioural family, not several votes. See Verdicts for the table and its assistive-input limitations.
Human needs more than input that looks human. The page's behaviour stream must also show that the input came through a physical device: the few milliseconds between an event happening and the page seeing it, or pointer samples at a steady device rate. Without that the session stays Unknown. A site that turns the behaviour stream off (behaviourStream: false) therefore gets no Human verdicts. On Chromium-based browsers, input with essentially no delay at all (at least 10 recent key events, typical delay under 1 ms, a tenth of them with no delay at all; mouse presses and wheel steps are left out) was injected by software and reads AI agent at high confidence; on Safari, iOS browsers and Firefox it stays Unknown until checked with real people there. See Verdicts.
Who decides what
- Your consent manager decides whether collection is permitted. The SDK enforces the decision locally: a start paused until consent waits, withdrawal cancels owned work and discards pending telemetry, and reapproval starts fresh.
- Your backend decides authorization. A verdict, identity or signature never authorizes an action by itself, and a verification failure is not a verdict.
- Naming the agent (attribution) is separate from the verdict. An agent is named only with how we know the name: Verified (
signed, a valid Web Bot Auth signature from a reviewed operator), Unconfirmed (weak-marker, a renamable page marker, named only beside an agent verdict), Self-reported (declared, the client named itself in its user agent; anyone can send it, named only beside an agent verdict) or Not named (unattributed-automation, shown as an unnamed agent). A valid signature is also verdict evidence and makes the session agent; a page marker never does, and beside an unknown verdict it is shown only as a page-marker diagnostic. An active page marker in any tab still keeps the session from being called human: it is held at unknown. Generic automation never establishes which AI model is driving. - Each browser tab (context) keeps its own observed verdict. When one tab turns agent while a person is still active in another, the timeline records a parallel agent context, not a handover, and the person's tab is never counted as agent time. The session verdict combines tabs conservatively: it is never human while any tab shows an agent sign. An agent's time in control ends at its last agent evidence plus the 60-second evidence window, so a session that comes back hours later is not credited to the agent for the gap.
Browser probes
The SDK's probes send fixed labels only, never raw values: no user-agent strings, client-hint brands, renderer names, stacks, sizes or property names. Probes read property descriptors and call built-in methods captured when the SDK loads, so a page that later replaces a built-in is never invoked by a probe.
- Verdict evidence: navigator.webdriver, a HeadlessChrome user agent and a small set of framework globals.
- Framework artifacts: Playwright, Puppeteer, Selenium, ChromeDriver (including the
cdc_…_Array/Promise/Symbolnames current ChromeDriver puts on window), PhantomJS, Nightmare. They name a framework, never an AI model, and only as Unconfirmed (weak-marker) beside an agent verdict in a WebDriver-controlled page. - Diagnostics (shown as reasons, never a verdict by themselves): whether navigator.webdriver was redefined or deleted (
removedin an engine that should have it); whether a hidden same-origin frame, a dedicated worker or a shared worker reports different browser details from the page (a property present on one side only is a mismatch); whether the console was serialized by a debugger (reported unsupported on engines where this cannot be observed); blank or nonstandard client hints; core document methods redefined on the page; automation-tool globals; Electron or PhantomJS runtimes. - Some of these (deleted webdriver, shared-worker user-agent mismatch, blank or nonstandard client hints, automation globals, PhantomJS) feed the heuristic-v5 shadow rule as one runtime class, which can make the shadow candidate agent only beside an independent input-behaviour class. The shadow candidate is shown beside the verdict and changes nothing; there is no switch that promotes it.
- The frame, canvas and WebGL checks run off the start path: frame and canvas work waits for an idle moment, and WebGL is read inside a worker on an OffscreenCanvas where the browser supports it.
Ordinary people can trip some diagnostics: open DevTools (console serialization), user-agent or privacy extensions (worker, shared-worker and client-hint checks), and page libraries that patch document methods. That is why they are diagnostics, not verdicts.
Behaviour and environment diagnostics
These are recorded per session as labels, counts and rounded measures and shown with the other probe and input diagnostics. None of them changes a verdict, a score, a reason or the heuristic-v5 shadow rule: they are collected so their prevalence can be measured on test-set human trials before any is considered as evidence. Nothing here collects typed content, key identities or values.
- Input behaviour, summarised in the browser (the per-sample timing and geometry the server sees are in the behaviour stream below): pointer samples per second of pointer activity, including coalesced samples; the share of clicks with no pointer trajectory in the 1.5 seconds before the press; key hold times with a sub-millisecond bucket and sub-millisecond gaps; overlapping keystrokes (a person's down-down-up-up rollover, human-leaning but still only a diagnostic, because unknown is never human); paste counts by pattern (shortcut paste, select-all then paste, paste with no shortcut); wheel bursts and how many are single discrete steps; scrolls with no wheel, key or press before them; untrusted (script-dispatched) event counts per type; and the median lag between an event's own timestamp and the moment the listener ran. The pointer-rate and teleport-click definitions follow Choudhary et al., "What Does It Take to Detect an AI Agent?" (arXiv 2607.26935, CC BY 4.0); the key-hold, keystroke-overlap, paste and scroll-burst features follow Wang, Shafiq and Vekaria, "FP-Agent" (arXiv 2605.01247, CC BY 4.0). Feature definitions only; no code or data from either paper is used.
- Movement shapes (added 2026-11-02; shadow only). In the FP-Agent research no AI agent's click followed real pointer travel, while people drift and curve toward what they click. The browser now measures this from every real mouse movement and key press, keeps the raw positions and times to itself, and sends only a few rounded numbers per page: for each of the last 16 mouse presses, how many separate pointer positions came in the 1.5 seconds before it, how far the pointer travelled, how straight that path was (distance travelled divided by the straight-line distance) and how much it curved; for the page, how many pointer positions and movement stretches there were and the average, middle, spread and range of the path's turning angle and bend; and the same four numbers for how long keys were held down and for the pause between releasing one key and pressing the next. Which key was pressed is used only to match a press with its release and is never sent; typed text, field contents, and keys in password fields or inside
[data-telltale-ignore]are never measured. The collector refuses the whole set if any number is missing or out of range. Two plain technical notes can appear (every recent press arrived with no pointer travel; every path into a press was ruler-straight). They never change a verdict, a reason, an action check, protection or what is chargeable. The turning-angle, bend, segment and key-timing definitions are adapted from the FP-Agent authors' code with their written permission (licenses/fp-agent-permission.txt). - Device sampling (added 2026-11-03; collected and measured only, no verdict reads it yet). Four small groups of counts that tell a physical mouse and keyboard from injected input, sent with the input summary as whole numbers and fixed labels: for real mouse movements, whether the browser offers merged samples at all (a browser without them, such as some Safari versions, is reported as "unsupported", never as one sample per move), how many moves carried more than one merged sample, and how evenly those samples are spaced (the middle gap, its spread, the share of gaps that sit on the device's own rate, and an eight-bucket count of gap lengths up to 40 ms); for mouse presses, how many reported pressure 0, 0.5, another value or none (the pressure itself is not sent); while Claude in Chrome's on-page cursor is showing, how many real presses landed within 8 px of it, at how many different spots, and how often it had moved since the previous press (no position leaves the page); and for held keys, how many auto-repeat events came, how many repeats lacked the browser's repeat flag, and for up to eight held keys the delay before repeating began and the median and spread of the repeat gaps. Which key was held is used only to pair a repeat with its press and is never sent; repeats in password fields and inside
[data-telltale-ignore]are not measured. The collector refuses the whole group if any number is missing or out of range. - Cross-stream consistency, after the idea Cloudflare describes for Precursor: text keys pressed while no editable field has focus (shortcuts, Tab and Space are excluded); pointer movement while the tab is hidden; an editable field focused with no press or key just before it; input to an element other than the focused one; and clicks while navigator.userActivation says the page has never been activated (script-dispatched clicks never grant activation, while CDP and operating-system input do).
- No-operator checks: on trusted input that a person's device sends only to an active, focused page (a click, a key release other than a modifier or Escape, or a text input), whether navigator.userActivation says the page was never activated, whether document.hasFocus() is false, and, once per page, whether a silent AudioContext stays suspended. The audio check runs only in Chromium, renders to no output device (sinkId none), is never resumed and is closed as soon as its state is read; Chromium may log its autoplay warning once when the page has no activation. Counts and labels only.
- Chrome brand check (chromeBrand): the user agent reads as Google Chrome but userAgentData.brands lists only Chromium. Chrome for Testing, Playwright's bundled Chromium and agent-browser's default launch show this, and so do plain Chromium and Vivaldi users, so it is never evidence. Browsers that name themselves (Edge, Opera, Brave, Samsung Internet, Yandex, Android WebView and others) read other-browser; a user agent without a Chrome token reads not-claimed.
- When the chromeBrand label or a no-operator check is observed, the session view's diagnosticNotes and the demo panel's Technical notes say so in plain words, each ending "diagnostic only, it never changes the verdict". They are never reasons.
- Marker and viewport timeline: the page narrowing (inner width down at least 120 px) inside a window of the same outer width, which is what a browser side panel opening looks like, recorded beside the time an agent marker appears. Sizes never leave the page.
- Cloud-VM environment tells: a software WebGPU adapter (SwiftShader, llvmpipe), no media devices, no speech voices, a Linux font set, and an empty keyboard layout map, plus a count of these signs (cloudVmTells: none, one, two, three-plus). Ordinary Linux desktops, machines without speakers or cameras and privacy settings trip these too.
- Runtime shape: whether sensitive built-in functions still read as native when formatted by a fresh frame's own Function.prototype.toString (a Proxy-wrapped or script-replaced function does not), whether a dedicated worker's WebGPU adapter matches the page's, and further automation names (webdriverio, Cypress, Playwright's binding controller, and selenium, webdriver and __webdriver_script_fn as window or document properties). The ChromeDriver pattern also matches the newer
_Object,_Proxy,_JSONand_Windownames.
Behaviour stream and the behaviour-v1 shadow model
After consent, the SDK also records a compact behaviour stream and sends it with its ordinary event uploads (one batch per upload, at most 16 KB, normally 1 to 2 KB). The Telltale server turns it into features and runs a separate rule set, behaviour-v1, in shadow: its result is stored with the session and shown under Technical as "Behaviour shadow model, not the verdict". The shadow model itself never changes the verdict, score, reasons, driver, attribution, policies, webhooks or what is chargeable, and there is no switch that promotes it; that needs a code change after a test-set evaluation shows no added human false positives. Since 2026-10-28 a few stricter cuts of its features reach the live verdict, only when clues from two or more different families agree in one page (timing, pointer and scrolling, page side effects, network); the Site setting "Behaviour signals can call AI agent" decides whether that makes the session read AI agent or only keeps it from reading Human. Verdicts lists the clues, the exclusions for people and the known risks. Unknown is never human.
What a batch contains, all as small integers:
- Pointer movement: time, position as thousandths of the viewport (not the page), and how many coalesced samples the browser delivered; at most one sample per 15 ms, and every dispatch is counted.
- Presses (pointerdown, pointerup, click): time, where the press landed relative to the target's own box (thousandths of its half-size, so the centre is 0), the pointer position in viewport thousandths, pointer type, and whether the click came from the keyboard.
- Dispatch latency: the gap between an event's own timestamp and the moment the listener ran.
- Wheel deltas and timing, and window scroll steps as thousandths of the viewport height.
- Keys: down or up, a press number that pairs each press with its release (restarting on every page), and an intent category: text, navigation, modifier, correction, activation or other.
- Text input: the input type (insertText, insertFromPaste, deleteContentBackward and so on) and the change in length.
- Paste, copy, cut, composition start and end, focus in and out (editable or not), visibility and window focus changes, as event codes.
- Whether each event was trusted, and how many samples were dropped under the caps.
What is never collected: key values or codes (which key or character), typed text, field values or their contents, clipboard contents, element names, selectors, attributes or page content, and nothing at all from password fields or from elements inside [data-telltale-ignore]. There are no identifiers: press numbers restart on every page. Under pressure the SDK thins pointer samples first, then drops the oldest rows, and counts what it dropped.
Withdrawal (optOut, a refused or withdrawn preference, reset) discards the pending batch, and a revoked session's raw batches are deleted on the Telltale server. behaviourStream: false in init turns the stream off.
Retention: raw batches are kept for the server's raw-behaviour window (default 7 days, never more; an operator can set it shorter) and at most 600 per session; features (rounded statistics, no samples) and the shadow result stay with the session and leave with it. Subject erasure deletes both.
Features: pointer strokes (straightness, speed variation, turning angle, acceleration changes, jerk), coalesced samples, press hold times, presses the pointer never travelled to, click offsets from the target centre, dispatch latency, key hold and gap times, overlapping keys, text inserted without key presses, paste, wheel and scroll cadence. Rules: a strong rule alone decides agent (pointer presses released within 5 ms; presses at points the pointer did not travel to; several characters inserted at once without keys, paste or composition). Supporting signals (exact-centre clicks, no dispatch delay, machine-regular key timing, straight constant-speed paths, identical wheel steps, script-dispatched input, scroll jumps without input, text without key presses) decide agent only in pairs. Human needs at least 30 trusted interactions, curved paths with varying speed reaching each press, human press and key timing, a hardware-like trait (coalesced samples, overlapping keys or varied dispatch delay) and no agent signal. (The live verdict borrows the dispatch-delay and steady-polling part of that trait as its Human requirement; the shadow model itself still never decides.) Everything else is unknown. Labelled evaluation snapshots keep the features, not the samples, and the evaluation report lists behaviour-v1 beside the live classifier and heuristic-v5.
The SDK sends the first batch straight after init, uploads early once 50 events are queued, and sends the three instant environment booleans with init itself, so a WebDriver-flagged or headless browser is answered by the init response using the existing rules; nothing from init is stored.
The session timeline
Every session's timeline begins with a started entry: type started, at, verdict (the verdict when the session opened, normally unknown), browser and os. The Telltale server derives browser (Chrome, Safari, Firefox, Edge or other) and os (macOS, Windows, Linux, iOS, Android or other) from the User-Agent header of the request that opened the session and keeps only those labels; the raw user agent is never stored. They are display labels, not evidence: a user agent can say anything, so they never affect a verdict or an attribution. The session view returns the entry in transitions and again as started.
The started entry is not a verdict change. Later entries are verdict_change (a classification change, with the attribution at that moment and, for a change to agent, the handover latency) or controller_change (attribution changed while the verdict stayed the same). The latest takeover, session.verdict_changed, session.identified (which needs a real verdict change first) and analytics count verdict changes only.
Which agents can be named
An agent is named only from a public signal, always with how we know the name: a Web Bot Auth signature from a reviewed operator directory (Verified, signed), a renamable page marker beside an agent verdict (Unconfirmed, weak-marker), a self-declared user-agent token beside an agent verdict (Self-reported, declared), or a crawler or fetcher verified against its operator's IP-range list or by forward-confirmed reverse DNS (Verified by network address, ip-verified). Google says Google-Agent is "used by agents hosted on Google infrastructure" and signs only a subset of its requests while the protocol is experimental; its directory uses short key IDs instead of JWK thumbprints, which this Telltale server accepts for that reviewed operator only. Google-Agent is named as Google publishes it, not as any particular Google product.
Signing operators
Every operator below names a platform or operator, never the user, the customer agent or the AI model. Keys are never built in: each directory is fetched when a signature arrives and cached for its Cache-Control max-age minus the response's Age (CDN copies arrive partly aged), never past the earliest key expiry still ahead. When a signature names a key that the cached copy lacks or shows as expired, the directory is refetched once, at most every 30 seconds, before the signature fails. Kernel, for example, republishes the same key with a sliding one-hour validity window, and ChatGPT's key expiry is always about seven days after each fetch. Documented says whether the operator itself ties the name to the directory.
Type follows Cloudflare's signed-agent field: Direct means only the operator acts through this identity; Intermediary means a platform that many customers' agents run on. A Verified name for a cloud browser platform (Intermediary) names the platform its customers' agents run on, not the customer agent. The attribution carries it as operatorType (direct or intermediary).
| Operator | Signature-Agent origin | Type | Documented |
|---|---|---|---|
| ChatGPT agent | https://chatgpt.com | Direct | Yes (OpenAI) |
| Google-Agent | https://agent.bot.goog | Direct | Yes (Google; experimental) |
| Browserbase | https://www.browserbase.com | Intermediary | Yes (Browserbase identity docs) |
| Anchor Browser | https://api.anchorbrowser.io | Intermediary | Yes (Anchor; per session, off by default) |
| Kernel | https://www.kernel.sh (the bare kernel.sh redirects and is not accepted) | Intermediary | Yes (Kernel bots page) |
| Cloudflare Browser Run | https://web-bot-auth.cloudflare-browser-rendering-085.workers.dev (Cloudflare may change this host) | Intermediary | Yes (Cloudflare Browser Run docs) |
| Manus | https://api.manus.im | Direct | No: operator mapping undocumented, inferred from the domain. Manus's local Browser Operator does not sign |
| Hyperbrowser | https://www.hyperbrowser.ai and https://api.hyperbrowser.ai (same directory) | Intermediary | No |
| Cursor-hosted agent | https://cursorusercontent.com | Direct | No: an Anysphere (Cursor) domain. Named by its domain only, not by which product or customer signed |
Keys are found the way the Signature-Agent header says (draft-ietf-webbotauth-httpsig-protocol-00): type=directory (the default) reads the operator's well-known key directory; type=jwks_uri (a JWK Set URL) and type=cimd (an OAuth Client ID Metadata Document, whose client_id must equal its own URL and which carries jwks or one jwks_uri) are accepted only for a reviewed operator whose entry names that exact URL. Anything else is unreviewed-operator and is never fetched; a redirect or any status other than 200 is directory-unavailable. None of the operators above publishes such a URL yet.
Not listed: www.browserless.io (its directory has no keys), www.meta.com (Meta does not document what it signs for) and search.bot.kernel.sh (Kernel's search crawler, a separate identity). Amazon Bedrock AgentCore Browser is reviewed but not yet listed (2026-11-03): AWS documents that it signs with Web Bot Auth (preview, turned on per browser), but does not publish the Signature-Agent origin, so there is no key directory to check. A Cloudflare customer still sees it through Cloudflare's own signed-agent result (Edge evidence). Google-Agent was rechecked the same day against Google's page (updated 2026-10-13, still experimental) and its directory, and stays listed. Only tag="web-bot-auth" signatures are verified. Visa's Trusted Agent Protocol tags (agent-browser-auth, agent-payer-auth) are not accepted: TAP keys come from Visa's key store by key ID rather than a Signature-Agent directory, and TAP uses rsa-pss-sha256 with a required nonce and an 8-minute window, which this Telltale server does not yet implement.
Page markers
Claude in Chrome (active overlays only; since 2026-08-12 its side panel is Claude Cowork, so a sighting is shown as Claude in Chrome, possibly a Cowork session), Perplexity Comet, OpenClaw and the browser-use framework. Two more frameworks are named from their own source (names used as facts, notices in licenses/): Stagehand, from its opt-in cursor overlays and the style tag it injects while taking a screenshot (browserbase/stagehand, MIT), and agent-browser, from the attribute its snapshot sets on elements for a moment (vercel-labs/agent-browser, Apache-2.0). Both name a tool, never an operator or a model. Claude's idle static-UI buttons and its page-world helper globals are reported separately as diagnostics (tooling present, not control) and never name anyone, because no independent public source corroborates them; its residual animation style tag is reported apart as well. Two third-party observed heuristics from botscent (MIT) are also weak markers: ChatGPT for Chrome (a data-codex-favicon-badge attribute on an element in the page head, or in a favicon link's href; the badge can outlive the task) and the Codex in-app browser (at least two of three shell signs: a 29-character anonymous native window.prompt, an empty keyboard layout map in Chromium, and a top-level shadow-root overlay with an unrecognised id at the maximum z-index; one sign alone never names it). Neither is documented by OpenAI.
More frameworks are named from their own source (added 2026-10-28; names used as facts): Eko (framework) from its highlight container and highlight attribute (FellouAI/eko, MIT; named as the framework, never as the Fellou browser), Skyvern from the window property it sets by default (Skyvern-AI/skyvern; the name only), AgentQL from the element attribute its SDK stamps (agentql, MIT) and Magnitude from its injection flag (magnitudedev/browser-agent, Apache-2.0). agent-browser is also named from its annotation layer and recording cursor, and Stagehand from its mask attributes. The legacy browser-use highlighter that many forks still copy is named browser-use-derived DOM highlighter: it names a code lineage, not a product. Each names a tool, never an operator or a model, and only beside an agent verdict. Generic signs such as a nodriver pointer flash or a Playwright screenshot caret flash never name anyone.
Two joint rules from botscent (MIT, reimplemented) name Muse and Instinct as Unconfirmed third-party observations, beside an agent verdict only. The page sends only the combined outcome (match or no-match), never the parts, and a single part never names anything. Muse is never named from Meta's www.meta.com signing directory, whose purpose Meta does not document.
Runs from a cloud computer. When the verdict is already agent, the connection comes from a published data-centre range, and the browser shows two or more cloud-computer signs or a software renderer, the attribution also carries cloudRun ("Runs from a cloud computer", Unconfirmed). It describes where the agent runs; it never names an agent, a product or a model, and it is never verdict evidence.
Declared user agents
Some agents name themselves in their user agent. A declaration is a claim anyone can copy or remove, so it is attribution only: it never changes the verdict, never selects a usage category and never appears as a named usage row. It names the controller (strength declared) only beside an agent verdict; otherwise the session and verify responses carry it as declaredAgent, a labelled diagnostic. The user-agent string is never stored.
| Token | Named as | Source | Source strength |
|---|---|---|---|
| Agent-NovaAct/ | Amazon Nova Act | aws/nova-act README ("look for NovaAct in the user agent string"); default suffix in its source (Apache-2.0), appended only when no custom user agent is set | medium: vendor-documented, but absent with a custom user agent |
Cursor/<version> in an Electron user agent | Cursor in-app browser | measured by a third party (botscent, Cursor 3.23.12): the in-app browser keeps Electron's default user agent; Anysphere documents nothing | low: names a browser surface inside the IDE, not which agent or model drives it |
A GrokAgent/ token has been reported by a single press source; it is not used, because it is unconfirmed and its suffix is an account-derived identifier. Manus-User and Devin/ appear only in third-party lists and are not used.
These agents have no public signal (checked 2026-10-25; entries revised 2026-10-26 or added 2026-10-28 are marked in the API), so their sessions stay unnamed agents (unattributed-automation) when the evidence shows an agent. Each is reserved and will be added only when a first-party marker or signing directory can be verified.
| Agent | Finding | Sources checked |
|---|---|---|
| Gemini Spark | Since 2026-07-30 it can drive the user's own desktop Chrome; Google documents no signature, user-agent token or page marker for it, and gemini.google.com and www.google.com have no directory (404). Google-Agent is not said to be Spark (checked 2026-10-28) | blog.google Gemini Spark update (July 2026); 9to5google (2026-07-30); support.google.com/gemini/answer/17094507; gemini.google.com and www.google.com directory paths |
| OpenAI Dots | Always-on agents with their own cloud machine and browser (DevDay, 2026-09-29); no documented signature, user-agent token or IP list; openai.com has no directory (404); chatgpt-agents.json is Codex cloud egress, not Dots (checked 2026-10-28) | infoq.com DevDay 2026 report; androidheadlines.com (2026-09); developers.openai.com/api/docs/bots; developers.openai.com/api/docs/guides/ip-addresses; openai.com directory path |
| Gemini in Chrome | No directory on gemini.google.com (404); its overlay is a chrome:// WebUI page, not a page-visible marker; Google-Agent's directory does not name Gemini | gemini.google.com/.well-known/http-message-signatures-directory; developers.google.com/crawling/docs/crawlers-fetchers/web-bot-auth; chromium/src ce6c06cc chrome/browser/resources/actor_overlay |
| Microsoft Copilot (Copilot Mode in Edge) | copilot.microsoft.com redirects to www.bing.com, which has no directory (404); no first-party marker or signing documentation | copilot.microsoft.com and www.bing.com directory paths |
| ChatGPT Atlas agent mode | Retired 2026-08-09; no OpenAI document says it signed requests | help.openai.com/en/articles/12628199; help.openai.com/en/articles/6825453-chatgpt-release-notes |
| OpenAI Codex coding agent | No documented signature or marker; no directory on openai.com (404). The Codex in-app browser and ChatGPT for Chrome now have third-party observed heuristics and are named separately as Unconfirmed (weak-marker) (revised 2026-10-26) | developers.openai.com/codex/changelog; openai.com directory path; botscent page rules (commit 8c1fb53) |
| Cursor browser tool | A web view inside the IDE; cursor.com answers the directory path with an HTML page, not a key directory. Its Cursor/<version> user-agent token is recorded only as the self-declared surface "Cursor in-app browser" (revised 2026-10-28) | cursor.com/docs/agent/browser; cursor.com directory path; botscent page rules (commit 8c1fb53) |
| Grok (xAI) | No directory on grok.com or x.ai (404); no first-party documentation. Only unconfirmed third-party reports exist, and none is used: a GrokAgent/ token from one press report, and a third-party catalog's mapping of the cursorusercontent.com signer, which is an Anysphere (Cursor) domain named only as a Cursor-hosted agent (revised 2026-10-26) | grok.com and x.ai directory paths; runtimewire.com (2026-08-12); cursorusercontent.com directory path |
| Claude Cowork | In Chrome, Cowork runs through the Claude in Chrome extension, so it is shown only as "Claude in Chrome, possibly a Cowork session". Its built-in desktop browser documents no signature, user agent or marker; no directory on claude.ai or claude.com (404) (revised 2026-10-26) | claude.com/blog/cowork-chrome-side-panel; claude.com/blog/cowork-built-in-browser; claude.ai and claude.com directory paths |
| Opera Neon | No documented signature, user-agent token or marker; no directory on opera.com, neon.opera.com or operaneon.com (checked 2026-10-26) | those directory paths; 2025 launch press |
| Dia (The Browser Company) | No documented agent identity, signature or marker; no directory on www.diabrowser.com (checked 2026-10-26) | www.diabrowser.com directory path |
| Genspark AI Browser | Runs locally; no documented signature, user-agent token or marker; no directory on www.genspark.ai (checked 2026-10-26) | www.genspark.ai directory path; genspark.ai/blog/genspark-ai-browser |
Muse and Instinct left this table on 2026-10-28: the joint rules above now name them, Unconfirmed. Neither vendor documents a signature or marker, and instinct.ai has no directory (404).
Verified crawlers and fetchers
Some crawlers and user-triggered fetchers name themselves in their user agent and their operators publish the IP ranges they use. The Telltale server recognises these user agents and checks the request IP against the operator's own JSON list: CF-Connecting-IP when the request came through the public edge listener, the socket address otherwise (a CF-Connecting-IP header sent to the loopback listener is ignored).
| Name | Operator | Kind | Published list (fetched and checked 2026-10-25) |
|---|---|---|---|
| GPTBot | OpenAI | crawler | https://openai.com/gptbot.json |
| OAI-SearchBot | OpenAI | crawler | https://openai.com/searchbot.json |
| OAI-AdsBot (added 2026-10-28) | OpenAI | crawler (checks the landing pages of ChatGPT ads) | https://openai.com/adsbot.json |
| ChatGPT-User | OpenAI | user-triggered fetcher | https://openai.com/chatgpt-user.json |
| ClaudeBot, Claude-SearchBot (added 2026-10-28), Claude-User | Anthropic | crawler, crawler, user-triggered fetcher | https://claude.com/crawling/bots.json (one list for Anthropic's bots) |
| PerplexityBot | Perplexity | crawler | https://www.perplexity.ai/perplexitybot.json |
| Perplexity-User | Perplexity | user-triggered fetcher | https://www.perplexity.ai/perplexity-user.json (Perplexity's docs now cite www.perplexity.com, which redirects; the .ai URL is fetched because redirects are never followed) |
| Googlebot | crawler | https://developers.google.com/static/crawling/ipranges/common-crawlers.json (the older googlebot.json URL now redirects there) | |
| Google-Agent | agent hosted on Google infrastructure | https://developers.google.com/static/crawling/ipranges/user-triggered-agents.json | |
| Google-GeminiNotebook (added 2026-10-28; the former Google-NotebookLM token also matches) | user-triggered fetcher | https://developers.google.com/static/crawling/ipranges/user-triggered-fetchers-google.json | |
| Bingbot | Microsoft | crawler | https://www.bing.com/toolbox/bingbot.json |
OpenAI's bot documentation moved to developers.openai.com/api/docs/bots.
- A matching user agent from an IP inside the list is status
verified("Checked"). When the operator's list was freshly downloaded and the IP is outside it (and reverse DNS, where the vendor offers it, did not confirm either), the status isdeclared-contradicted("Claim didn't check out"): something may be borrowing the name. When the list could not be used (not downloaded yet, failing, or older than two days), or no address was available, it isdeclared-unverified("Couldn't check"). A claim that didn't check out is a diagnostic only: it is not evidence of a person, never changes the verdict, and never names anyone. A user agent alone never yields verified, a usage category or a named controller. - Lists in the JAFAR format (draft-illyes-webbotauth-jafar, media type
application/jafar+json) may list services per prefix. Then a claim is confirmed only when its own name is listed on the longest prefix containing the IP; a prefix without services covers every service. - Where the vendor recommends it, forward-confirmed reverse DNS also verifies: Googlebot (the PTR name must end in googlebot.com) and Bingbot (search.msn.com), and the name must resolve back to the same IP. Google-Agent is verified by its own list only: its google-proxy-<address>.google.com PTRs are shared by every Google-owned user-triggered fetcher (IPs Google lists for other fetchers confirm there too), so a PTR match cannot tell Google-Agent apart. Lookups never hold up a request: the first request from an IP starts a background lookup (1.5-second timeout, bounded concurrency) and later requests use the cached result (six hours when confirmed, ten minutes for a mismatch, 45 seconds after a DNS error or timeout). verifiedBy.method says whether ip-range or reverse-dns verified, and verifiedBy.reverseDns carries the lookup status, never the hostname. Set TELLTALE_CRAWLER_RDNS=off to disable it.
- Lists are downloaded when the Telltale server starts and refreshed in the background, so a request never waits for one: a declared crawler that arrives before its list is answered declared-unverified. Lists are fetched only from these exact HTTPS URLs, never following a redirect, with a 5-second timeout and a 1 MiB, 20,000-prefix bound. A good list is reused for a day; a failed refresh is retried after an hour and a list older than two days is no longer used. Any failure fails closed: the claim stays declared-unverified.
- The session keeps only labels (name, operator, kind, status, the list URL and when it was checked), never the user agent string or the IP address.
- Verification never changes the verdict and never names a browser controller. A verified crawler or fetcher selects the crawler usage category (Google-Agent selects browser use) and appears in usage-by-agent rows at ip-verified strength. Google-Agent is named as Google publishes it; the list does not say which Google product (such as Gemini) was driving.
- The dashboard's admin state lists each list's fetch time, prefix count and last error as crawlerIpRanges. Set TELLTALE_CRAWLER_IP_LISTS=off to disable fetching.
Network and timing diagnostics
The Telltale server derives a few labels and counts itself, at ingestion. They are stored on the session as networkSignals and timingSignals, appear in the session JSON and under Technical details as "Not used for the verdict", and never change a verdict, score, attribution or policy decision.
Connection origin. At SDK init and with each evidence batch, the client address the Telltale server already sees (the socket address; CF-Connecting-IP on the public edge listener; the TRUST_PROXY hop of X-Forwarded-For when TRUST_PROXY is set) is matched against published provider IP ranges. The session keeps only:
- a label:
ai-agent-provider,tor-exit,data-centre,consumer-or-other(no list matched) orunknown; - the provider name (for example Amazon Web Services or OpenAI), the provider's own service label when its list names one, and the matching list's ID, URL and version;
- whether the address is public or private (loopback, RFC 1918, link-local, CGNAT, unique-local), and up to five distinct labels seen during the session;
- a compact view,
network: {network, provider}:networkisdatacenter(a cloud, hosting or AI-agent-provider range),relay(iCloud Private Relay or Tor) orresidential-or-unknown(nothing matched, or the lists were unavailable), andprovidera short ID such asaws,gcporapple(null forresidential-or-unknown). The session detail shows it as, for example, "Connected from a cloud provider (AWS)".
The IP address itself is never stored, logged, hashed or returned. Lists (checked October 26, 2026): AWS ip-ranges.amazonaws.com/ip-ranges.json, Google Cloud gstatic.com/ipranges/cloud.json, Oracle Cloud public_ip_ranges.json, the DigitalOcean and Akamai (Linode) geofeeds, Cloudflare ips-v4/ips-v6, the Tor Project's bulk exit list, and the agent egress lists published by OpenAI (ChatGPT-User), Anthropic, Perplexity (Perplexity-User) and Google (user-triggered agents). Azure, Hetzner, OVH, Vultr and Cloudflare WARP egress are not included: none publishes a stable machine-readable list. Since 2026-11-03 Apple's iCloud Private Relay egress list (mask-api.icloud.com/egress-ip-ranges.csv) is included as relay: real people use Private Relay heavily, so it is never labelled a data centre. When several lists match, an AI agent provider wins over a relay, a relay over Tor, and Tor over a data centre. A compact snapshot of every list but Tor ships with the server (refreshed with each release) and labels addresses from the first request until each live download is fresh; a snapshot entry is used for at most 60 days. Sources and terms: licenses/network-ranges.md.
Two service egress lists were added on 2026-10-28. Each names only the operator's egress for one service, never a product, an agent or a model:
- Anthropic, agentic-browser-use: Anthropic's SAIL file at
https://claude.com/.well-known/sail(the IETF draft draft-kelley-webbotauth-sail, a small TOML file). Its one service (named agentic-browser-use until 2026-11-03; renamed since, so the agentic-browser-use prefix set is read whatever the service is called, and the label stays agentic-browser-use) lists a handful of Google Cloud addresses that are not in Anthropic's bot list. Anthropic does not document which product sends from them, so it is never shown as Claude in Chrome, Claude Cowork or a model. The file is read with a 512 KiB limit, never through a redirect, and refreshed after its own max-age (at least an hour); Anthropic's file omits the draft'sschemakey, which is accepted and shown in the list status. - OpenAI, Codex cloud egress:
https://openai.com/chatgpt-agents.json, which OpenAI's IP egress page lists for connections from Codex cloud. Despite the file name it is not ChatGPT agent (ChatGPT agent signs its requests as https://chatgpt.com).
Like the other agent-provider lists, a match keeps a person-looking session at Unknown ("not called human") and never makes it agent.
Lists download in the background at startup and are refreshed daily (Tor every six hours); a request never waits for or starts a download. A list older than three days (Tor: one day) is ignored. If any list is missing or stale, an address that matches nothing is unknown, never consumer-or-other. A data-centre, Tor or agent-provider label is not proof of automation: VPNs, corporate proxies and cloud desktops use these networks. The admin endpoint /api/admin/network-origin-lists shows each list's fetch time, version, prefix count and last error. Set TELLTALE_NETWORK_RANGES=off to disable fetching.
Transport. What the listener that received the request can actually measure:
- Loopback listener, direct: plain HTTP, so the HTTP version is the client's own (
1.1) and there is no TLS handshake; JA4 is "not available". - Behind a reverse proxy (TRUST_PROXY): the connection seen is the proxy's; TLS, HTTP version and JA4 are "not available".
- Public edge listener: TLS ends at Cloudflare and cloudflared speaks HTTP/1.1 to the Telltale server, so the HTTP version and ALPN are not available. JA4 is recorded only when a Cloudflare rule writes it into a header named by
TELLTALE_EDGE_JA4_HEADER(the rule must overwrite any client-sent value); a value that is not a JA4 string is ignored.
The standalone network collector (npm run network:collector) still measures JA4, ALPN and HTTP/2 settings on its own TLS port for the lab page only; it is not joined to ordinary sessions.
Action-to-input timing. Where request capture is on, the SDK notes in page memory when the last trusted pointerdown or keydown happened (never the key, target or position). Each captured request shape and each WebMCP tool call start carries the whole milliseconds since then, or null when there was none. The Telltale server keeps only a count per bucket: under 300 ms, under 2 s, under 10 s, over 10 s, none.
Page rhythm. From the evidence events the SDK already sends (navigation, trusted click and keydown, on the page's own clock): time from page start to its first trusted input (under 1 s, 3 s, 10 s, 30 s, over 30 s, none), dwell per page (under 5 s, 30 s, 2 min, 10 min, over 10 min), the number of pages and navigation cadence in pages per minute (rounded to one decimal and bucketed: single page, under 1, 1 to 3, 3 to 10, over 10).
Cost: under 1 KB per session (about 440 bytes of origin and transport, 290 of timing counts and about 130 of open-page bookkeeping, up to 20 open pages), and one extra integer field on each request-shape and tool-call upload. Erasing a subject deletes their sessions and these fields with them.
Optional action discovery
When a workspace owner turns discovery on for a Site, the SDK also observes the structure of same-origin JSON requests: methods, path shapes, query and property names and types, media types and statuses. It does not keep values, cookies, authorization headers or raw responses. Structures become proposed agent tools and workflows that stay disabled until someone reviews and enables them. Approval is rechecked before every call, schema drift disables a tool, writes need confirmation, and your application's own backend authorization still applies to every request a tool makes.
Words we use
The dashboard, the demo and these guides use one plain word per idea. The API keeps its own names, shown here in code.
| In the dashboard | API name | What it means |
|---|---|---|
| Human, AI agent, Unknown | verdict: human, agent, unknown | A category, not a probability. Unknown is not counted as human. |
| Agent name | attribution.controller | Which agent product, when the evidence names one. |
| How we know | attribution.strength | How the name was established. Always shown beside the name. |
| Verified | signed (or ip-verified for crawlers) | The operator signed its requests, or the request came from an address the operator publishes. It names the operator, not the person or the AI model, and authorizes nothing. |
| Unconfirmed | weak-marker | Named from something the agent's tool leaves on the page (a page marker). Anyone can rename or fake it. |
| Self-reported | declared | A browser agent named itself in its user agent. Anyone can send or remove it. |
| Declared | client.name on MCP and API calls | The name an MCP or API client gives for itself (for example Claude Code). What the software says it is, never a verified identity. |
| Not named / Unnamed agent | unattributed-automation | Automation is driving, but nothing reliable says which agent or AI product. |
| No recent activity, Outdated | stale | Nothing arrived within the 60-second evidence window, or the newest activity is older than the policy's limit. |
| Technical note | diagnostic | A detail that never changes the verdict. |
| From your server / From the page (unconfirmed) | server-associated (/v1/identify) / browser-supplied (identify()) | Who the session belongs to, and whether your backend or the page said so. |
| Activity minutes | interaction minutes | Minutes in which a click, key, scroll or request arrived. Not elapsed, worked or chargeable time. |
| Mixed minutes | mixed | Minutes in which activity with more than one verdict arrived for the same account (for example Human and AI agent, or Human and Unknown). Counted once, apart from human, AI agent and unknown minutes. |
| Hold for review, held for review | review, held action | A person decides. Approving still needs your backend's own authorization. |
| Paused until consent | waitForConsent (held) | Nothing is collected until your consent manager calls optIn(). |
| Changed since approval | drift | A tool's request format changed since you approved it, so it is off until reviewed again. |
| Telltale server | collector | The Telltale server your SDK and backend talk to (endpoint in configuration). |
Where the pieces live
- Browser SDK: /sdk/v1.js on your Telltale server (Browser SDK guide); an immutable hashed URL with Subresource Integrity is listed in the Content Security Policy guide.
- Server API: /openapi.json, the API reference pages and the API explorer.
- Installation paths: hosted tag, Google Tag Manager, Cloudflare Zaraz or your own code.
Continue with the Quickstart.