a Date's own text agrees with the Date's own offset
Check id date-tostring-coherence
What an ordinary browser yields
At every instant, the GMT±HHMM the string renders equals minus getTimezoneOffset() read off the same Date, and the string is exactly toDateString() + " " + toTimeString().
What a detector infers
A Date's text is not opaque. ECMA-262 defines toString() as toDateString() + " " + toTimeString(), fixes the weekday and month tables to English so they are never localized, and bakes the UTC offset into the result as GMT±HHMM. So a single Date object answers the timezone question through paths that do not consult each other: the numeric one (getTimezoneOffset), and the one nobody remembers to hook — the offset the engine renders into the string. PerimeterX collects the raw toString() output in the same pass as getTimezoneOffset() and Intl.resolvedOptions().timeZone, then differences them server-side for free. The check reads four instants spread across the current year plus now, and three details in it are load-bearing rather than incidental. It parses hours and minutes SEPARATELY and signs the whole quantity, because a shortcut that reads only the hours flags every user in India, Nepal, Chatham and Newfoundland. It pins the probes to the current year, because an epoch-0 date reintroduces sub-minute Local Mean Time — Asia/Kolkata before 1906 was +05:53:20, which renders as GMT+0553 against a whole-minute API and manufactures a false contradiction out of a correct browser. And it never predicts DST: both readings come off ONE Date at ONE instant, so a zone whose rules we would have got wrong is still self-consistent and still passes. What the check permanently refuses to touch is the parenthesised zone NAME and the IANA id. The name renders in the browser's own ICU locale, Antarctica/Troll's name is literally the string (GMT+02:00), and Asia/Kathmandu resolves to the alias Asia/Katmandu — asserting on any of that would be both a false-positive minefield and a hidden reference table.
How to resolve it
A timezone is one host setting rendered by several readers that do not check with each other. Patching Date.prototype.toString in JavaScript leaves getTimezoneOffset and Intl telling the truth. Set the zone at the layer they all derive from — the OS, the container, or the browser's own timezone override, which reconfigures ICU underneath every reader at once and is invisible to this check by design. If you run a timezone-changing extension, this is what it looks like from outside: your setup contradicts itself, which is not the same as you being a bot.
Read in the wild by
PerimeterX / HUMAN
Captures the engine's full Date() string — weekday, month, GMT offset and the localized zone name in one field — in the same payload as the numeric getTimezoneOffset and the Intl IANA zone.
PerimeterX collector main.min.js (iFood deployment, /root/re/PerimeterX_RE/stample/ifood/source/main.min.js line 7898) contains `t["Czt+cU1WeEM="] = hS[hQ(802)]()` where hQ_map.json line 804 gives "802": "Date" — Date called without `new`, which returns the full toString-format string. Live EV2 payloads (stample/ifood/sample/*/decoded_payload_2.json) decode "Czt+cU1WeEM=" to "Wed May 20 2026 15:52:00 GMT+0800 (中国标准时间)", travelling in the same payload as "OARNTn5iRnw=" = -480, assigned by `t[0]["OARNTn5iRnw="]=CD()` where `function CD(){try{return(new Date).getTimezoneOffset()}catch(t){return 9999}}`, and "UT0kdxRdI0Y=" = "Asia/Shanghai", assigned by `try{var n=hS[hQ(357)][hQ(358)]();t[hQ(359)]=n.resolvedOptions()[hQ(360)]}` with hQ 357="Intl", 358="DateTimeFormat", 359="UT0kdxRdI0Y=", 360="timeZone". main/EN/EV1_EV2_UNIFIED_REFERENCE.md §B "Browser Runtime" line 110 labels the field "Date.toString()". Corroborated on a second deployment: the same semantic under Grubhub key "UiJkKBRPYRo=" decodes to "Wed May 20 2026 16:02:20 GMT+0800 (中国标准时间)" in stample/grub/sample/1/decoded_payload_2.json.
CreepJS
CreepJS, an open-source fingerprinting research demo rather than a production detector, lists Date's text surface — toString, toDateString, toTimeString — alongside getTimezoneOffset in a single lie-test target array, checking each for signs of tampering (native-code toString signature, proxy traps, stray descriptors) as one entry in a uniform sweep across dozens of prototypes. It tests those methods for having been hooked; it does not difference the rendered GMT offset against getTimezoneOffset the way this check does.
creepjs, src/lies/index.ts, inside getPrototypeLies() (between the CSS2Properties and GPU entries): `searchLies(() => Date, { target: ['getDate', 'getDay', 'getFullYear', 'getHours', 'getMinutes', 'getMonth', 'getTime', 'getTimezoneOffset', 'setDate', 'setFullYear', 'setHours', 'setMilliseconds', 'setMonth', 'setSeconds', 'setTime', 'toDateString', 'toJSON', 'toLocaleDateString', 'toLocaleString', 'toLocaleTimeString', 'toString', 'toTimeString', 'valueOf'] })` — where searchLies delegates to queryLies, whose tampering tests include `['failed toString']: (!hasKnownToString(name)[scope.Function.prototype.toString.call(apiFunction)]`.
Every attribution traces to a published artifact. See the sources and their limits.
Nearby checks in Navigator identity
- a worker resolves the same IANA timezone as the main thread
worker-main-timezone-coherenceA browser has one host timezone, and the specification ties both the main-thread global and every worker global to it, so a real browser… - the keyboard layout maps the OEM keys the way one real Windows layout would
keyboard-layout-single-originA keyboard layout is not a set of independent key mappings — it is one artifact the operating system hands over whole. - navigator.appVersion is the user agent minus its leading token
appversion-vs-useragentnavigator.appVersion is not an independent fact. In Blink and WebKit it is computed from the User-Agent at read time — literally the UA with… - Trusted Types hands back the branded object its own type demands
trusted-types-value-invariantTrusted Types is a typed API, not a boolean feature flag, and that is the whole of the check. - the user agent on the wire and the one JavaScript reports describe the same browser
ua-wire-vs-navigatorYour user agent is asserted on two channels built by different layers: the network stack writes the header, and the renderer answers… - Origin-scoped storage and browsing state
origin-storage-historyCookies, storage, IndexedDB, and same-tab history reveal only this origin's state. - Profile age and cross-site history boundary
profile-age-boundaryPrivacy boundaries prevent reading global history or true profile age. - Account reputation boundary
account-reputation-boundaryAccount reputation needs authentication and server history, neither of which this public audit has.
Clearcote is a browser built for fingerprint coherence
It is a Chromium fork, maintained by the same people who wrote this reference. It ships as a compiled browser rather than as a stealth script injected into someone else's — which is a description of how it is built, and is not an argument about how it behaves on this check.
This audit takes no position on how Clearcote scores on Navigator identity checks, on this one, or anywhere else. It has no baseline corpus of other people's fingerprints to rank you against and no vendor scoreboard — nearly every check is self-referential, asking one browser the same question through two independent APIs and reporting whether both answers can be true at once. It runs identically on any browser, including ours. Run it on yours and read the result yourself.
See the other checks in Navigator identity — the family date-tostring-coherence belongs to.