bot-signal

Bot detection for JavaScript & Node.js — live demo Is your browser
being scanned…

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

TypeScript · ESM + CJS · browser/server entry points · one dependency · MIT

Verdict Scanning… Running instant checks
Instant checks
Flagged
Behavioral score
not run

Layer 1 · synchronous

Instant detection

detectInstantClient(window) inspects the environment in a single pass — WebDriver flags, automation artifacts, rendering, and window geometry. Signals roll up into a weighted suspicionScore.

Pretend to be a bot:

Layer 2 · over time

Behavioral detection

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.

What the detector sees

move or swipe, scroll, tap, type — anywhere on the page
0mouse moves
0scrolls
0keys
0clicks
0touches
0synthetic
Suspicion score 0.000
0.55 threshold
0 · human1 · automated

    Layer 3 · Node

    Server-side scoring

    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.

      Suspicion score 0.000
      0.50 threshold
      0 · human1 · automated
      Illustrative context · signals score independently

      Ship it

      Quick start

      One package, three layers. Import from the root, or use the /browser and /server entry points.

      Browser — block on load

      import { isHuman } from "bot-signal";
      
      const result = detectInstantClient(window);
      
      if (!result.isLegitClient) {
        location.href = "/blocked";
      }

      Behavioral — observe & score

      import { createBehavioralClientDetector } from "bot-signal";
      
      const result = await createBehavioralClientDetector({ context: window })
        .observe(10_000);
      
      if (!result.isLegitClient) challenge();

      Server — score a request

      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

      Frequently asked

      What the library detects, and where it deliberately stops.

      Can bot-signal detect headless Chrome?

      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.

      Does it detect Playwright, Puppeteer and Selenium?

      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.

      Does it need an API key or an external service?

      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.

      Will it block real users by mistake?

      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.

      Can a determined bot evade it?

      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.

      Does it work with React, Next.js or plain HTML?

      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.