Most 2010s "antibots" rely on IP lists: a giant database of suspicious addresses, and if a request comes from one — block. In 2026 that is not enough: grey IP pools cost pennies, and residential proxies give clean US IPs for a few dollars a day. On top of that, big companies hand their users shared addresses — Apple iCloud Private Relay, Google One VPN — and those IPs look cleaner than a regular office address.

TL;DR

IP lists catch cheap bots, but medium and advanced ones walk past them. JS-fingerprint looks at the browser itself — Canvas, WebGL, fonts, plugins — and builds its fingerprint. Behavioural signals (timings, scrolling, mouse movement) add another line of defence. Together with network checks this noticeably cuts automated traffic, but no filter is perfect, and you still need to watch your stats.

Why IP blocking is outdated

Simple math: a datacenter IP costs a couple of dollars a month, a residential one a few dollars a day. A bot stealing conversions in an affiliate program with a $5 payout needs one or two successful leads to cover a day of work. IP banning is whack-a-mole — you have one hammer, the bot has thousands of holes.

What pure IP lists also lose to:

  • Mobile carriers — tens of thousands of users behind one NAT.
  • Corporate networks — you can't ban them, real users are inside.
  • Apple iCloud Private Relay — hides the IP, while ordinary people are behind it.
  • Headless browsers with residential proxies — look like a regular Mac user in Chicago.

What JS-fingerprint is

Idea: every browser is unique in its small details. Engine version, font set, GPU, audio stack, screen resolution, timezone, plugins. Individually banal, together — a fingerprint with roughly 30 bits of entropy or more. Enough to tell a billion users apart.

Roughly the signals a client-side script collects:

// simplified set of signals
{
  canvas: hash(canvasRender("TDS-probe", "Inter 16px")),
  webgl: hash(gl.getParameter(gl.RENDERER) + gl.getParameter(gl.VENDOR)),
  audio: hash(offlineAudioContext.fingerprint()),
  fonts: detectAvailableFonts(['Inter', 'Roboto', ...]),
  screen: { w, h, depth, dpr },
  navigator: { ua, platform, lang, hwConcurrency, memory },
  timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
  plugins: navigator.plugins.length,
  webdriver: !!navigator.webdriver,  // headless flag
  permissions: queryPermissions(['notifications', 'geolocation'])
}

The fingerprint decides nothing on its own — what matters is whether it agrees with what the browser says about itself. If the User-Agent claims an iPhone while WebGL reports software rendering with no GPU, or navigator.webdriver is on, you are more likely looking at automation than at a person.

Behavioural signals

A fingerprint is static. Behaviour isn't. A headless browser doesn't move the mouse like a human, doesn't scroll with "jittery" gestures, doesn't pause on the page for a couple of seconds before clicking. These signals add another layer:

Signal What we observe Bot tells
mousemove curve, acceleration straight, instant
scroll timings, deltas uniform or absent
click coords relative to the element dead centre of the element
focus/blur time on page < 500 ms
keypress key intervals constant or 0

Scoring and decisions

All signals combine into a single probability that the visitor is a bot. The decision isn't binary "bot/not" — it uses thresholds:

  • below 0.5: clean traffic, route to the offer.
  • 0.5–0.7: doubtful visit, show a CAPTCHA; whoever solves it goes on.
  • 0.7 and above: bot, send it to the default page for non-target traffic that you chose: your own URL, a landing page or a response code.

Method trade-offs

JS-fingerprint isn't perfect. Honest downsides:

  • An extra request: it needs JavaScript on the client, which adds latency. The check can be kept short, but it can't be skipped entirely.
  • Browsers with JavaScript turned off: they can only be judged by network, headers and lists.
  • Privacy modes: Brave, Firefox strict mode and Tor spoof part of the fingerprint. The fingerprint alone can't judge these visitors, so the decision comes from all checks together.
  • False positives: some real users land in the CAPTCHA zone. They pass once they solve it, but some drop off.

What we use in TDS

The TDS.SO antibot checks every click in order: rules and lists → network → fingerprint and behaviour → ensemble verdict.

  • Rules and lists: your own IP and User-Agent white and black lists, allowed referrers, empty-UA checks, filter levels 0 to 3.
  • Network: data-center IPs, proxies, TOR exit nodes, public IP-reputation lists.
  • Fingerprint and behaviour: WebGL and canvas, headless-browser markers, navigator.webdriver and automation tools, the mouse movement trail.
  • Ensemble verdict: signals from all checks combine into one probability — from 0.5 the visitor gets a CAPTCHA, from 0.7 the click counts as a bot.

How much traffic was filtered is visible in the stats for every link. Over the last 90 days about 45.8M client clicks went through TDS.SO, and the filter cut about 31.6% of them on average. No filter is perfect: bots with a spoofed referrer still get through often, so watch your stats rather than take anyone's word for it.