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
| Threat | What it is | What it leaves behind | Bucket that catches it |
|---|---|---|---|
| Honest automation | Playwright, Selenium, Puppeteer, or headless Chromium: a driver that needs to work, not hide | Driver and headless-mode artefacts in browser state | automationLikelihood |
| Anti-detect browser | A browser tuned to forge a human-looking signal surface | Cross-signal contradictions: GPU vs claimed OS, non-deterministic canvas and audio | spoofLikelihood |
| Stealth-wrapped driver | Automation patched to hide its driver footprint | Some surfaces forged, others left intact across both buckets | Both, independently |
| Real user on Brave or a hardened browser | A legitimate privacy browser, not fraud | Patterns that overlap with spoofing but resolve on inspection | Suppressed 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.
The end-to-end flow
| Step | What happens |
|---|---|
| 1. Capture | On page load or at the action you want to protect, call getFingerprint() client-side. You get fingerprint, hardwareFingerprint, and consistency (including spoofLikelihood). No server involved yet. |
| 2. Send | POST the fingerprint hashes and the full consistency result to your backend over your existing API. Decide server-side: never trust a verdict computed in the browser. |
| 3. Store | Optionally persist { hardwareFingerprint, fingerprint, spoofLikelihood, firstSeen, lastSeen } keyed by hardwareFingerprint, so repeat offenders are flagged without recapturing. For pure bot detection, a session-scoped record is often enough. |
| 4. Look up | For the consistency check, the score is already in the payload. If you want to check whether this device has previously been flagged, query your store by hardwareFingerprint. Use compareFingerprints in cross-browser mode for a matchScore when a returning bot may have changed browsers. |
| 5. Decide | high spoofLikelihood: challenge or deny the request immediately. medium spoofLikelihood: apply step-up friction (CAPTCHA, email verification). low spoofLikelihood: allow through. |
| 6. Act and log | Challenge, deny, or allow. Log the spoofLikelihood and which category triggered it for tuning review. Update lastSeen if you maintain a device record. |
Anti-detect gate by spoofLikelihood
| spoofLikelihood value | Recommended action |
|---|---|
| 'low' | Signals are mutually coherent. Treat as a genuine browser session and allow through. |
| 'medium' | Some inconsistency detected. Apply step-up friction: CAPTCHA, rate-limit, or require email verification before continuing. |
| 'high' | Strong cross-subsystem contradiction. Challenge with a hard gate or deny the request outright. Log for audit. |
| Any value on a device already flagged 'high' in a prior session | Persistent offender: deny immediately and escalate to IP-level block or account suspension. |
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.
How should I handle the medium spoofLikelihood tier?
Step-up friction is the right response: a CAPTCHA, a rate limit, or an email verification request. A hard deny at medium is too aggressive for a live product and will affect genuine users. Medium is a signal to raise the cost of the action, not to block it outright.
Can a sophisticated bot pass the consistency check?
A bot running a real browser on real hardware with no signal manipulation will score low spoofLikelihood because its signals are genuinely coherent. The consistency check is most effective against browsers that substitute synthetic signal values to evade fingerprinting. Behavioral signals and rate limiting are complementary layers for fully real-browser bots.
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.
Last reviewed June 17, 2026

