Research

FingerprintJS Pro and Kaidn, side by side in 24 privacy browser sessions

Alex MugoFounder, Kaidn
10 min readRevised
We ran both engines in the same page loads across Tor, Mullvad, Brave and Camoufox. Every number, including the two rounds where our own detector was the one that was wrong.

We build a fraud scoring API. Part of what it scores is a browser fingerprint, so we wanted to know two things that vendors in this market do not usually publish:

  1. Does our detector actually fire on the browsers it claims to catch, or is that design intent that nobody ever measured?
  2. How does it compare to the market leader when both engines see the exact same page load?

So we built a bench that runs FingerprintJS Pro and Kaidn's collector in the same page, in the same browser, on the same visit, and we ran real privacy and anti-detect browsers through it as GUI applications rather than headless automation.

This post is every number that came out, including the two rounds where we were the ones who got it wrong. It is longer than a marketing post because the numbers only mean something with the method attached.

First, what "the agent" is#

Skip this if you already know.

Browser fingerprinting vendors do not run inside your server. They ship a JavaScript agent: a script your site loads into the visitor's browser, which measures a few hundred properties of that browser (canvas rendering, fonts, audio stack, screen metrics, WebGL driver strings, timezone) and sends them to the vendor's API. The vendor turns that into an identifier and a risk signal.

That architecture has a dependency most risk dashboards never surface: the script has to load and execute before anything else can happen. It is fetched from a third-party domain, it runs in a browser the visitor controls, and either of those can stop it.

When it does not load, you do not get a cautious verdict, an elevated score, or an error row. You get nothing at all, and nothing looks a great deal like a clean session.

That was the first thing we set out to measure.

The setup#

  • Both engines in one page context. FingerprintJS Pro's agent and Kaidn's collector run in the same page load, so neither gets a different environment to judge. Roughly 160 browser features are captured per session alongside both engines' output.
  • Real GUI applications, not headless. Privacy browsers were launched as real apps after a manual first launch, because running them headless changes the exact signals under test.
  • Sessions counted individually, never averaged. Anti-detect tools randomise per launch, so a mean across launches hides the thing you are looking for.
  • One Mac host, one geography. This is a real limit and it is repeated in the limitations section, not buried there.

Tor needed Security Level Standard and network.proxy.allow_hijacking_localhost=false to reach a local capture page at all, or it routes 127.0.0.1 through SOCKS and the page never loads.

What each engine gives you, and why the two numbers are not the same number#

This is the part that makes the results readable, and it is where most comparisons quietly cheat.

FingerprintJS Pro's browser agent returns a visitor_id, an event_id, a suspect_score, and a sealed_result that was null in every session we ran. Setting extendedResult: true added nothing. Their richer signals, including their tampering and anti-detect ML scores, require a server-side lookup keyed on that event_id with a secret key. We are reporting the browser agent's suspect_score because that is what the agent itself exposes.

Kaidn's collector (@kaidn/fp) returns an antidetect_score from 0 to 100, the list of signals that fired, and a coarse confidence. That score is computed in the browser, by rules, in the open. There is no model.

Two warnings you need before reading any table below:

  • These are two vendors' internal 0 to 100 scales and they are not interchangeable. Their threshold for "suspicious" is not documented publicly and ours is not their scale. Compare the direction and the behaviour, never the absolute values.
  • Kaidn's antidetect_score is a collector output, not a /v1/score verdict. At the time of writing the API accepts the field and does not yet persist or score it, so nothing in this post describes a production block decision. Saying otherwise would be the same overclaiming we are trying to measure.

How the Kaidn score is actually built#

The number is worth explaining in full, because every figure in this post is one of four combinations of it.

Seven signals, each with a weight:

signalweightwhat it means
tampered45a native fingerprinting API was overridden and the patch leaks
contextMismatch40the browser's identity differs between the main thread and a Web Worker
osMismatch40an OS-truth signal contradicts the OS the User-Agent claims
engineMismatch35the real JS engine contradicts the claimed browser
noiseInjected30the same canvas draw reads back differently within one session
fontStandardized30the font surface matches a known anti-detect profile
headless15automation tells: webdriver, headless UA, no languages

