Human interaction verification through behavioral biometrics. Analyzes mouse movement, click patterns, keystroke dynamics, scroll behavior, touch pressure, and device sensors to distinguish humans from bots. Returns integrity score and anomaly flags. Zero dependencies.
Sophisticated bots with undetected-chromedriver can pass signal checks. They cannot replicate the involuntary biomechanical patterns of a human hand.
npm install motion-attestationimport { createCollector } from 'motion-attestation';
const collector = createCollector();
collector.attach();
// Track clicks on specific elements
collector.bind(document.getElementById('submit'), 'submit-btn');
collector.bind(document.getElementById('agree'), 'agree-checkbox');
// Wait for enough data (3-15 seconds of interaction)
const interval = setInterval(() => {
if (collector.isReady()) {
clearInterval(interval);
const data = collector.getData();
collector.detach();
// Send data to server for analysis
}
}, 500);import { analyze, classifyScore } from 'motion-attestation';
const { score, penalty, reasons, categories } = analyze(data);
// score: 0.0-1.0 (1.0 = human, 0.0 = bot)
// penalty: total deductions
// reasons: ["[mouse] Low curvature entropy: 0.82 (straight-line)"]
// categories: per-category { penalty, maxPenalty, reasons }
const verdict = classifyScore(score);
// "human" | "suspicious" | "bot"// Server
import { createServer } from 'motion-attestation';
const attestation = createServer({
secretKey: process.env.MOTION_SECRET, // optional
scoreThreshold: 0.5,
});
// Mount on existing HTTP server
import { createServer as createHttpServer } from 'node:http';
const httpServer = createHttpServer(attestation.handler());
httpServer.listen(3000);
// Verify tokens from downstream services
const payload = attestation.validateToken(token);// Client (browser)
import { createCollector } from 'motion-attestation';
// 1. Init challenge
const { challengeId } = await fetch('/interactions/init', {
method: 'POST',
}).then((r) => r.json());
// 2. Collect interactions
const collector = createCollector();
collector.attach();
// 3. Submit when ready
const data = collector.getData();
collector.detach();
const response = await fetch('/interactions/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ cid: challengeId, d: data, ts: Date.now() }),
});
const { cleared, score, token, flags } = await response.json();import { signToken, verifyToken, generateKey } from 'motion-attestation';
const key = generateKey();
const token = signToken({ score: 0.95, iat: Date.now() }, key);
const payload = verifyToken(token, key); // null if invalid/expiredClient Server
| |
|--- POST /interactions/init ------------->| Create challenge
|<-- { challengeId, ttl } -----------------|
| |
| +----------------------+ |
| | Collect for 3-15s: | |
| | * Mouse movement | |
| | * Click positions | |
| | * Keystroke timing | |
| | * Scroll patterns | |
| | * Touch + pressure | |
| | * Gyro/Accel sensors | |
| | * Event ordering | |
| +----------------------+ |
| |
|--- POST /interactions/verify ----------->| Analyze biometrics
| { cid, d, ts } |
|<-- { cleared, score, token, flags } -----|
| |
|--- GET /protected ---------------------->| Bearer token validation
| Authorization: Bearer <token> |
|<-- { message, score } -------------------|
| Category | Data | Desktop | Mobile |
|---|---|---|---|
| Mouse position | Sub-pixel x,y with timestamps | * | |
| Click landing | Offset from target center + dwell time | * | |
| Keystroke timing | Hold duration + inter-key gaps | * | * |
| Scroll behavior | Position, delta, timestamps | * | * |
| Touch events | Position, pressure, contact radius | * | |
| Accelerometer | 3-axis acceleration readings | * | |
| Gyroscope | 3-axis rotation rate | * | |
| Device orientation | Alpha, beta, gamma angles | * | |
| Event ordering | Timestamped sequence of all event types | * | * |
| Bound element hits | Click offset from center per bound element | * | * |
| Engagement | Time-to-first-interaction, session duration | * | * |
The core signal. Human motor control follows biomechanical constraints that are extremely hard to fake.
| Check | Human | Bot | Penalty |
|---|---|---|---|
| Curvature entropy | Variable curvature, high entropy | Near-zero curvature, low entropy | 0.05-0.12 |
| Micro-tremor | 0.3-8px wobble (8-12 Hz tremor) | <0.05px smooth or >20px noise | 0.06-0.10 |
| Velocity variance | CV >0.4 (Fitts' Law profile) | CV <0.15 (constant speed) | 0.05-0.12 |
| Jerk analysis | High variance (corrections) | Low variance (smooth function) | 0.06 |
| Straightness index | 1.02-1.5 (natural curves) | ~1.000 (ruler-straight) | 0.04-0.10 |
| Direction entropy | >2.5 (normal distribution) | <1.2 (discrete angles) | 0.08 |
| Timing regularity | Variable intervals | >70% identical intervals | 0.08-0.10 |
| Teleportation | Never | >300px in <10ms | 0.08-0.15 |
| Origin clustering | Never | Points at (0,0) | 0.08 |
| Bezier detection | Irregular acceleration | >85% constant acceleration | 0.10 |
| Timestamp format | Integer milliseconds | Non-integer (synthetic generation) | 0.10 |
| Sub-pixel precision | 0-2 decimal places | >6 decimal places (Math.random) | 0.08-0.15 |
| Autocorrelation | Aperiodic movement | Periodic patterns (sinusoidal) | 0.10 |
| Movement continuity | Pauses >150ms (thinking) | No pauses (continuous automation) | 0.06 |
| Velocity minima | Corrective sub-movements | No mid-trajectory corrections | 0.06 |
Humans almost never click the exact geometric center of an element.
| Check | Human | Bot | Penalty |
|---|---|---|---|
| Center offset | >5% offset, variable | >70% within 5% of center | 0.06-0.12 |
| Offset variance | std >0.02 | std <0.02 (identical) | 0.08 |
| Click dwell time | 60-250ms, variable | <10ms or perfectly uniform | 0.06-0.08 |
| Zero-duration | Never | Dispatched without mousedown | 0.08 |
Fitts' Law: humans decelerate as they approach a click target.
| Check | Human | Bot |
|---|---|---|
| Velocity in last 500ms before click | Decreasing (approach phase) | Constant or increasing (no targeting behavior) |
Every human has a unique typing rhythm that is impossible to perfectly replicate.
| Check | Human | Bot | Penalty |
|---|---|---|---|
| Dwell time | 50-200ms, variable | <5ms or identical across keys | 0.08-0.10 |
| Flight time | 30-500ms, variable | <15ms (impossible) or uniform | 0.08-0.10 |
| Rhythm entropy | >2.0 (natural variance) | <1.5 (mechanical precision) | 0.06 |
| Uniform detection | CV >0.15 | CV <0.08 (robotic uniformity) | 0.08 |
| Check | Human | Bot |
|---|---|---|
| Velocity variance | Variable bursts + pauses | Constant velocity (CV <0.15) |
| Direction reversals | Frequent (re-reading) | None (one-directional) |
| Pauses | >300ms gaps (reading) | Continuous scrolling |
| Delta uniformity | Variable scroll amounts | Fixed increments (CV <0.05) |
Real fingers produce a pressure distribution and blob-shaped contact area.
| Check | Human | Bot |
|---|---|---|
| Pressure variation | CV >0.01 (changing during gesture) | CV ~0 (constant or absent) |
| Contact area | radiusX/Y vary as finger rolls | Constant or zero |
| Trajectory wobble | Sub-pixel deviations | RMS <0.3px (geometrically perfect) |
| End deceleration | Natural slowdown at swipe end | Constant velocity or abrupt stop |
A human holding a device introduces involuntary micro-movement.
| Check | Human | Bot |
|---|---|---|
| Accelerometer noise | stddev 0.01-0.5 per axis (micro-vibrations) | Zero or perfectly static |
| Gyroscope tremor | Detectable 8-12 Hz oscillation | Zero rotation rate |
| Orientation drift | Slow natural drift over time | Perfectly fixed angles |
| Cross-axis correlation | Correlated noise between axes | Independent or zero |
Browser events fire in specific sequences. Violations indicate synthetic dispatch.
Expected: mousemove -> mousedown -> mouseup -> click
keydown -> keyup
touchstart -> touchmove -> touchend
| Anomaly | Penalty |
|---|---|
| Click without preceding mousedown/mouseup | 0.02 each |
| mousedown/mouseup at identical timestamp | 0.03 |
| touchmove without preceding touchstart | 0.02 |
| keyup without preceding keydown | 0.02 |
Cross-signal analysis that catches automation frameworks even when individual signals look plausible.
| Check | Human | Bot | Penalty |
|---|---|---|---|
| Cross-signal fast dispatch | Normal dwell times | Both clicks AND keys <5ms | 0.10 |
| Single-channel fast | Normal dwell times | Clicks OR keys <5ms | 0.04 |
| Zero-time click pairs | mousedown/mouseup gap >0 | Identical timestamps (CDP dispatch) | 0.05 |
| Check | Human | Bot |
|---|---|---|
| Time to first interaction | >200ms (visual processing) | <50ms (pre-scripted) |
| Event density | Reasonable for session duration | >50 events in <500ms |
Final Score = 1.0 - sum(category penalties)
Each category has a maximum penalty cap:
Mouse: 0.30 Click: 0.15
Pre-click: 0.10 Keystrokes: 0.15
Scroll: 0.10 Touch: 0.10
Sensors: 0.10 Event order: 0.05
Synthetic: 0.15 Engagement: 0.05
-----------------
Maximum total: 1.15 (capped at 1.00)
Score >= 0.5 -> CLEARED (human) Token issued
Score < 0.5 -> BLOCKED (bot) No token
The threshold is configurable via scoreThreshold option.
| Option | Default | Description |
|---|---|---|
secretKey |
Random 32 bytes | Token signing secret |
scoreThreshold |
0.5 |
Minimum score to clear (0.0-1.0) |
debug |
false |
Include full analysis in response |
challengeTtl |
60000 |
Challenge expiry in milliseconds |
| Attack | Defense |
|---|---|
Selenium/Puppeteer moveTo() |
Produces straight lines with uniform velocity and zero curvature entropy |
| Bezier curve mouse libraries | Detectable by constant second derivative (too-smooth acceleration) |
| Recorded human replay | HMAC nonce prevents replay; timestamps won't match |
Synthetic dispatchEvent() |
Missing mousedown/mouseup sequence; zero click dwell |
| Headless touch simulation | Zero pressure, zero contact radius, no wobble |
| Emulated sensors | Zero noise floor, no cross-axis correlation |
Fast sendKeys() |
Flight times <15ms (physically impossible), zero dwell variance |
| WindMouse / ghost cursor | Autocorrelation, missing velocity minima, no movement pauses |
| Perlin/Catmull-Rom paths | Sub-pixel precision anomaly, periodic autocorrelation |
| CDP mouse dispatch | Zero-time mousedown/mouseup pairs, cross-signal fast dispatch |
The fundamental insight: human motor control is governed by biomechanical constraints (Fitts' Law, physiological tremor, speed-accuracy tradeoff) that produce characteristic statistical signatures in movement data. These signatures are involuntary and extremely difficult to replicate at the distribution level, even when individual data points can be faked.
Track click accuracy on specific interactive elements:
const collector = createCollector();
collector.attach();
collector.bind(submitBtn, 'submit');
collector.bind(checkbox, 'agree');
// getData() includes:
// bc: [[offsetX, offsetY, dwell, width, height, time, index], ...]
// bl: ['submit', 'agree']
collector.unbind(submitBtn);Bound element clicks are analyzed for center offset precision — bots click exact center, humans don't.
npm run example
# http://localhost:3002npm test # unit tests
npm run test:e2e # playwright e2e (10 bot algorithms)
npm run test:all # bothThe e2e suite runs 10 humanization algorithms through real browsers and verifies all are detected:
| Algorithm | Technique |
|---|---|
| Linear | Straight line with random jitter |
| Bezier | Cubic bezier curve interpolation |
| Sinusoidal | Sine wave path with variable amplitude |
| WindMouse | Ghost Mouse algorithm (wind + gravity model) |
| Overshoot | Target overshoot with correction |
| Perlin | Perlin-like noise displacement |
| Spring-Damper | Physics spring simulation |
| Gaussian Jitter | Gaussian noise on linear path |
| Catmull-Rom | Spline through random control points |
| Bell Velocity | Bell-curve speed profile |
All 10 score below 0.5 (blocked) in both unit tests and real browser e2e tests.
npx prtfm