Our sign-up and contact forms displayed a verification control. It was a div with a checkbox drawn in CSS and an 800ms spinner, followed by a tick. It verified nothing whatsoever.
Why it was worse than nothing
A bot does not render your page. It posts to the endpoint directly, and the endpoint never asked for proof of anything. The control existed only for humans, who were the ones already behaving. Meanwhile it created a confident impression internally that bot protection was handled.
reCAPTCHA v3, scored server-side
The forms now obtain a token from reCAPTCHA v3 and the server verifies it with Google, checking both the score and that the action matches the form that claims to have produced it. Below the threshold, the request is rejected. There is no puzzle for the user — v3 scores silently.
const result = await requireHuman(token, { expectedAction: "signup" });
if (!result.ok) {
return NextResponse.json({ error: result.error }, { status: result.status });
}Rate limiting underneath
Scoring is not a substitute for a limit. Auth and admin endpoints carry their own per-identity rate limits, so a credential-stuffing run is bounded regardless of how it scores.