They combine as a noisy-OR rather than a sum. Each fired signal multiplies the probability the session is clean, and the score is what is left:

the noisy-OR, in full
P(clean) = Π (1 - weight)        score = round((1 - P(clean)) × 100)

So the score can never run past 100, and the second signal always adds less than the first. That is deliberate: two signals agreeing is stronger than one, but not twice as strong.

Confidence is separate and blunter. A signal counts as strong if its weight is 30 or more. Two or more strong signals is high, one is medium, none is low. Remember that rule, because it is what makes our worst result in this post worse than it first looks.

That is where the numbers in every table below come from:

scorewhat firedarithmetic
0nothingno signal
15headless only1 - 0.85 = 0.15
30one 30-weight signal only1 - 0.70 = 0.30
41headless + noiseInjected1 - (0.85 × 0.70) = 0.41
51noiseInjected + fontStandardized1 - (0.70 × 0.70) = 0.51

The results#

Three runs per session unless stated. Every row states who was right, explicitly.

sessionrunsKaidnFingerprintoutcome
Chrome 151, driven headless315suspect 7Both correct. Ours is the headless signal firing on a genuinely headless browser, which is what it is for
Brave, real GUI30suspect 0Both correct. An honest browser scored honest by both
Tor Browser, honest user, real GUI351suspect 21We were wrong and they were less wrong. See below
Mullvad Browser, honest user, real GUI351⛔ agent blocked 3/3We were wrong. They had no verdict at all
Camoufox spoofing macOS on macOS90 in 6, 30 in 3⛔ agent blocked 9/9Nobody caught it. We missed it two thirds of the time
Camoufox spoofing Windows on macOS930 in 9⛔ agent blocked 9/9We caught it, unopposed

Four findings come out of that table, and they do not all point the same way.

Outcome 1: their agent never executed on 21 of 24 sessions#

Mullvad 3 of 3, Camoufox 18 of 18. The Mullvad failures were fast, 316 to 833ms, which is what a blocked request looks like rather than a slow one.

The cause is not clever. Both of those browsers ship uBlock Origin, and uBlock subscribes to EasyList and EasyPrivacy by default. Both lists carry rules for Fingerprint's domains:

the two rules that do it
EasyPrivacy    ||fpjs.io^$third-party
EasyList       ||fpnpmcdn.net^

The first blocks their API domain whenever it loads from a site other than the one being visited, which for a vendor embedded on customer sites is every time. The second blocks their CDN outright.

We confirmed it two ways rather than one. From the lists: 164,000 lines of EasyPrivacy, EasyList and uBlock's own privacy and badware filters. Empirically, with the capture page on localhost so that both vendors' domains are third-party exactly as they would be on a real customer site:

browserapi.kaidn.iofpnpmcdn.netapi.fpjs.io
Mullvad (uBlock)200⛔ NetworkError⛔ NetworkError
Camoufox (uBlock)200⛔ NetworkError⛔ NetworkError
Brave (Shields)200⛔ Failed to fetch⛔ Failed to fetch

This is not a defeat of the fingerprinting technique. The technique never runs.

And it cannot be repaired afterwards. Their richer signals need a server-side lookup keyed on the event_id that the agent produces. No agent run means no event_id, so a blocked session is not delayed data. It is absent data, permanently.

The honest reading of our own 200s: we are not on those lists because nobody has added us. Zero matches for kaidn across all 164,000 rules. Any fingerprinting endpoint gets listed once it is worth listing, and ours will be. Note also the $third-party scope on their rule: serving the script first-party is the known escape, and it is available to them exactly as much as to us. This is obscurity, not architecture. A head start, not a moat.

Outcome 2: we scored honest privacy users as anti-detect browsers, at high confidence#

This is the worst result in the study and it is ours.

Tor Browser and Mullvad Browser, real GUI, honest users, scored 51 across all 15 runs. From the table above, 51 is noiseInjected plus fontStandardized. Both are 30-weight signals, so two strong signals agreed and the collector reported high confidence. It was confidently wrong.

