Show HN: I Rebuilt Captcha with Jev A developer built an invisible CAPTCHA that replaces puzzles with behavioral analysis, using TypeSafe's Jev "System One" model to score mouse movement, typing rhythm and button presses on a waitlist form. In testing, the developer's own visits passed and a screen reader completed the check with nothing to click, while test scripts and two AI agents were blocked and Claude in Chrome refused to attempt it; a bot designed to imitate human behavior still got through. Each check takes about a third of a second and costs less than a hundredth of a cent, with Jev released in early access on 15 September 2026 and served via OpenRouter as typesafe/jev-1.13. In July 2025, a Reddit user shared screenshots of OpenAI's ChatGPT Agent meeting Cloudflare's "Verify you are human" checkbox. The agent clicked it↗ https://www.tomshardware.com/tech-industry/artificial-intelligence/chatgpt-agent-casually-brushes-aside-i-am-not-a-robot-captcha-so-now-ill-click-the-verify-you-are-human-checkbox-to-complete-this-verification-it-declared-without-a-hint-of-irony and narrated as it went: The link is inserted, so now I'll click the 'Verify you are human' checkbox to complete verification on Cloudflare. This step is necessary to prove I'm not a bot and proceed with the action. It's funny, and it's also the whole problem with puzzles and checkboxes. They test chores: tick a box, find the traffic lights, type the wobbly letters. Agents do chores now. So I built a CAPTCHA with no puzzle at all. It asks a different question. Not "can you do this?" but "how was this form filled in?" The first half is a Jev tutorial that builds it in TypeScript, with excerpts from the demo repo↗ https://github.com/LocalCan/invisible-captcha MIT . The second half is what happened when I pointed people and AI agents at it. Summary I built a sign-up form that distinguishes people from bots without asking anyone to solve a puzzle. Traditional CAPTCHAs ask you to click traffic lights, but AI agents can do that now. Instead, the form looks at how you fill it in: how you move your mouse, type, and press the button. My server turns those signals into a few plain sentences, which an AI model called Jev reads to decide whether you're human. You never write anything Jev sees, so you can't persuade it to let you through. Here's what happened: my own visits passed, and a screen reader completed a brief automatic check with nothing to click. Test scripts and two AI agents were blocked, while Claude in Chrome refused to try. A bot designed to imitate human behaviour still got through, so this is one layer of protection, not an impenetrable wall. Each check takes about a third of a second and costs less than a hundredth of a cent. The demo is a waitlist form with three fields: name, email and an optional "What are you building?". Nothing on the page asks you to prove anything. Behind it, three things happen: 1. The browser measures how the form gets filled in: how the pointer moved, how each field got focus, the rhythm of the typing, how the button was pressed. 2. The server turns those measurements into a short, plain-English story of the visit. 3. TypeSafe's Jev↗ https://typesafe.ai reads the story and answers one question: who is behind this session? Jev gives a probability to each of five situations, three human and two automated. The person score adds up the three human ones and the bot score the two automated ones, so together they make 1. The scores decide what happens: | Decision | When | What the visitor sees | |---|---|---| | Pass | person score 0.85 or higher | nothing: the form submits | | Challenge | anything in between, or when Jev fails | nothing to click: the browser solves a proof of work in a second or two | | Block | bot score 0.80 or higher | the form is blocked, and the page shows why | The page shows Jev's probabilities and the exact story it read, so you can see why. In production you would keep both on the server. TypeSafe released Jev↗ https://typesafe.ai/blog/introducing-system-one-models-and-jev on 15 September 2026, in early access. TypeSafe calls it a "System One" model: you send a state and some typed questions, and you get typed answers with probabilities. It doesn't generate text. Of its three question types↗ https://docs.typesafe.ai/introduction , a CAPTCHA needs one: a choice between a handful of options. TypeSafe's own console has a waitlist, but you don't need it. OpenRouter serves Jev↗ https://openrouter.ai/docs/guides/community/jev as typesafe/jev-1.13 to anyone with an OpenRouter key, and TypeSafe's official SDK switches over when you change the base URL. The whole client is five lines of config: jev.ts js import { choice, TypeSafeClient } from '@typesafe-ai/sdk' // Jev through OpenRouter. To call TypeSafe directly, set TYPESAFE API KEY and drop apiKey, // baseURL and defaultModel: the SDK then defaults to https://api.typesafe.ai and jev-latest. // Another gateway that serves Jev, such as Vercel AI Gateway, needs its own baseURL, key and model. export const client = new TypeSafeClient { apiKey: process.env.OPENROUTER API KEY, baseURL: 'https://openrouter.ai/api', defaultModel: 'typesafe/jev-1.13', timeout: 3 000, retry: { maxRetries: 1 }, } Vercel AI Gateway↗ https://vercel.com/changelog/ai-gateway-now-supports-typesafe-clients-and-http-api-for-jev takes the same SDK with its own base URL, key and model name, and Cloudflare Workers AI↗ https://developers.cloudflare.com/ai/models/typesafe/jev/ serves Jev as typesafe/jev through its own Workers AI API. The timeout and retry matter more than they look. A CAPTCHA sits in front of a sign-up, so every check also gets a hard 4-second budget. If Jev misses it, the visitor gets the proof of work, never a free pass. The tempting design is to hand Jev everything the raw events, the headers, maybe the form itself and let the model sort it out. TypeSafe's notes on Jev's known limits↗ https://docs.typesafe.ai/model-jaggedness/jev-1.13.md give two reasons not to. The first: Jev is not a calculator. We strongly recommend implementing any mathematical logic in code. Bot detection is mostly arithmetic: milliseconds between keys, how straight a mouse path is, how far a press landed from the centre of a button. So the code does the arithmetic and hands Jev the conclusions, in words. The second, from the section on adversarial content: State is data, and jev-1.13 does not treat it as hostile by default The state of a CAPTCHA comes from the one party you don't trust. If the visitor can write words that Jev reads, the visitor can argue with the judge. I tested that too: a polite note beat a direct order prompt-injection-a-polite-note-beats-a-direct-order . So the demo follows one rule: the visitor never writes a word Jev reads. Numbers become words from my own tables, the user agent becomes an enum, and the form text is never sent at all. Here is the path from the browser to Jev, and what each step lets through: | Step | Runs in | What it passes on | |---|---|---| | collector.js | browser | counts, durations and flags, never what was typed | | validateSignals | server | numbers, booleans and allowlisted words only | | requestFacts | server | the user agent and headers, reduced to enums and booleans | | writeStory | server | sentences built only from my own word tables | | judge | server | Jev sees { session: story } and nothing else | The collector is one plain JavaScript file with no dependencies. It listens to pointer, key, focus, paste, scroll and blur events, and on submit it returns a summary: - pointer moves, grouped into strokes, and how many were straight lines - how each field got focus: a click or tap, the Tab key or neither - which fields were typed, pasted into, autofilled or filled with no keys at all - the typing speed and how even the gaps between keys were - how the button was pressed: hover time, press length, distance from the centre - how long the visit took, and whether the browser reports automation The file's header states the contract, and one helper enforces it: public/collector.js // Measures how the form gets filled in. Only counts, durations, flags and a few fixed words leave // the page: snapshot returns that summary, never the raw events or anything the visitor typed. // ... // Only trusted events count, so a script cannot fake input by dispatching events of its own. const on = target, type, fn = target.addEventListener type, e = e.isTrusted && fn e , LISTEN A page script can dispatch its own mouse and key events, but they arrive with isTrusted set to false, so they count for nothing. The collector's comments double as a list of browser quirks: Chrome's autofill sends each field a trusted keydown with no key, Android keyboards report most keys as "Unidentified", and Windows reports AltGr as Ctrl+Alt. The server doesn't trust the summary either. On arrival, validateSignals builds a fresh object from known keys only, and the schema doubles as the allowlist: signals.ts // The schema is also the allowlist: a key not listed here never leaves validateSignals . const NUMBERS = { maxTouchPoints: COUNT, viewportW: DIM, // ... pressesWithoutMove: COUNT, } as const // ... function readNumber raw: Record