Browser Fingerprinting for Click-Fraud Detection
Detecting bots is easy. Detecting bots without blocking customers is the actual problem.
A business spending seriously on paid search eventually notices that traffic and revenue have stopped moving together. Click volume rises, conversions do not, and the ad platform reports everything as healthy.
Some of that gap is ordinary conversion-rate variance. Some of it is traffic that was never going to convert because there was no person attached to it.
Why one signal is never enough
User agent is trivially spoofed. IP address rotates. Screen resolution is a coarse bucket shared by millions of real devices. Any single attribute either misses most automation or catches large numbers of genuine visitors.
What works is the combination, and specifically the inconsistencies within it. A browser claiming to be Safari on iOS while exposing rendering behaviour that only exists on headless Chrome has told you something no individual field could.
- Rendering characteristics: canvas and font metrics as measured, not as declared
- Timing behaviour: how quickly interaction follows page load
- Declared versus observed capability — the mismatch is the signal
- Network context: hosting-provider ranges, known proxy pools
- Behavioural signals: pointer movement, scroll pattern, or their complete absence
Identity that survives evasion
Cookies are cleared, private windows are used, IPs rotate. A useful identifier is derived from the combination of characteristics rather than stored on the device, which is why the same actor can be recognised across sessions that share nothing obvious.
This is deliberately a probabilistic identifier, not a certainty. Treating it as certain is how false positives happen.
Compliance is a design input rather than an afterthought. Fraud prevention and analytics rest on different legal bases in most jurisdictions, so what is collected, why, and for how long is documented and implemented per purpose.
Real-time or pointless
Blocking fraudulent traffic after the click has been billed saves nothing. Scoring runs on the landing request, which means the model must be cheap enough to execute inline.
score = w1*rendering_mismatch
+ w2*timing_anomaly
+ w3*network_reputation
+ w4*repeat_offender_history
if score > block_threshold: block() # high confidence only
elif score > flag_threshold: flag() # admit, but mark
else: allow()
Two thresholds, not one. Between them sits traffic that is admitted and marked, because the cost of the two error types is not symmetric.
Tuning against false positives
Blocking a genuine customer costs a sale and possibly the relationship. Admitting one bot costs a fraction of a click. So thresholds are set conservatively and the flagged band is deliberately wide — flagged traffic is analysed later rather than refused now.
Evidence, not conclusions
Every decision is stored with the signals that produced it. This matters twice: for tuning, because you cannot improve a scoring model whose past decisions you cannot inspect, and for disputes, where a per-click record of reasoning is far more persuasive than an assertion that traffic looked wrong.
Across engagements running this approach, wasted ad spend fell around 40% on average — not by eliminating spend, but by moving it to segments where conversion was possible at all.