These are the codes the beacon reports about the browser it is running in. A code is an observation, not a verdict: the scorer correlates them with the transport and behaviour planes and owns the final number.
The weight column here is only this plane’s contribution to the client sub-score. The rules that turn them into a verdict are on the scoring page, and the identifiers that land on a session are on identifiers and labels.
| Code | Weight | What it means |
|---|---|---|
automation_webdriver | 100 | The navigator webdriver flag is true |
automation_cdc | 100 | chromedriver’s marker property on window or document |
automation_playwright | 100 | A Playwright binding |
automation_puppeteer | 100 | A Puppeteer binding |
automation_selenium | 100 | A Selenium, webdriver, driver or fxdriver binding |
automation_nightmare | 100 | The Nightmare binding |
automation_phantom | 100 | A PhantomJS binding |
automation_domauto | 100 | A DOM automation controller |
Hiding these is a one-line patch that every serious scraper applies. They are cheap, they catch the lazy majority, and they are the reason the other planes exist.
Figure 1 — five families of observation, one scorer. The beacon reports raw measurements and the server does the comparing, because the server holds the authoritative User-Agent and a client cannot suppress a comparison it never performs.
| Code | Weight | What it means | Why the weight is what it is |
|---|---|---|---|
headless_renderer | 90 | WebGL renderer is a software rasteriser | A consumer desktop browser with no GPU is a container |
headless_notif_contradiction | 45 | Notification permission says denied while the permissions API says prompt | No real profile is in both states at once |
headless_no_window_chrome | 40 | No browser-specific window object under a Chrome User-Agent | Strong, but trivially faked |
headless_zero_outer | 40 | The outer window width or height is zero | Also legitimately zero in some cross-origin frames |
headless_no_languages | 25 | The languages list is missing or empty | Real browsers always populate it |
headless_no_plugins | 20 | Zero plugins under a desktop Chrome User-Agent | Modern Chrome exposes five PDF entries |
headless_no_concurrency | 15 | Hardware concurrency is zero or absent under a Chrome User-Agent | Some privacy-focused browsers clamp this value |
headless_screen_eq_avail | 10 | Available screen size exactly equals screen size on a desktop User-Agent | Also true of Linux kiosks and full-screen presentations |
headless_no_chrome_runtime | 10 | No runtime object under a Chrome User-Agent | Its presence on ordinary pages has changed across releases |
Only the strong four — the renderer, the missing window object, the notification contradiction and the zero outer size — plus any automation marker set the headless boolean. The weak ones each have a real population of genuine humans behind them, and a boolean that is wrong for real visitors is worse than no boolean.
| Code | Weight | What it means |
|---|---|---|
ua_older_engine | 85 | The User-Agent claims a version whose features the engine does not have |
ua_newer_engine | 85 | The engine has features that shipped after the claimed version |
ua_probe_<major> | 0 | Which probe caught it — diagnostic only |
A scraper can set any User-Agent string it likes, but it cannot retrofit a JavaScript engine. The probe table pairs a browser major version with a function that first shipped natively in it:
var UA_PROBES = [
63, 'Promise.prototype.finally',
69, 'Array.prototype.flat',
73, 'Object.fromEntries',
85, 'String.prototype.replaceAll',
93, 'Object.hasOwn',
98, 'structuredClone',
110, 'Array.prototype.toSorted',
122, 'Set.prototype.union'
];Given a User-Agent claiming version N: every entry at or below N minus two must be present, or the engine is older than claimed; every entry at or above N plus two must be absent, or the engine is newer than claimed. The grace of two majors means a browser mid-upgrade, an enterprise pin, or one of the browser’s own User-Agent reduction quirks is never flagged.
Adding a row as the browser advances
Roughly every ten releases, pick a feature that is shipped in a known version — look it up rather than guessing, because a wrong version number here manufactures false accusations against real people — that is reachable as a function by a dotted path, and that is not commonly polyfilled. Never remove old rows; they are what catches ancient engines.
The polyfill guard: the probe requires the function to report as native code, or a site loading a compatibility library would make an old engine look new and get its own visitors flagged. The iOS exclusion: every browser on iOS is one engine wearing another’s User-Agent, so their feature set has nothing to do with the version in the string and they are skipped outright. Flagging them would be a pure false positive on millions of real phones.
| Code | Weight | What it means |
|---|---|---|
platform_mismatch | 35 | The reported platform contradicts the operating system in the User-Agent |
touch_missing_mobile | 30 | A phone or tablet User-Agent on a device with no touch support |
screen_outer_impossible | 25 | The window is larger than the screen it sits on |
dpr_odd | 10 | The device pixel ratio is absent, zero or not a finite number |
tz_mismatch | — | The browser timezone disagrees with the one derived from the address |
tz_unknown | 5 | The browser gave no timezone at all |
The beacon reports the raw measurements and the server does the comparing. Three reasons, and byte count is the least of them: the server holds the authoritative User-Agent read off the connection, and half these checks compare something against the User-Agent, which is precisely the thing we do not trust; a client cannot suppress a comparison it never performs; and thresholds can be tuned server-side without asking every site to redeploy a script tag.
Two conservatism rules apply here. The reverse of the touch check is deliberately not performed, because a desktop User-Agent with touch would flag every touchscreen laptop. And the display checks only run when the payload actually reported a screen size — a truncated write or an older client sends zeroes, and “no data” must never be read as “a window with no size”.
| Code | Weight | What it means |
|---|---|---|
human_mouse_natural | −25 | Sampled pointer positions vary in a way a straight line cannot explain |
mouse_linear | 40 | Nine in ten sampled triples are exactly collinear — what an interpolating driver produces and a hand never does |
mouse_static | 25 | The pointer fired move events but never changed pixel |
no_interaction | 20 | Zero interactions across the whole session, added server-side because only the server knows the session ended |
no_scroll_tall_page | 10 | A page half again as tall as the viewport that was never scrolled |
beacon_forged | 90 | The payload claimed time that provably did not exist |
Mouse positions are sampled at most once per second, up to sixteen points, and the classifier stays silent below six samples: somebody who nudged the mouse twice is not evidence of anything. The coordinates never leave the browser — only the verdict does.
Throughout the beacon, a probe whose interface is missing, blocked or throwing records nothing at all. A false “this human is a bot” is far worse than a missed bot, and every ambiguous case in this codebase is resolved in that direction.