Both reasons were wrong, and each for its own mechanism:

  • fontStandardized fired because Tor and Mullvad ship a 150-entry font.system.whitelist which includes Helvetica and excludes the macOS system UI face. Gecko's macOS sans-serif default is Helvetica, so system-ui falls back onto it and three generic font families collapse to the same metrics. Our rule read that collapse as an anti-detect profile. It is a privacy browser doing exactly what a privacy browser is supposed to do.
  • noiseInjected fired because privacy.resistFingerprinting randomises canvas readback, which is the entire point of the setting. Tor, Mullvad and LibreWolf all enable it by default.

For a rewards operator, elevating a Tor session may well be the right verdict. But the reason attached to it claimed an anti-detect browser was actively randomising its fingerprint to evade detection, which is a different accusation about a different person. Explainability is the product. A right answer with wrong evidence is not a right answer.

What we changed. We recomputed the rule over the full raw corpus, 85 honest sessions across 18 configurations plus 10 Camoufox sessions, and replaced the naive generic collapse with a narrower predicate that requires the specific system-ui behaviour that only the anti-detect tool shows:

shipped rulereplacement
fires on honest sessions (n=85)150
catches Camoufox (n=10)710

Strictly better on both axes. Shipped and deployed, then re-measured in the real browsers rather than assumed: Tor and Mullvad both fell from 51 to 30, with the font signal silent 3 of 3 each.

The residual 30 is still a false positive and it is still open. It is resistFingerprinting tripping noiseInjected, one strong signal, medium confidence. Brave is already suppressed in that detector. RFP needs either the same treatment or its own reason code, and until it gets one, an honest Tor user still collects a signal that says something untrue about them.

Outcome 3: where both engines actually ran, Fingerprint was better#

Three sessions had both engines executing. On the one that matters, they beat us.

On the honest Tor session, they returned suspect 21 against our 51. Their threshold is not published, so we cannot say 21 was "a pass". What we can say is that we crossed our own review bar on an honest user and they scored the same session at less than half our number.

The honest Chrome and Brave rows are ties, and the Chrome 15 is our automation signal correctly detecting a headless browser, not a false positive.

There is a fourth data point from earlier work that belongs here because it is the most important one. On a Multilogin profile spoofing its operating system, Fingerprint's own explicit os_mismatch method returned false, and their ML model returned 0.9999. Their rule missed it and their model caught it, at near-certainty.

Our engine is explicit rules of exactly the kind that returned false. Their anti-detect detection is population ML trained over billions of labelled sessions. We cannot replicate that data, and pretending the gap is a matter of cleverness rather than corpus size would be dishonest. What we can do is start accumulating our own labelled corpus, which is why the bench in this post exists at all.

Outcome 4: same-OS spoofing beat both engines#

The strongest anti-detect configuration we tested is the least exotic one: Camoufox spoofing macOS while running on macOS. No operating system to contradict, so every OS-truth signal has nothing to compare against.

Kaidn scored 0 in 6 of 9 runs. Fingerprint had no verdict, because their agent was blocked. Neither engine caught it. Our new font predicate did fire on 9 of 9, and it is the only thing in either engine that saw that session, but it was not shipped into a verdict at the time.

We are stating this plainly because the blocked-agent result would otherwise imply we came out of this study ahead. Same-OS spoofing defeats the current state of the art on both sides.

What this means if you are buying fraud detection#

Any vendor whose detection runs in the browser has the dependency described at the top of this post. Ask them:

  • What is your agent's load-failure rate, and how are those sessions represented in my dashboard? Do they appear as rows scored unknown, or do they simply never arrive?
  • Is agent failure itself surfaced as a signal? A fingerprinting script failing to load is anomalous, and it is information even when the fingerprint is not. Most stacks discard it.
  • When a signal fires, what evidence is attached? Our Tor result is the argument for this one. A score you cannot audit is a score you cannot correct.
  • Which findings are rules and which are models? Both are legitimate. They fail differently, and you should know which failure you have bought.

