Our browser fingerprint detectors list has 49 public pages on it, in five groups: 12 consistency checks, 11 live commercial gates, 10 leak tests, 8 uniqueness tests and 8 automation tests. A script reads the verdict on 40 of them. The other 9 print values and no verdict, so we keep them as reference pages and read them by eye.

All 49 rows are one TypeScript array, DETECTION_TESTS in src/shared/detection-tests.ts. The Check detectors menu on a profile row reads it. So do GET /detection-tests and the MCP tool list_detection_tests, and so does the batch runner we use before a release. With one list, the window, the API and our reports cannot disagree about what "tested" meant.

The browser fingerprint detectors list, in five groups

GroupCountDetectors
Consistency12Pixelscan, IPHey, BrowserScan, Whoer.net, Fingerprint-Scan, JA3/JA4 echo, BrowserLeaks JavaScript, Client Hints, WebGPU, Features, TLS and HTTP/2
Live gates11Cloudflare Bot Fight, Cloudflare Turnstile demo, reCAPTCHA v3 score, hCaptcha demo, DataDome demo, Akamai Bot Manager, F5 / Shape, Kasada, Imperva / Incapsula, HUMAN / PerimeterX, Qrator
Leaks10BrowserLeaks WebRTC, IP, DNS, Geolocation and Proxy, ipleak.net, DNS Leak Test (extended), diafygi WebRTC IPs, Cloudflare trace, a header echo
Uniqueness8CreepJS, AmIUnique, EFF Cover Your Tracks, FingerprintJS demo, deviceinfo.me, BrowserLeaks Canvas, WebGL and Fonts
Automation8bot.sannysoft.com, Rebrowser bot detector, Incolumitas BotOrNot, deviceandbrowserinfo, BrowserScan bot detection, CreepJS proxy detection, prototype tampering and worker realms

We never add the five groups into one score, because they fail for unrelated reasons. AmIUnique, in the uniqueness group, wants to know whether it has seen this device before. Rebrowser, in automation, looks for traces a driving program leaves, such as the DevTools Runtime domain switched on. BrowserLeaks WebRTC is a leak test: it gathers the addresses WebRTC can find and expects no public one except the proxy's exit. The eleven live gates have no score at all, since Akamai or Kasada either serves the page or puts a challenge in front of it. A profile can be green on all eight automation pages and red on every consistency page, and that would be two bugs with two different fixes.

The app shows Consistency first. Its blurb says these pages were built to catch anti-detect browsers specifically, and when one fails it names the contradiction: a timezone against the exit, or a GPU that does not belong with the screen. The values those pages compare are described in what a browser fingerprint is.

Every profile is drawn from one real computer, not from separate listsIn Mosaic: Machines that exist. GPU, screen, CPU and fonts from one real device. Free holds 3 profiles. Pro is $49 a month for any number.

How it works: Machines that exist

Automatic, reference, and the word we removed

verdict is the field that decides whether the runner owes a page a reader. Set to automatic, it says Pixelscan or IPHey draws an answer and run-detection-tests.mjs has a function to read it. Set to reference, it says there is no answer to read. deviceinfo.me is the plainest example. It lists sixty browser properties under headings, behind a consent dialog, and never says which one is wrong, so there is nothing for code to read.

A third value, manual, once covered two different things: "we have not written the reader yet" and "there is nothing to read". A tally of manual rows mixed work we owed with work that could never exist, so we gave the second its own word and then wrote the readers we owed. On 21 October 2026 the file has 40 automatic rows, 9 reference rows and no manual ones.

The nine reference pages are AmIUnique, deviceinfo.me, BrowserLeaks Fonts, Geolocation, Features, TLS and HTTP/2, the Turnstile demo and the hCaptcha demo. Each prints a hash or a table. The screenshot is the deliverable.

Rules the runner follows

The runner keeps four rules, and we broke each of them before we wrote it down.

  1. A test that did not run is never a pass. A site that serves nothing is unreachable. Pixelscan loading with no headline the reader can find is unread.
  2. Read the verdict the page draws, not a word in its prose. Pixelscan's green card says "No masking detected", which contains "masking detected".
  3. Run it more than once. IPHey gave an unmodified Google Chrome 100, 100, 70 and 0 on one afternoon, so rounds that disagree are reported as flaky.
  4. Write down the exit. Whoer and BrowserScan compare the IP's country with the timezone and language, so a run without the proxy grades a different pairing.

Each run also checks the catalogue against the runner. A row marked automatic with no reader behind it is printed as a fault in our list, under its id, not as a result about the profile. Before that check, docs/DETECTION.md printed "34 automatic" while three of the 34 had no code that could read them.

Rows we deleted

Third-party test pages move and retire. When one stops answering, we find out where the test went before concluding anything. Three examples:

  • fpscanner pointed at a page that had become its author's homepage. His projects page sends visitors to deviceandbrowserinfo and Fingerprint-Scan instead, and both were already on the list.
  • datadome-bots served the same homepage, with an HTTP 200. A 200 is never unreachable, and a challenge reader looking at a page with no challenge on it scores a pass. A retired demo would have reported a clean run on every profile, the kind of result nobody goes back to check, so we deleted the row.
  • Pixelscan moved its scan from the front page to /fingerprint-check. The old address showed a marketing page with no verdict for sixty seconds, in an untouched Chrome too.

We also found a row the runner had silently stopped running. It read the list with a regular expression, and when Pixelscan's entry grew a long comment between two fields, the pattern skipped it: 53 tests declared, 52 parsed. The runner now compiles the file the app ships instead of pattern-matching it.

Why the list matters to the engine

Several of these pages compare what the page reads with what a worker or an iframe reads. A value changed by a script in the page and not in the worker is worse than the real value, because the disagreement is the signal. That is why Mosaic changes fingerprint values in C++ inside its own Chromium, as told in why we built our own Chromium. The program driving a browser can give it away too, which is the Runtime.enable story, and the hardware records profiles are drawn from are on real fingerprints.

A profile's row menu in Mosaic, with Check detectors next to the leak test
Check detectors opens any of the 49 in that profile