Browser — block on load
import { isHuman } from "bot-signal";
const result = detectInstantClient(window);
if (!result.isLegitClient) {
location.href = "/blocked";
}
bot-signal detects headless Chrome, Playwright, Puppeteer and Selenium, and scores scripted mouse, touch and typing input — in the browser and on the server. This page runs the real library against you.
npm install bot-signal
Layer 1 · synchronous
detectInstantClient(window) inspects the environment in a single pass —
WebDriver flags, automation artifacts, rendering, and window geometry. Signals roll up
into a weighted suspicionScore.
Layer 2 · over time
createBehavioralClientDetector() watches how the pointer, touch,
wheel, and keyboard behave. Humans are noisy; scripts are not. Swipe paths and tap
rhythm are scored the same way mouse movement is, so this works on a phone or
tablet. Keyboard clicks and key auto-repeat are recognized — they never count
against you.
Layer 3 · Node
detectServerClientAsync() runs in Node — GeoIP, datacenter ranges,
AbuseIPDB, iCloud Private Relay, and TLS fingerprints, all offline. This simulator
uses the library's real weights and formula:
score = 1 − Π(1 − wᵢ).
Simulated This page is frontend-only — it is served as static files with no backend, so nothing here inspects your real IP, TLS fingerprint, or headers. Pick a scenario or toggle signals to see how the server layer would score a request; the two layers above do run against your actual browser.
Ship it
One package, three layers. Import from the root, or use the
/browser and /server entry points.
import { isHuman } from "bot-signal";
const result = detectInstantClient(window);
if (!result.isLegitClient) {
location.href = "/blocked";
}
import { createBehavioralClientDetector } from "bot-signal";
const result = await createBehavioralClientDetector({ context: window })
.observe(10_000);
if (!result.isLegitClient) challenge();
import { detectServerClientAsync } from "bot-signal/server";
const result = await detectServerClientAsync({
clientIp: req.ip,
clientTimezone: req.headers["x-timezone"],
userAgent: req.headers["user-agent"],
tlsFingerprint: req.headers["x-ja3-hash"],
});
if (!result.isLegitClient) res.status(403).end();
Questions
What the library detects, and where it deliberately stops.
Yes. It checks the HeadlessChrome User-Agent,
appVersion and Client Hints brands, plus the indirect
tells that survive a User-Agent rewrite: a software WebGL renderer
(SwiftShader or llvmpipe), no enumerable media devices, an
unbranded Chromium build with no H.264 support, and a zero
Network Information RTT.
Yes. It looks for Playwright bindings and init scripts, Puppeteer
evaluation artifacts, ChromeDriver cdc_ keys and
Selenium document markers. It also detects the Chrome DevTools
Protocol itself, by the Error serialization side
effect it leaves in both the page and a dedicated worker — which
catches automation that has scrubbed its own globals.
No. The GeoIP database and the IP blocklists ship inside the package and are refreshed weekly, so every layer runs on your own infrastructure with nothing phoning home.
Every signal carries a weight and a confidence level. Definitive automation markers weigh 0.9–1.0 and block on their own, while ambiguous checks that also fire on in-app browsers, virtual machines and privacy-hardened browsers weigh 0.25–0.5, so they only block when several stack up. Browser fingerprint protection is treated as a privacy feature, not as automation.
Yes — client-side detection is a cost, not a wall. A purpose-built anti-detect browser can pass the instant checks. That is why the library scores three independent layers: forging the browser environment, human-looking mouse and touch input, and a residential IP with a matching TLS fingerprint all at once is a much higher bar than any one of them.
All three. The package ships ESM and CJS builds with TypeScript types, resolves to a browser-only bundle in browser bundlers, and is also published as a script tag build on unpkg for pages with no build step.