Agent traffic
People are starting to tell an AI assistant "book me a plumber for Thursday", and the assistant goes to a website and tries. This is the reference for the instrument that counts those visits: the four verdicts, the five families of evidence behind them, and every field the tracker refuses to look at.
The tracker exists to answer one question nobody has an honest number for: how much AI-assistant traffic does an ordinary service business actually get, and how often does the assistant try something and fail?
It observes and classifies. It never executes an action, never writes to the host site, and has no code path to either. There is no Action Graph in it, no endpoint for an agent to call, and no way for it to touch a form.
Four verdicts
| Verdict | Meaning |
|---|---|
human | A person, on the site, behaving like a person. This is the default. |
agent | Software acting on the site — declared by its user-agent, presenting a signed identity, or strongly fingerprinted. |
suspected-agent | Real but ambiguous evidence. Deliberately not rounded up to agent. |
bot-other | A bot that is not an agent: search crawlers, SEO tools, monitors, and AI training crawlers. |
The classifier is a pure, synchronous, dependency-free function that runs identically in the browser, in the collector and in the tests. The verdict that matters for the measurement is agent — an assistant acting for a person. It is line 2 of the report, never line 1.
Five families of evidence
| Family | What fires | Verdict |
|---|---|---|
| 1. Known agent user-agents | ChatGPT-User, Claude-User, Perplexity-User, Operator, NotebookLM — an assistant acting for a user. | agent, confidence 0.90 |
| 1b. AI crawlers | GPTBot, ClaudeBot, PerplexityBot, CCBot, Bytespider, Applebot-Extended, Google-Extended, Amazonbot, meta-externalagent. | bot-other, platform recorded |
| 2. Web Bot Auth | The Signature, Signature-Input and Signature-Agent headers — a signed agent identity. | agent, confidence 0.95–0.97 |
| 3. AI referrers | chatgpt.com, perplexity.ai, claude.ai, gemini.google.com, copilot.microsoft.com. | Stays human, recorded as its own signal |
| 4. Headless and browser-agent fingerprints | A headless user-agent, navigator.webdriver, automation globals, a permissions-surface mismatch, an empty language list, no plugins on desktop Chrome, a mobile user-agent at a desktop viewport, no pointer, scroll or key events, and programmatic or inhumanly fast form fills. | agent at two or more signals, otherwise suspected-agent |
| 5. Failed form interactions | Which form, which field, and how it died: validation-rejected, captcha-blocked, no-submit, abandoned. | Contributes nothing to the verdict — captured as evidence |
The fingerprint weights, and why no single one is enough
| Signal | Weight |
|---|---|
| Headless user-agent (server-side, trustworthy) | 0.70 |
navigator.webdriver | 0.60 |
| Automation globals present | 0.60 |
| Inhuman fill speed | 0.40 |
| No pointer, scroll or key events | 0.35 |
| Fields set without keystrokes | 0.35 |
| Zero languages declared | 0.30 |
| Mobile user-agent at a desktop viewport | 0.30 |
| No plugins on desktop Chrome | 0.20 |
| Permissions-surface anomaly | 0.20 |
agent threshold is 1.00 and requires at least two independent signals; suspected-agent is 0.45. The largest single weight is 0.70, so no single self-reported signal can reach agent — an automation global on its own lands on suspected-agent, and that is intended.Three ways this could quietly lie, and what stops each
- Counting AI crawlers as agents. Training and indexing crawlers visit nearly everything; if they counted, every site would show agent traffic on day one and the measurement would return a guaranteed false positive. They are classified
bot-otherwith the platform recorded, and the report shows them on their own line. That split was put to a decision and confirmed: the headline number counts only assistants acting for a user. - Counting AI referrals as agents. An AI referrer means a person followed an AI answer here. Commercially interesting, and not an agent acting on the site. A referrer never moves the verdict off
human. - Counting failed forms as agent evidence. People fail forms constantly. Using failure as evidence of automation would manufacture exactly the false positives that would corrupt the measurement. A failure only becomes a failed agent action once the verdict independently says agent or suspected-agent.
What it collects
- The site’s own domain name, and a reduced page path. Only the first segment of the URL is kept, and only when it does not look like an identifier. The query string is never seen, so nothing after a
?is ever collected. - Which site sent the visitor, as a domain only.
- The browser’s user-agent string, as every web server already receives on every request.
- Signals that distinguish a person from software: whether the browser reports itself as automated, how many languages and plugins it declares, the window size, counts of mouse, scroll and keyboard events, and whether the browser’s own notification-permission settings are self-consistent. None of these identify a person; they describe the software.
- If a form is used: the names and types of the fields, how long each took to fill, how many keystrokes were involved, and whether the form succeeded, was rejected by validation, hit a CAPTCHA, or was abandoned. Where a field was rejected, one word from a fixed list of nine —
required,email,number,format,length,range,select,url,other— never the wording of the message.
What it never collects
- No cookies. Nothing is stored on the visitor’s device — no cookie, no
localStorage, nosessionStorage, nothing. - No identifier. No visitor ID, no fingerprint hash, no cross-site tracking. Nothing is created or stored whose job is to name a visit or tie it to another one.
- No IP addresses. Not logged, not stored.
- No form contents, ever. The names typed in, the phone numbers, the email addresses, the message text — none of it is read, stored or transmitted. The only question the tracker ever asks about a text field is whether it is empty.
- No password, hidden or file fields at all. These are not observed even as shapes. A keystroke count on a password field approximates the length of somebody’s password, so the field is excluded at the single gate every field observation passes through — and the collector drops any such record a forged beacon tries to supply.
- No tick-box answers. For checkboxes and radio buttons the ticked state is the answer — whether somebody consented to marketing, whether they have private health cover — so the tracker does not read it. It records that the box was interacted with and when, never which way it was set. The collector enforces this a second time.
- No error-message wording. A website can put anything into its own validation messages, including a customer’s name, so the message never leaves the page.
What the path reduction does and does not hide
Only the first URL segment survives, and only when it is not identifier-shaped: no run of four or more digits, no @, no more than 40 characters. So /patients/jane-smith-0412345678/appointment is recorded as /patients/*, and /0412345678 as /*.
The first segment can still be sensitive as a category — /patients, /invoices, /accounts are kept deliberately, because knowing that agents fail somewhere under /booking is the entire point of the measurement. And it costs detail on purpose: we can see that agents visited and failed under /booking, not which booking page. The form’s own name is recorded separately, and that is the identifier that actually matters when replaying a failed action.
The reduction runs in the browser, so the full path normally never leaves the visitor’s device, and it runs again in the collector, because a beacon is a string a stranger can POST.
Do Not Track and Global Privacy Control are honoured
If the browser sets navigator.globalPrivacyControl or reports doNotTrack === "1", the snippet does nothing at all: no listeners, no beacon, no request. Only doNotTrack === "1" is matched — not the legacy spellings, and not window.doNotTrack.
Two consequences, both recorded rather than hidden. It undercounts human page views, because agents do not set these signals, so the agent count is unaffected and the agent share is biased slightly upward — the only bias in this package that points that way, which is why it is written down twice. And an automated browser can suppress its own beacon by asserting the signal, a real evasion route accepted in exchange for honouring it, which pushes toward under-counting agent traffic.
Retention: 60 days, enforced by the store
Events older than 60 days are deleted, on by default, with no configuration required. That used to be a promise a person kept — the file was append-only with no expiry — and it was the only claim in the client-forwardable summary that the code did not enforce, which matters because an agency repeats that claim to its client on our behalf.
Nothing survives expiry: the row is deleted outright, with no aggregate, no counter and no rollup. That is what makes "then deleted" literally true rather than approximately true. It costs something, and the cost is stated: a report cannot show a trend older than the retention window.
Deletion is irreversible and this data cannot be re-collected, so the prune is conservative. A row whose timestamp will not parse is kept rather than guessed at; an unreadable or missing file is left alone; survivors are written to a temp file and renamed over the original, so an interrupted prune cannot leave a half-written store; and a retention window that is not a whole number of days of at least one is refused at construction, so a typo or a bad environment variable cannot empty the store. Turning retention off has to be typed out explicitly — "off" should never be somewhere you arrive by leaving a flag blank.
The collector treats every beacon as hostile
A beacon body is a string a stranger POSTed. Anyone can send anything, from a fuzzer to a competitor trying to make the numbers say whatever they want. So the collector assumes exactly that:
- Nothing from the body is trusted. Only known keys are copied; every string is length-clamped and stripped of control characters; every number must be finite and is clamped; every enumerated value must be a member of its list or it is coerced to a safe default.
- The classification is computed server-side from the real request headers. A verdict, user-agent, platform or signal list present in the body is ignored outright — a client cannot vote on its own classification.
- Site attribution prefers the
OriginorRefererheader over the body’s self-reported site, so a beacon cannot credit its traffic to somebody else’s domain. - Prototype-pollution keys are never read, so a
__proto__payload has nothing to reach. - The response is always
204, whatever the collector decides. It is seen by a stranger’s browser on somebody else’s site, so it says nothing and never fails visibly.
| Limit | Value |
|---|---|
| Body size | 16 KB |
| Forms per beacon | 5 |
| Fields per form | 40 |
| Automation globals recorded | 8 |
| Site string | 120 chars |
| Path string | 200 chars |
| Field name | 64 chars |
| Field type | 24 chars |
| User-agent | 256 chars |
| Any duration | 86,400,000 ms |
The beacon, on the wire
{ v:1, site, path, ref, cs:{ wd webdriver · ag automationGlobals · pc pluginCount ·
lc languageCount · vw/vh viewport · tp maxTouchPoints · pe pointerEvents ·
se scrollEvents · ke keyEvents · ms timeOnPage · pa permissionsAnomaly · fm forms } }
form: { n name · fc fieldCount · fl fields · tt totalFillMs · oc outcome · ff failedField · ft failureCode }
field: { n name · t type · ms fillMs · k keystrokes · fi filled }packages/tracker/README.md. Short keys because bytes are the constraint; the collector expands them into the descriptive stored event.The one table that goes stale
The list of known agent user-agents is the part of this package that ages. New assistant products ship constantly, and adding one is a single-line edit with nothing else in the file changing:
Adding an agent
{ pattern: /NewAgent-User\//i, platform: "Vendor", product: "NewAgent-User", uaClass: "assistant" },packages/tracker/README.md, "Monthly maintenance — the UA table".uaClass is the load-bearing field: assistant means acting for a user and counts as agent traffic; ai-crawler means training or indexing and does not. The table records where each entry came from.
The list was last reviewed on 25 August 2026 and the next review is due 25 September 2026. It was compiled by a model whose training data ends in May 2026, so anything that shipped after that is missing until a human re-checks the vendor pages named in the file. A stale table undercounts, which biases toward a false negative — the same direction as every other error in this package.
What this instrument has measured so far
0
sites are running the tracker. The collector is live and answering, and the 60-day measurement clock has not started, because the clock starts on the first installed site.
Running it
Inside the repository
npm install
npm test # 108 assertions incl. the byte budget
npm run snippet -- --endpoint https://<host>/b --out dist/t.js # --endpoint is REQUIRED
npm run snippet -- --endpoint https://<host>/b --readable # readable build, to show an agency
npm run collector -- --port 8787 --store data/events.ndjson # 60-day retention, on by default
npm run collector -- --port 8787 --store data/events.ndjson --retention-days off # opt out, explicitly
npm run report -- --store data/events.ndjson --weeks 4 --out report.md
npm run report -- --store data/events.ndjson --json # same summary as JSONpackages/tracker/README.md, "Run it". The assertion count printed there is 108; the suite currently reports 118 checks with the snippet at 5,689 of its 5,800-byte budget. The README line is the one that has not been refreshed, not the number.Written from
- The classifier
packages/tracker/src/classify.ts - The collector, its threat posture and its clamps
packages/tracker/src/collect.ts - The event types and the privacy boundary
packages/tracker/src/types.ts - Retention
packages/tracker/src/storage.ts - The snippet and its byte budget
packages/tracker/src/snippet.ts - The package explainer, written to be forwarded to a client
packages/tracker/README.md - What counts as agent traffic
issues/closed/decision-006-what-counts-as-agent-traffic.md