Behavioral analysis + proof-of-work CAPTCHA. Drop-in "I'm not a robot" protection for any site.
- No third-party services — runs entirely on your infrastructure
- Adaptive difficulty — real users solve in ~100ms, bots get punished with up to 20s of compute
- 18-point bot detection — mouse patterns, keystroke timing, automation flags, headless browser detection
- Cryptographically signed — HMAC-SHA256 challenges with expiry and replay prevention
npm install yourcaptcha# .env.local
YOURCAPTCHA_SECRET=your-secret-here
# Generate one: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"// app/api/captcha/challenge/route.ts
export { challengeHandler as POST } from "yourcaptcha/nextjs";// app/api/captcha/verify/route.ts
export { verifyHandler as POST } from "yourcaptcha/nextjs";"use client";
import { useState } from "react";
import { CaptchaWidget, useBehaviorCollector } from "yourcaptcha/client";
import type { CaptchaPayload } from "yourcaptcha/client";
export default function RegisterPage() {
const { getSignals } = useBehaviorCollector();
const [captcha, setCaptcha] = useState<CaptchaPayload | null>(null);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!captcha) return alert("Please complete the captcha");
const res = await fetch("/api/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ captcha, /* ...other form data */ }),
});
// handle response...
}
return (
<form onSubmit={handleSubmit}>
{/* your form fields */}
<CaptchaWidget onVerified={setCaptcha} getSignals={getSignals} />
<button type="submit" disabled={!captcha}>Register</button>
</form>
);
}// app/api/register/route.ts
import { verifyCaptcha } from "yourcaptcha/server";
export async function POST(request: Request) {
const { captcha, ...formData } = await request.json();
const result = await verifyCaptcha(captcha, {
hmacSecret: process.env.YOURCAPTCHA_SECRET!,
});
if (!result.verified) {
return Response.json({ error: result.reason }, { status: 400 });
}
// Captcha is valid — proceed with registration
}import express from "express";
import { createChallenge, verifyCaptcha } from "yourcaptcha/server";
const app = express();
app.use(express.json());
const HMAC_SECRET = process.env.YOURCAPTCHA_SECRET!;
app.post("/api/captcha/challenge", (req, res) => {
const result = createChallenge(
{ hmacSecret: HMAC_SECRET },
req.body.signals
);
res.json({
algorithm: result.algorithm,
challenge: result.challenge,
maxnumber: result.maxnumber,
salt: result.salt,
signature: result.signature,
});
});
app.post("/api/captcha/verify", async (req, res) => {
const result = await verifyCaptcha(req.body, { hmacSecret: HMAC_SECRET });
res.json(result);
});| Prop | Type | Default | Description |
|---|---|---|---|
onVerified |
(payload: CaptchaPayload) => void |
required | Called when challenge is solved |
getSignals |
() => BehavioralSignals |
required | From useBehaviorCollector() |
challengeUrl |
string |
"/api/captcha/challenge" |
Your challenge API endpoint |
brandText |
string |
"Protected by YourCaptcha" |
Branding text below checkbox |
className |
string |
— | CSS class for the outer button |
Generates a proof-of-work challenge with adaptive difficulty.
import { createChallenge } from "yourcaptcha/server";
const result = createChallenge({
hmacSecret: "your-secret",
expiryMs: 5 * 60 * 1000, // default: 5 minutes
defaultMaxNumber: 100_000, // default difficulty when no signals
}, signals);
// Returns: { algorithm, challenge, maxnumber, salt, signature, riskScore }Verifies a solved captcha payload with full cryptographic + behavioral validation.
import { verifyCaptcha } from "yourcaptcha/server";
const result = await verifyCaptcha(payload, {
hmacSecret: "your-secret",
maxRiskScore: 0.85, // reject if behavioral score exceeds this
maxScoreDrift: 0.4, // reject if signals were tampered
minSubmitTime: 3000, // minimum ms between page load and submit
checkReplay: async (challenge) => {
// Optional: check Redis/DB for replay attacks
// Return true if challenge was already used
const used = await redis.get(`captcha:${challenge}`);
if (used) return true;
await redis.set(`captcha:${challenge}`, "1", "EX", 300);
return false;
},
});
// Returns: { verified: boolean, reason?: string, riskScore?: number }Run the behavioral scoring engine standalone (useful for logging/monitoring).
import { scoreSignals } from "yourcaptcha/server";
const { score, reasons, maxnumber } = scoreSignals(signals);
console.log(`Risk score: ${score}, reasons:`, reasons);- User loads the page —
useBehaviorCollector()starts tracking mouse, keyboard, and environment signals - User clicks "I'm not a robot" — signals are sent to your challenge endpoint
- Server scores the signals and returns a SHA-256 proof-of-work challenge with adaptive difficulty
- Web Worker solves the challenge off-main-thread
- Solved payload is submitted with your form
- Server verifies: HMAC signature, proof-of-work solution, expiry, and re-scores behavioral signals
MIT