Orange textured background

Anti-fraud

How to detect bots and anti-detect browsers without a CAPTCHA

Honest automation and anti-detect browsers are two different threats with two different tells. A single fingerprint hash catches neither; two independent scores catch both.

Bots and anti-detect browsers are two unrelated threats that a single fingerprint hash misses. Benny returns two independent scores on every call, an automation likelihood for drivers and a spoof likelihood for forged browsers, so you can gate both client-side without a CAPTCHA.

Two threats that look nothing alike

When people say 'bot detection' they usually mean two unrelated problems. The first is honest automation: a Playwright, Selenium, or Puppeteer script, or a headless Chromium instance, driving a real browser engine. It is not trying to hide. It just needs to work, so it leaves behind the artefacts that driver-controlled browsers have always left behind.

The second is an anti-detect browser: a browser deliberately tuned to forge a human-looking signal surface so it can spin up fresh, believable identities at scale. Its posture is the opposite. Its entire job is to hide. It does not look automated; it looks like a slightly-off human.

These two threats leave completely different evidence, and a defence built for one is mostly blind to the other. That is why a single answer, one hash or one CAPTCHA or one blended score, tends to miss half the problem.

Why a single fingerprint hash catches neither

A fingerprint hash answers one question: is this the same device as before. It is silent on 'is this a driver' and 'is this surface forged'. A headless script and a hand-driven browser on the same machine can produce the same hash, and so can an anti-detect browser that happens to reuse a profile. The hash is an identity primitive, not a risk primitive.

CAPTCHAs have the inverse problem. They add friction to every real user to catch a slice of automation, and modern solver services and stealth wrappers clear them routinely. They also do nothing about an anti-detect browser operated by a human, because there is a human present to solve the puzzle.

The signal you actually want is evidence about the browser itself: does it carry driver artefacts, and do its signals tell a coherent story. Those are two separate questions with two separate answers.

Two threats, two kinds of evidence

ThreatWhat it isWhat it leaves behindBucket that catches it
Honest automationPlaywright, Selenium, Puppeteer, or headless Chromium: a driver that needs to work, not hideDriver and headless-mode artefacts in browser stateautomationLikelihood
Anti-detect browserA browser tuned to forge a human-looking signal surfaceCross-signal contradictions: GPU vs claimed OS, non-deterministic canvas and audiospoofLikelihood
Stealth-wrapped driverAutomation patched to hide its driver footprintSome surfaces forged, others left intact across both bucketsBoth, independently
Real user on Brave or a hardened browserA legitimate privacy browser, not fraudPatterns that overlap with spoofing but resolve on inspectionSuppressed by Brave-aware handling

Two free scores, computed on every call

Every getFingerprint call returns two scores alongside the fingerprint, at no extra cost and with no extra round trip. They are independent by design.

result.automation.automationLikelihood targets driver-controlled browsers. It returns 'low', 'medium', or 'high' from a battery of checks across driver leaks, headless-mode artefacts, and stealth-patch residue. result.consistency.spoofLikelihood targets anti-detect activity. It returns the same three buckets from cross-signal consistency checks: whether the GPU agrees with the claimed OS, whether canvas and audio are deterministic across repeat calls, and similar contradictions a forged surface tends to expose.

The two evidence sets barely overlap. A stealth-wrapped driver can read clean on consistency while tripping automation, and an anti-detect browser can read clean on automation while tripping consistency. Consume both buckets independently rather than collapsing them into one number. The full scoring model behind each bucket lives in the docs/scoring-automation and docs/scoring-consistency pages.

import { getFingerprint } from 'doorman-benny';

async function screenSession() {
  const result = await getFingerprint();

  // Two independent buckets, computed on the same pass. No extra calls.
  const driverRisk = result.automation.automationLikelihood; // 'low' | 'medium' | 'high'
  const spoofRisk = result.consistency.spoofLikelihood;      // 'low' | 'medium' | 'high'

  return { driverRisk, spoofRisk };
}

Both scores ride on the result you already have. automationLikelihood targets drivers; spoofLikelihood targets forged surfaces. They are computed independently.

Wiring it into a gate

