botcheck for your site
One script tag. Ask whether this visitor's browser looks automated.
The same checks as the botcheck page, packaged for any website. The script runs in the visitor's browser, sends nothing anywhere and stores nothing. Your page decides what to do with the answer. Version botcheck-0.1.0.
1. Try it on this page
This page uses the hosted script itself. Move your mouse, click, or tap; the box updates after each click, tap or key.
2. Quick start
Put this in your page's <head>, so it starts listening before the visitor does anything:
<script src="https://botcheck.traland.com/botcheck-0.1.0.js"
integrity="sha384-fK7ONY3RqtwMSv2VAvqJKsveItwLUQskstqX5D5ABeyAInbTEC/ZZsDNHZUJsVX/"
crossorigin="anonymous"></script>
Then, whenever you need to know (before a sign-up, a checkout, a vote):
const result = botcheck.check();
if (result.isBot) {
// add friction: a second step, a CAPTCHA, a delay, a review queue
}
That's all. Loading the script creates window.botcheck and starts recording right away; there is nothing to configure.
https://botcheck.traland.com/botcheck.js follows each new version without an integrity hash. It is easier, but your site then runs whatever botcheck publishes next. Prefer the pinned file above with integrity: the browser refuses it if a single byte changes.When is the answer ready?
- Environment checks (automation flag, headless browser, user agent that disagrees with the engine, …) work immediately.
- Mouse checks need a few clicks with movement in between; touch checks need about 20 taps; the typing check needs some keys.
- Until then those checks report
not_tested. Ask late (at submit time) to get the most evidence.
3. Protect a form
One line. On submit, botcheck adds a hidden field botcheck holding the verdict, and when the verdict is flagged it stops the submit and calls your onBot:
botcheck.protect(document.querySelector('#signup'), {
onBot(result) {
document.querySelector('#signup-msg').textContent =
'Your browser looks automated. Please try again or contact us.';
}
});
Options: onBot(result, event); field (hidden field name, default "botcheck"); block: false to let flagged submits through and only label them, so your server decides. protect() returns a function that removes the protection.
A script calling form.submit() skips submit events in every browser, so it skips this check too. Your server can treat a missing botcheck field as a reason to look closer.
4. React as the visitor interacts
const stop = botcheck.onResult(result => {
badge.hidden = !result.isBot;
});
// later: stop();
onResult runs after each click, tap and key press. It is the same object check() returns.
Single-page apps (React, Vue, Svelte, …)
Load the script once in index.html, then use the global anywhere:
// React
useEffect(() => window.botcheck?.onResult(r => setLooksAutomated(r.isBot)), []);
function onSubmit(e) {
if (window.botcheck?.check().isBot) { e.preventDefault(); showExtraStep(); }
}
Call botcheck.reset() when you want a fresh start, for example after a route change, so evidence from earlier pages does not count.
5. Check again on your server
A verdict computed in the browser can be faked by the browser (see trust). For a decision that matters, send the raw evidence to your own server and run the same analysis there:
// browser
await fetch('/api/signup', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ form: formData, botcheck: botcheck.data() })
});
// Node server (save https://botcheck.traland.com/lib/botcheck.mjs into your project; no dependencies)
import { analyze } from './botcheck.mjs';
const result = analyze({ events: body.botcheck.events, env: body.botcheck.env });
if (result.verdict === 'flagged') { /* review, slow down, or ask for more */ }
Sending is your choice and your responsibility: the data holds pointer positions and timings, key classes (letter, delete, enter, …, never which key) and browser facts. Tell your visitors in your privacy notice.
6. API reference
| Call | Returns | What it does |
|---|---|---|
botcheck.check() | result | Runs every check on what has been recorded so far. Cheap; call it as often as you like. |
botcheck.onResult(fn) | unsubscribe function | Calls fn(result) after each click, tap or key press. |
botcheck.protect(form, options) | remove function | Adds the verdict to the form on submit; blocks flagged submits and calls onBot. See 3. |
botcheck.data() | { events, env } | A copy of the raw evidence, for analyze() on your server. |
botcheck.analyze({ events, env }) | result | The analysis itself, for evidence you hold. |
botcheck.reset() | Forgets everything recorded so far. | |
botcheck.stop() / botcheck.start() | botcheck | Stop or resume listening. Started automatically on load. |
botcheck.version | string | For example "botcheck-0.1.0". |
7. The result object
{
isBot: true, // verdict === 'flagged'
verdict: 'flagged', // 'flagged' | 'suspicious' | 'clear' | 'inconclusive'
version: 'botcheck-0.1.0',
score: 0.9, // 0 to 1, combined weight of the failed checks
headline: 'Strong signs of automation found',
checks: [ // every check, whether it ran or not
{ rule: 'webdriver_flag', layer: 'environment', status: 'fail',
title: 'Automation flag is off (navigator.webdriver)',
detail: 'navigator.webdriver reports true' },
{ rule: 'click_without_movement', layer: 'mouse', status: 'not_tested', … },
…
],
reasons: [ { rule, layer, severity, weight, summary, limitation } ], // failed checks, with why each can be wrong
layers: { … }, stats: { … }, notes: [ … ], limits: [ … ]
}
| verdict | Meaning | Suggested use |
|---|---|---|
flagged | Strong signs of automation (score ≥ 0.70). | Add friction or review. isBot is true. |
suspicious | Some checks failed, not enough to call it a bot. | Log it; maybe a light extra step. |
clear | Every check that could run passed. | Carry on. It does not prove a person. |
inconclusive | Nothing failed, but too little interaction to judge. | Ask again later, or treat as clear. |
Each check's status is pass, fail or not_tested; the botcheck page shows all of them with explanations.
8. What it can and cannot tell you
- It catches the common cases: browsers driven by Selenium, Playwright or Puppeteer with default settings, headless Chrome, user agents that lie about the browser engine, clicks made by script, clicks with no mouse movement, perfectly straight or constant-speed mouse paths, and machine-perfect taps.
- It runs in the visitor's browser, so a determined bot can fake it. It can skip the script, edit
window.botcheck, or send made-up evidence. Use botcheck to add friction to cheap automation, not as a lock. Never let it be the only thing between a bot and money, accounts or data. - It can be wrong about people. Assistive technology, voice control, password managers, hardware keyboards on phones and unusual devices can fail checks. Offer a way forward (a second step, a contact) instead of a dead end.
- No network signals: no IP reputation, TLS fingerprint or cross-site history. Combine it with rate limits and your own server checks.
- Experimental: the touch rules were set from one participant on one phone and are not yet validated on many people.
9. Privacy
- The script sends nothing and stores nothing: no network requests, cookies, localStorage or fingerprint IDs. The evidence lives in the page's memory and is gone when the page closes.
- It never records which key was pressed, only its kind (letter, delete, enter, tab, other).
- Loading the script is a normal file download: like any CDN, the botcheck.traland.com server keeps a standard access log (address, time, browser, and the referring page if your site sends it). Add
referrerpolicy="no-referrer"to the script tag to leave out the referring page. - If you send
botcheck.data()to your server, you are collecting interaction data: say so in your privacy notice.
10. Content-Security-Policy
If your site sends a CSP header, allow the script's origin:
Content-Security-Policy: script-src 'self' https://botcheck.traland.com
botcheck needs nothing else: no connect-src, no inline code, no eval.
11. Versions and caching
| File | Changes? | Cache |
|---|---|---|
/botcheck-0.1.0.js | Never. A new version gets a new file name. | One year, immutable. Use with integrity. |
/botcheck.js | Always the newest version. | Revalidated on every load. |
Both files are served with Access-Control-Allow-Origin: *, which crossorigin="anonymous" and integrity need. The script is about 34 KB (12 KB compressed) and has no dependencies. You may also download it and serve it from your own domain.