Behavioral captcha that catches bots through real-time ball-tracking analysis.
Users follow a ball moving unpredictably across a canvas for 8 seconds. The trajectory is generated server-side in real-time and streamed as rendered images — future positions never exist on the client. The server runs a deep multi-layered analysis of the cursor trace: per-frame tracking enforcement, velocity-curvature power law fitting, spectral timing analysis, jerk profiling, sub-movement segmentation, reaction time modeling, drift detection, and environment fingerprinting — signals that are extremely difficult for automated agents to replicate convincingly.
All verification runs server-side. The client never holds scoring logic, detection parameters, or signing secrets. Tokens are HMAC-SHA256 signed, single-use, and expire automatically.
Zero runtime dependencies across all packages.
- Real-time ball streaming — Ball trajectories are computed tick-by-tick on the server and streamed as rendered images via SSE. Future positions don't exist until generated. No video, no DOM elements, no extractable assets.
- Frame-level tracking enforcement — The client commits to its cursor position at the moment each frame is received. The server validates those commitments against the real ball positions it sent. Pre-computed cursor paths cannot pass.
- Fully server-side verification — All scoring, detection, and token signing run on your server. The browser is a thin input-capture layer with no access to scoring logic or detection parameters.
- Multi-layered behavioral analysis — 14 scoring signals including spectral timing analysis (DFT on inter-event intervals), velocity-curvature power law fitting, interval-regularity coefficient-of-variation, locally-detrended residual-noise analysis, jerk profiling, sub-movement segmentation, drift/bias detection, reaction time distribution modeling, and environment fingerprinting.
- Hard bot flags — Eleven independent hard flags trigger an immediate bot verdict that bypasses scoring: non-monotonic or resolution-locked timestamps,
navigator.webdriver, classic-textbook power-law fits (bot signature on this task), spectral periodicity in inter-event timing, mechanical timing with zero saccade pauses, near-zero post-detrend residual noise, inhumanly-tight cursor-to-ball precision, too-narrow distance distribution with full tight coverage, zero reaction time at direction changes, missing or clock-incoherent frame acknowledgments, and aggregate multi-signal suspicion. - Visual decoy camouflage — The rendered ball frame includes small independently-moving decoy shapes in the ball colour. Humans lock onto the larger coherent moving ball visually; naive centroid-extraction attacks (corner-background subtraction + colour-averaging) are materially biased, forcing adversaries to implement connected-component labelling + shape-specific filtering.
- HMAC-SHA256 tokens — Single-use, signed server-side, auto-expire after 5 minutes. Verified with one function call.
- Zero runtime dependencies — Server package uses only Node.js built-in
crypto. No native modules, no C++ bindings, no external services. - Framework-agnostic — Vanilla JS via script tag, ES modules, or the
@007captcha/reactcomponent. Works with any backend framework. - Light & dark themes — Built-in
'light','dark', and'auto'(follows system preference) themes. - TypeScript-first — Full type definitions shipped with every package.
pnpm add @007captcha/client @007captcha/serverimport express from 'express';
import { verify, BallChallengeManager } from '@007captcha/server';
const app = express();
const SECRET = process.env.CAPTCHA_SECRET || 'change-me';
const ball = new BallChallengeManager(SECRET);
app.use(express.json());
// Start a new session
app.post('/captcha/ball/start', (req, res) => {
res.json(ball.createSession());
});
// Stream ball frames as SSE
app.get('/captcha/ball/:id/stream', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
});
let done = false;
const ok = ball.startStreaming(
req.params.id,
(frame) => res.write(`event: frame\ndata: ${JSON.stringify(frame)}\n\n`),
() => { done = true; res.write('event: end\ndata: {}\n\n'); res.end(); },
);
if (!ok) { res.end(); return; }
req.on('close', () => { if (!done) ball.cancelSession(req.params.id); });
});
// Verify the submitted cursor trace and frame acks
app.post('/captcha/ball/:id/verify', (req, res) => {
const { points, cursorStartT, frameAcks, origin, clientEnv } = req.body;
const requestMeta = {
userAgent: req.headers['user-agent'],
acceptLanguage: req.headers['accept-language'],
};
res.json(ball.verify(
req.params.id,
points || [],
cursorStartT || 0,
frameAcks || [],
origin || '',
clientEnv,
requestMeta,
));
});
// Token verification
app.post('/verify', async (req, res) => {
res.json(await verify(req.body.token || '', SECRET));
});
app.listen(3007);<div id="captcha"></div>
<script src="https://unpkg.com/@007captcha/client/dist/umd/index.global.js"></script>
<script>
OOSevenCaptcha.render({
siteKey: 'change-me',
container: '#captcha',
serverUrl: window.location.origin,
onSuccess(token) {
fetch('/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token }),
});
},
});
</script>Or with ES modules:
import { render } from '@007captcha/client';
const widget = render({
siteKey: 'change-me',
container: '#captcha',
serverUrl: window.location.origin,
onSuccess: (token) => { /* send to server */ },
});pnpm add @007captcha/client @007captcha/reactimport { OOSevenCaptcha } from '@007captcha/react';
function App() {
return (
<OOSevenCaptcha
siteKey="change-me"
serverUrl={window.location.origin}
onSuccess={(token) => { /* send to server */ }}
/>
);
}After the challenge completes, the client receives a signed token. Send it to your backend and verify:
import { verify } from '@007captcha/server';
const result = await verify(token, SECRET);
if (result.success) {
// result.score — 0.0 (bot) to 1.0 (human)
// result.verdict — 'human', 'uncertain', or 'bot'
// result.method — 'ball'
}Tokens are single-use and expire after 5 minutes.
| Option | Type | Default | Description |
|---|---|---|---|
siteKey |
string |
required | Shared secret for HMAC token signing |
container |
string | HTMLElement |
required | CSS selector or DOM element to mount the widget |
serverUrl |
string |
required | Base URL for challenge endpoints |
theme |
'light' | 'dark' | 'auto' |
'light' |
Color theme |
timeLimit |
number |
14000 |
Time limit in ms |
onSuccess |
(token: string) => void |
— | Called when challenge passes |
onFailure |
(error: Error) => void |
— | Called when challenge fails |
onExpired |
() => void |
— | Called when token expires |
const widget = render({ ... });
widget.getToken() // Current verification token
widget.reset() // Reset for a new challenge
widget.destroy() // Remove widget from DOM| Package | Description |
|---|---|
@007captcha/client |
Browser widget — renders the ball, captures cursor input and frame acks, communicates with server |
@007captcha/server |
Node.js backend — session management, analysis, token signing & verification |
@007captcha/react |
React component wrapper |
- Server-side analysis — All scoring, detection, and token signing happen on the server. The client is a thin rendering layer that captures cursor input and sends it back along with per-frame commitments.
- Frame-level temporal binding — For every streamed ball frame, the client sends a
frameAckwith its cursor position at the moment the frame was received. The server checks that these commitments align with the real ball positions it sent, that the latency distribution looks like network jitter (not a constant replay offset), and that the committed positions match the main cursor trace. A pre-computed cursor path cannot satisfy all three constraints. - No client secrets — The browser never holds detection logic, scoring thresholds, or signing keys.
- Multi-signal behavioral analysis — Each challenge evaluates 14 independent signals: spectral timing analysis, velocity-curvature power law, interval-regularity CV, locally-detrended residual-noise analysis, jerk profiling, sub-movement segmentation, drift/bias detection, and more.
- Hard bot flags — Eleven hard flags bypass scoring and return an immediate bot verdict: spectral peak ratios above 8.0, non-monotonic/duplicate/resolution-locked timestamps,
navigator.webdriver === true, headless browser signatures, textbook 1/3 power-law fits (bots mimic the literature value; real humans on this task show β ≈ 0.65–1.05), mechanical inter-event timing with zero saccade pauses, near-zero residual noise after local detrending, frame-level tracking below 55%, inhumanly precise average distance, too-narrow cursor-to-ball distance distribution, missing or clock-incoherent frame acknowledgments, zero reaction time on ball-direction changes, and aggregate multi-signal suspicion. - Visual decoy camouflage — Each rendered frame includes small moving decoys in the ball colour. Naive centroid extraction (corner-background subtraction + all-non-bg-pixel averaging) becomes biased away from the real ball, forcing attackers to implement connected-component labelling and shape-specific filtering.
- Environment fingerprinting — Client-collected browser signals (webdriver, plugins, screen dimensions, touch support) combined with server-side HTTP header analysis (User-Agent, Accept-Language).
- Real-time streaming — Ball positions are computed tick-by-tick on the server and streamed as rendered images. Future positions don't exist until each frame is generated.
- HMAC-SHA256 tokens — Single-use, signed server-side, 5-minute expiry.
- Canvas rendering — No
<video>, no extractable DOM assets, no readable coordinates in the markup.
| Example | Description |
|---|---|
examples/express-server/ |
Express.js with the ball challenge, SSE streaming, and full verification |
examples/react-app/ |
Vite + React with server-side verification |
examples/vanilla-html/ |
Minimal HTML page with script tag |
pnpm install
pnpm demo
# → http://localhost:3007pnpm build
cd examples/react-app
pnpm install
pnpm dev
# → Vite on http://localhost:5173, API on http://localhost:3007pnpm install # Install dependencies
pnpm build # Build all packages
pnpm test # Run tests
pnpm test:watch # Watch mode
pnpm demo # Build + start demo server