The pattern is to read both buckets at a decision point, such as signup, login, or checkout, and map the pair to an action. Treat 'high' on either bucket as strong evidence and 'medium' as a reason to add friction rather than block outright.

Key your rules off the bucketed likelihoods, not the individual flags. Each result also carries a flags array of opaque check names, but those are telemetry: forward them to your backend for logging and tuning, and expect the set to change between releases. The 'low' | 'medium' | 'high' bucket is the stable contract.

import { getFingerprint } from 'doorman-benny';

type Action = 'allow' | 'challenge' | 'review';

async function gate(): Promise<Action> {
  const { automation, consistency } = await getFingerprint();

  // Strong evidence on either bucket: route to manual review.
  if (automation.automationLikelihood === 'high') return 'review';
  if (consistency.spoofLikelihood === 'high') return 'review';

  // Medium on either bucket: add friction instead of blocking.
  if (automation.automationLikelihood === 'medium' ||
      consistency.spoofLikelihood === 'medium') {
    return 'challenge';
  }

  // automation.flags and consistency.flags are opaque telemetry strings.
  // Forward them to your backend for logging; do not gate on them directly.
  return 'allow';
}

Gate on the buckets, not the flags. The flag arrays are for telemetry and tuning; the likelihood buckets are the stable interface.

Handling false positives honestly

No browser-side check is free of false positives, and the worst false positive is flagging a privacy-conscious real user. Two browser states deserve special care: Brave, whose Shields apply deterministic per-site modifications that overlap with both driver and spoof signatures, and browsers running anti-fingerprinting extensions that inject noise.

Benny suppresses the known false positives from these states at a category level. When Brave is confirmed, the Brave-only signatures are re-evaluated so a normal Brave session does not read as automation or as a spoofer, while genuine driver or spoof evidence on Brave still surfaces through the rest of the check set. The marker stays visible in flags for your telemetry; only the bucket is adjusted.

The practical guidance is the same as for any risk signal: prefer friction over hard blocks at 'medium', reserve hard responses for 'high' on a bucket, and keep a human-review path for the cases that sit in between.

Frequently asked questions

Can't I just use a CAPTCHA to stop bots?

A CAPTCHA adds friction to every legitimate user and is routinely cleared by solver services and stealth-wrapped automation. It also does nothing about an anti-detect browser operated by a real human, because a person is present to solve it. Browser-side automation and consistency scoring evaluate the browser itself, so they catch both honest drivers and forged surfaces without interrupting real users.

Does this detect headless Chrome, Playwright, and Selenium?

Yes. Driver-controlled browsers are exactly what the automation score targets, and they surface a high automationLikelihood through driver leaks and headless-mode artefacts. Coverage is strongest on Chromium-derived browsers; detection on Firefox- and WebKit-based automation is intentionally narrower because the most reliable signals are Chromium-specific.

What is the difference between automationLikelihood and spoofLikelihood?

automationLikelihood flags driver-controlled browsers that are not trying to hide, such as headless Chromium or a Selenium script. spoofLikelihood flags anti-detect browsers that forge a human-looking signal surface. The two evidence sets barely overlap, so a backend should consume both independently rather than merging them into one number.

Will normal Brave or privacy-extension users get flagged as bots?

Benny applies Brave-aware false-positive suppression: when Brave is confirmed, the Brave-only signatures that overlap with automation and spoof patterns are re-evaluated so an ordinary Brave session reads as low risk. The same handling covers a small set of other known privacy browsers. Genuine driver or spoof evidence on those browsers still surfaces through the rest of the checks.

Does bot detection work without a server?

The scoring runs entirely client-side and is returned on the same getFingerprint call as the fingerprint, with no network round trip required to compute it. You will still want to send the bucketed results to your backend to act on them, but the detection itself needs no server-side service or API key.

Get started

Catch both kinds of bot in one call

npm install doorman-benny. Automation scoring, anti-detect consistency scoring, and cross-browser device identity in one client-side package. No CAPTCHA, no server-side service, no per-call cost.

Get startedSee Benny vs ThumbmarkJS

Related posts

Last reviewed June 17, 2026