Limitations, stated properly#

  • The consumer privacy browser finding rests on n=3. Of the 21 blocked sessions, 18 were Camoufox, an automation browser, and only 3 were Mullvad, a browser real people install. Three is below the n≥5 bar we hold ourselves to before quoting a rate. Treat the Mullvad result as a strong signal that needs replication, not as a published rate.
  • The Tor agent ran 2 of 3, and the third failure was our own 8 second deadline on a cold load over the Tor network, not a block. We are counting it as a harness artifact, not a finding.
  • Brave is the useful control. It ships aggressive tracker blocking and Fingerprint runs fine in it, scoring an honest Brave session at 0. The blocking is specific to browsers that ship uBlock Origin with EasyPrivacy enabled, not to privacy browsers as a category.
  • One Mac host, one geography. Network-layer differences elsewhere could change agent load behaviour.
  • The positive class is one tool. Every Camoufox catch is Camoufox. A rule fitted to one tool on one machine is a lead, not a general detector, and we say so in the code.
  • Corpus sizes: 103 honest sessions, 28 anti-detect sessions, 24 privacy and anti-detect sessions in the head-to-head above.

Reproduce the blocklist half yourself#

This takes one command and no access to our bench:

Terminal
curl -s https://easylist.to/easylist/easyprivacy.txt \
       https://easylist.to/easylist/easylist.txt \
  | grep -nE 'fpjs\.io|fpnpmcdn\.net'

You should get two rules back, one from each list. Swap in any other vendor's domains to check where they stand. The lists change, so line numbers will drift.


If you want the engine that produced the Kaidn numbers above, it is @kaidn/fp on npm, and every signal it reports comes back with the evidence attached. The scoring API docs show what the server does with it.

Frequently asked questions

Do ad blockers stop device fingerprinting?

They stop a third-party agent from loading, which for a hosted fingerprinting vendor is the same thing. In this head-to-head, Fingerprint's agent did not execute on 21 of 24 privacy and anti-detect browser sessions, because both EasyPrivacy and EasyList carry rules for its domains and the browsers involved ship uBlock Origin by default. A blocked agent does not return a weak verdict, it returns none at all.

Which filter rules block Fingerprint, specifically?

Two, one from each list: EasyPrivacy carries ||fpjs.io^$third-party, which blocks the API domain whenever it loads from a site other than the one being visited (for a vendor embedded on customer sites, that is every time), and EasyList carries ||fpnpmcdn.net^, which blocks the CDN outright. You can verify this in one curl command against the published lists.

Is Kaidn better at detecting anti-detect browsers than Fingerprint?

No, and the honest reading of these results is more mixed than that. Where both engines actually ran, Fingerprint was better. Kaidn's advantage in this test was availability rather than accuracy: a first-party collector is not on the filter lists, so it returned a verdict where a third-party agent returned nothing. Availability is a real advantage and it is not the same as being more accurate.

Can either engine catch a same-OS anti-detect profile?

Neither did. Camoufox spoofing macOS while running on macOS was missed by Kaidn in six of nine runs and produced no verdict at all from Fingerprint in all nine, because the agent was blocked. A profile that does not lie about its operating system removes most of the inconsistency both engines look for. This is the state of the art rather than a gap in one product.

What went wrong with the font signal?

It fired at high confidence on honest privacy users. Tor Browser and Mullvad Browser both standardise the font surface deliberately, as an anti-fingerprinting measure, and the rule read that collapse as an anti-detect profile: 51 out of 100 on real people doing nothing wrong. The fix narrowed the predicate to the specific system-ui behaviour only the anti-detect tool shows, and it is deployed.

How large was the sample, and what does that limit?

103 honest sessions, 28 anti-detect sessions, 24 privacy and anti-detect sessions, all from one Mac host in one geography. That is enough to establish that the blocking happens and that same-OS spoofing defeats both engines; it is not enough to quote rates as population statistics. Sessions are counted individually and never averaged, because anti-detect tools randomise per launch.

browser fingerprintingmeasurementprivacy browsersanti-detect browsers

Score your own traffic

10,000 events a month on the free tier, no card. One POST to /v1/score and you get a verdict with the evidence behind it.

Read next