Implementation Document: Kaptcha
📝 Markdown RenderedFor people wanting to make people mad.
Kaptcha - Implementation Document
Project: kaptcha
Artefact: <kaptcha-box> - a custom element implementing an adversarial, satirical, deliberately hostile CAPTCHA.
Document status: Normative. This document is the single source of truth for implementation.
Target implementer: A competent JavaScript engineer, or an agent capable of reading a specification and not inventing things.
0. Document control
0.1 Scope
This document specifies, completely and without omission:
- The interpretation of
prompt.mdinto engineering requirements. - The research base - what actually makes a CAPTCHA unbearable, drawn from the historical record, from academic HCI, and from the deployed state of the art.
- The architecture of the component.
- The complete public API surface.
- The Challenge Catalog - 84 distinct verification challenges, each fully specified.
- The Torture Engine - cross-cutting modifiers that can be applied to any challenge to increase suffering along measurable axes.
- Progression, loop mode, timing, telemetry, persistence.
- Responsive and touch behaviour.
- Ethics, legality, and the boundary conditions under which this artefact may be deployed.
- Build, verification, and acceptance criteria.
0.2 Requirement language
MUST, MUST NOT, SHOULD, SHOULD NOT, MAY follow RFC 2119 semantics. Anything marked MUST is an acceptance-test line item.
0.3 Deliverables
| File | Required | Purpose |
|---|---|---|
kaptcha.js |
Yes | The entire component. ES module. Single file. Zero dependencies. |
index.html |
Yes | Demonstration host page. Also the manual test harness. |
.env.json |
Yes | Central configuration defaults, dumpable by the component. |
Makefile |
Yes | serve, lint, check, dump-env, clean. |
README.md |
Yes | Professional, scientific, current. |
impl.md |
Yes | This document. |
docs/index.html |
Yes | Reference documentation page. Faceted challenge catalog, API reference, protocol schema, policy. |
docs/docs.css |
Yes | Documentation styles. Custom properties, container-aware, light and dark. |
docs/*.js |
Yes | Documentation page controller: Application, Navigation, Theme, TableFilter, Search, Clipboard. One class per file. |
tools/jscheck.py |
Yes | Structural verification of every script. Comment-, string-, template- and regex-aware. |
tools/htmlcheck.py |
Yes | Tag balance, anchor targets, asset resolution, duplicate ids, viewport zoom policy. |
tools/envdump.py |
Yes | Extracts and verifies .env.json against the component defaults. |
tools/selftest.py |
Yes | Enforces the acceptance criteria against the browser harness output. |
selftest.html |
Yes | Browser harness: sweeps every definition, plays a finite session and a hard-loop session. |
demo.js, controls.js, logger.js, metrics.js, demo.css |
Yes | The demonstration harness: Application, Controls, Logger, Metrics. One class per file. |
No build step. No bundler. No package manager. No transpiler. The file is served as-is and imported as-is.
0.3.1 Toolchain
The project MUST NOT require Node.js, npm, or any JavaScript ecosystem tooling - at any point, for any
purpose, including verification. The complete toolchain is python3, make, and a browser, all of which
are present on any system this project would run on.
This is a hard constraint, not a preference. A zero-dependency ES module and a static HTML page that cannot
be checked without installing a 200 MB runtime have failed at the thing they claim to be. The verification
tools in tools/ are therefore Python: they parse rather than execute, which also means make check is
safe to run against untrusted source and works with the network disconnected.
| Need | Node approach (rejected) | Actual approach |
|---|---|---|
| Syntax verification | node --check |
tools/jscheck.py - comment-, string-, template- and regex-aware brace and delimiter analysis, header, import resolution, module form, forbidden constructs. |
| Page verification | headless browser | tools/htmlcheck.py - tag balance, anchor targets, asset resolution, duplicate ids, viewport zoom policy. |
| Dumping defaults | node -e "β¦dumpDefaults()" |
tools/envdump.py - reads the DEFAULTS_JSON literal directly from source. See Β§4.7. |
| Serving | any dev server | python3 -m http.server |
0.4 Reserved port
The development server listens on 18473. This port is used consistently across Makefile, README.md, and all documentation. It MUST NOT be changed without updating all three.
1. Mandate
1.1 Source requirements
prompt.md states, verbatim:
- A html component that extends HTMLElement and is exposed the ecma module way.
- It should be a responsive captcha that works good on browser and phone.
- It should be single js file (with index.html).
- The captcha must be several different types and levels, as much possible.
- It must totally test the patience of the user.
- It should have html attributes to describe where to post progress to (optional) and identifier fields (like reference), also success url, loop-mode (never ending captcha), optional time in seconds per captcha.
- And last but not least, it must be the most annoying tests you can imagine.
1.2 Binding interpretation
| # | Requirement | Engineering translation | Section |
|---|---|---|---|
| 1 | Custom element, ESM | class KaptchaBox extends HTMLElement, customElements.define('kaptcha-box', KaptchaBox), export named + default. No Shadow DOM. |
Β§4 |
| 2 | Responsive, phone-capable | Container-query-driven layout, 320 px minimum viewport, pointer-type-adaptive challenges, safe-area aware, virtual-keyboard aware. | Β§11 |
| 3 | Single JS file | One kaptcha.js containing runtime, registry, catalog, styles, and i18n. Styles injected as a single <style> element. |
Β§4.2, Β§16 |
| 4 | Many types and levels | 84 challenges across 8 families, 5 difficulty tiers, weighted non-repeating selection, unbounded level count. | Β§9, Β§10 |
| 5 | Test patience | Formal Suffering Model with four measurable axes; every challenge scored against it. | Β§3, Β§8 |
| 6 | Attributes | 27 observed attributes. Progress POST protocol with envelope schema. | Β§5, Β§6 |
| 7 | Maximally annoying | The Torture Engine: 31 composable modifiers applicable to any challenge. | Β§8 |
1.3 Non-goals
This is explicitly not a security product. It provides no bot resistance worth the name: every challenge runs and is graded entirely client-side, and any adversary with a debugger defeats the whole system in under a minute. Nothing in this document should be read as a claim of security value. Kaptcha is an interaction-design artefact - a satire, a stress test, a demonstration piece, and a rather effective instrument for measuring human frustration tolerance.
If bot resistance is the goal, use a server-verified proof-of-work scheme or a behavioural signal system, and read Β§15 before doing anything else.
2. Research digest: the anatomy of annoyance
This section is the empirical basis of the Challenge Catalog. Every mechanic in Β§8 and Β§10 traces back to something here. Sources are listed in Β§21.
2.1 The scale of the problem
Cloudflare's 2021 estimate remains the canonical figure: an average CAPTCHA takes 32 seconds to complete; with roughly 4.6 billion internet users each encountering approximately one CAPTCHA every ten days, humanity spends on the order of 500 human-years per day proving it is human. This number is the moral centre of the project. Kaptcha exists to make that number visible by making a single user feel all 500 years personally.
2.2 Why the deployed state of the art is hated
The artificial fade. reCAPTCHA v2 fades a clicked tile out over several seconds and fades a replacement in, and Google has confirmed this is deliberate rather than a bug. Users describe sessions that never terminate. The mechanic is important because it inverts the normal contract of direct manipulation: the user acts, and the interface refuses to acknowledge the act for four seconds. This is the single most reproducible source of CAPTCHA rage in the wild.
Ambiguous object boundaries. Field reports converge on the same complaints: does the pole count as part of the traffic light? Does a bicycle clipped by one pixel at the tile edge count? Is the rider part of the motorcycle? Is a partially occluded object still an object? Grading is inconsistent between sessions, so the user cannot even learn the rule. Ambiguity that cannot be resolved by learning is the purest form of interface cruelty.
Distorted glyphs. The original scheme. Its failure mode is character confusion - l/I/1, 0/O, rn/m, vv/w - compounded by case sensitivity that is rarely announced. Early reCAPTCHA occasionally served glyphs users could not type at all.
Audio alternatives do not rescue anyone. The Lazar et al. study at Towson University measured a 46 % failure rate on audio CAPTCHAs among blind users. In WebAIM's screen-reader surveys, CAPTCHA has repeatedly ranked as the single most problematic element on the web, ahead of ambiguous links, missing alt text, and keyboard inaccessibility. The W3C has published on the inaccessibility of CAPTCHA for over a decade.
2.3 The novelty and joke tradition
Neal Agarwal's I'm Not a Robot (neal.fun) is the direct artistic predecessor: 48 escalating levels of CAPTCHA satire. Documented levels include checking a box; selecting the four tiles that complete a stop sign; typing a six-character wobbling string with case sensitivity; identifying vegetables; rotating tiles until roads connect; entering text in the dark; finding a specific cat in a panorama; selecting the items that have a soul; ordering mathematical expressions from least to greatest; a reverse Turing test; and finally a message from the inventor of the CAPTCHA followed by a downloadable certificate of humanity. The escalation structure, the certificate, and the "items with a soul" question are all adopted here, in spirit, with original implementations.
Doom CAPTCHA (Guillermo Rauch, 2024; an earlier variant by Camps Orteza in 2021) requires the user to kill three demons on Nightmare difficulty with no mouselook. It demonstrates that arbitrary skill gates can masquerade as verification.
ASIRRA (Microsoft Research with Petfinder) asked users to separate cats from dogs; KittenAuth did something similar with kittens. Both rested on the assumption that fine-grained animal classification was hard for machines. That assumption did not survive.
Elie Bursztein's survey of unusual schemes documents the pathologies of the genre: math CAPTCHAs ("obviously terrible for humans"), geometric-selection CAPTCHAs with a 1-in-6 guess rate and a 30-second limit, drag-and-drop CAPTCHAs with a fixed learnable element set and a 20 % random success rate, and "Hot or Not" CAPTCHAs that asked the user to rate strangers' attractiveness as a condition of access. The last of these is a reminder that the genre has always been willing to make the user complicit in something distasteful.
Field-reported absurdities include a CAPTCHA reading "click the button if you are feeling lucky", one asking the user to select images of things that have a soul, and a navigation puzzle requiring a train to be routed to coordinates named "23 Spatula", "22 No Planes Allowed", and "20 Pegasus-Eagleish-Bird", some of which were impossible to complete.
2.4 The commercial state of the art
Arkose Labs FunCaptcha / MatchKey presents rendered 3D objects that must be rotated to an upright or matching orientation using arrow controls, plus logic riddles and audio quizzes. Rotation challenges are notable because they impose a spatial reasoning cost on top of the perception cost, and because arrow-button rotation is deliberately low-gain - many clicks per solve.
GeeTest popularised the slider puzzle: drag a jigsaw piece horizontally until it seats in a notch. This is a Fitts's Law tax with a tight tolerance, and it is the most common CAPTCHA form in Asian markets.
Proof-of-work and invisible schemes - ALTCHA, Friendly Captcha, honeypot fields, behavioural biometrics sampling pointer trajectory, touch geometry, keystroke dynamics and scroll patterns - represent the industry's attempt to escape the user-hostility trap. Kaptcha implements a satirical proof-of-work challenge (Β§10.G.7) in which the user performs the work personally.
2.5 The dark-pattern literature
Harry Brignull named the field in 2010 with twelve patterns: friend spam, forced continuity, disguised ads, confirmshaming, bait and switch, hidden costs, roach motel, privacy zuckering, misdirection, price comparison prevention, trick questions, sneak into basket. Gray et al. (CHI 2018) refactored these into five higher-order strategies, and this is the vocabulary regulators now use:
| Strategy | Definition | Kaptcha usage |
|---|---|---|
| Nagging | Repeated, persistent interruption with requests the user did not ask for. | Loop mode; "one more round to be sure"; the queue that grows. |
| Obstruction | Making the user's actual objective unnecessarily hard to reach. | Every challenge. The roach motel is the entire product. |
| Sneaking | Hiding or delaying information material to the decision. | Undisclosed case sensitivity; rules revealed only after failure; hidden clause 14.3. |
| Interface Interference | Manipulating the visual hierarchy to privilege the wrong action. | Decoy verify buttons; drifting targets; the 8-pixel checkbox. |
| Forced Action | Requiring an unrelated action to obtain the desired one. | Cookie consent challenge; EULA signature; window resizing. |
User Inyerface (2019) is the purest available demonstration of these in combination: only one element on the screen actually works, hidden instructions, misleading affordances, a countdown that induces panic, and a punitive lock-and-restart on timeout. Its "Punitive UX" - penalising the user for reading carefully - is adopted directly by several challenges here.
2.6 The psychophysics of waiting
Nielsen's three thresholds define the perception of responsiveness:
- 0.1 s - the limit for the illusion of direct manipulation.
- 1.0 s - the limit for uninterrupted flow of thought. Between 0.1 s and 1 s the delay is noticed but accepted.
- 10 s - the limit for holding attention on the task at all. Beyond it, the user mentally leaves.
The Doherty Threshold (Doherty & Thadhani, 1982) found that sub-400 ms response times raise productivity by 25-30 %.
Kaptcha therefore operates a Delay Ladder (Β§8.2): feedback delays are tuned to sit deliberately in the worst band for each purpose - 1.2 s to break flow without releasing attention; 4.2 s to force a decision about whether the interface is broken; 11 s to push the user past the attention limit while a progress indicator implies imminent completion.
2.7 Why the user does not simply leave
This is the mechanism that makes patience-testing possible at all, and it must be engineered deliberately.
- Sunk cost. Effort already spent is treated as a reason to continue. Every completed challenge raises the cost of abandonment.
- Goal gradient (Hull, 1932; Kivetz, Urminsky & Zheng, 2006). Effort accelerates as the perceived goal nears. A progress bar at 94 % is far more coercive than one at 12 %.
- Zeigarnik effect. Incomplete tasks occupy memory and generate return pressure.
- Variable-ratio reinforcement. Unpredictable reward schedules produce the most persistent behaviour known in behavioural psychology. A CAPTCHA that sometimes passes on the first try and sometimes on the ninth is stickier than one that always takes five.
Kaptcha uses all four. The progress indicator is the primary instrument: it MUST always suggest that completion is near, and it MAY regress (Β§8.3.11). The regression is what converts a merely tedious product into a genuinely memorable one.
2.8 The motor-control tax on mobile
- Platform guidance sets minimum touch targets at 44 pt (iOS) and 48 dp (Android). A fingertip covers roughly 10 mm and lands 1-2 mm off intended target about half the time.
- Fitts's Law: acquisition time is a function of distance and target size. On a phone the operative distance is the biomechanical arc of the thumb, not the pixel distance - an element in the top-right of a 6.7-inch display is close on screen and far in the hand.
- Precision interactions (drag, long-press, multi-touch) should be confined to the easy thumb zone. Kaptcha places them in the hard zone.
- The virtual keyboard occupies 35-50 % of the viewport and routinely covers the input it was summoned by.
Every mobile-hostile pattern in this list is implemented deliberately in Β§10.C and Β§11.4.
2.9 Summary: the four axes of suffering
Everything above reduces to four measurable axes. The Suffering Model (Β§3.2) scores every challenge on each from 0 to 5.
| Axis | Symbol | Definition |
|---|---|---|
| Duration | D |
Wall-clock time the challenge consumes, including imposed delay. |
| Precision | P |
Motor or perceptual accuracy demanded relative to human capability. |
| Ambiguity | A |
Degree to which the correct answer cannot be determined from the instructions. |
| Indignity | I |
Degree to which the challenge insults, patronises, or wastes the user knowingly. |
A challenge's Cruelty Index is D + P + A + I, range 0-20.
3. Design principles
3.1 The Ten Laws of Cruelty
These are binding on every challenge implementation. A challenge that violates them is not merely off-brand; it is a defect.
- The task MUST be trivially describable and non-trivially performable. "Click the green square" is a good instruction. The green square is 9 pixels wide and moving.
- The user MUST always believe they are close to finishing. Progress display is never honest downward except when regression is deliberately applied.
- Failure MUST be attributed to the user. Error copy never says "we could not verify"; it says "verification failed". Passive constructions are forbidden.
- No rule MUST ever be stated before it can be violated. Case sensitivity, tolerance bands, and click budgets are disclosed only in failure messages.
- Success MUST be provisional. Any pass MAY be followed by "one more round to be sure" at the configured probability.
- Delay MUST be applied to acknowledgement, not to computation. The system knows the answer instantly. It withholds the verdict.
- The interface MUST NOT lie in a way that can be proven. Everything is technically true. "Almost done" is not a falsifiable claim.
- Escape MUST always be available and never be attractive. There is a way out. It is one small link, it is worded to induce shame, and it works instantly. Cruelty without an exit is coercion; cruelty with an exit is satire.
- Nothing MUST cause physical harm. No unmitigated flashing above 3 Hz, no sudden loud audio, no seizure risk, no motion sickness triggers without a reduced-motion escape. See Β§15.
- The joke MUST remain visible. The user must be able to tell this is deliberate. A user who believes the site is simply broken is not being entertained; they are being defrauded of their time.
3.2 The Suffering Model
Every entry in the Challenge Catalog carries a header block:
id: slider-notch
family: C - Precision
tier: 3
D:3 P:5 A:1 I:2 β Cruelty 11
pointer: any
requires: none
The scheduler (Β§9.4) uses the Cruelty Index to build the difficulty curve. It MUST NOT present a tier-5 challenge as the first challenge of a session, and it MUST NOT present three consecutive challenges from the same family.
3.3 Tone
All user-facing copy is in flat, corporate, faintly bureaucratic English. It never winks. It never uses exclamation marks except in the deliberately over-eager "Almost done!" string. The humour derives entirely from the contrast between the banality of the language and the absurdity of the demand.
Forbidden in user-facing copy: emoji, exclamatory enthusiasm, self-aware jokes, the words "fun", "quick", "easy", "just", or "simply".
Mandatory phrasings:
| Situation | String |
|---|---|
| Pass | Verification step complete. |
| Fail | Verification failed. Please try again. |
| Timeout | Time expired. This attempt has been discarded. |
| Extra round | One more round to be sure. |
| Progress | Almost done. |
| Loop-mode completion | Verification is proceeding normally. |
| Escape link | I am unable to continue. |
4. Architecture
4.1 Module shape
kaptcha.js is an ES module with these exports:
export { KaptchaBox }
export { ChallengeRegistry }
export { CHALLENGES }
export { DEFAULTS }
export { Rng }
export default KaptchaBox
Importing the module registers the element as a side effect, guarded against double registration:
if (!customElements.get('kaptcha-box')) customElements.define('kaptcha-box', KaptchaBox)
Host usage:
<script type="module" src="./kaptcha.js"></script>
<kaptcha-box reference="order-4471" success-url="/welcome" levels="12"></kaptcha-box>
4.2 Internal layout of the single file
The file is one file, but it is not one blob. It MUST be organised into labelled regions in this exact order. Region boundaries are marked with a banner line of the form // ββ REGION NAME βββββββββββ. These banners are structural markers, not explanatory comments, and are the only permitted non-code lines in the source.
| # | Region | Contents |
|---|---|---|
| 1 | HEADER | // retoor <retoor@molodetz.nl> as line 1. |
| 2 | DEFAULTS | DEFAULTS_JSON and the parsed DEFAULTS object. See Β§4.7. |
| 3 | STYLES | The complete stylesheet as a template literal. |
| 4 | STRINGS | All user-facing copy, keyed. |
| 5 | UTIL | Rng, clamp, wait, uuid, shuffle, dist, easing, escapeHtml. |
| 6 | DOM | el(), frag(), on(), and the Ui helper factory. |
| 7 | AUDIO | AudioKit - lazily constructed AudioContext tone/noise/speech synthesis. |
| 8 | ART | Doodler - the canvas primitive library that draws every "image" used by the grid families. No external assets exist. |
| 9 | TORTURE | The 31 modifiers of Β§8. |
| 10 | REGISTRY | ChallengeRegistry, definition validation. |
| 11 | CATALOG | The 84 challenge definitions of Β§10, in family order. |
| 12 | SCHEDULER | Tier curve, weighting, anti-repeat, loop policy. |
| 13 | TRANSPORT | Progress POST, retry queue, sendBeacon fallback. |
| 14 | STORE | localStorage persistence and resume. |
| 15 | ELEMENT | KaptchaBox itself. |
| 16 | REGISTER | customElements.define guard and exports. |
Estimated size: 4,500-6,000 lines. This is acceptable. The prompt requires a single file; Β§16 records the deviation from the house one-class-per-file rule.
4.3 No Shadow DOM
Per house rules, Shadow DOM MUST NOT be used. Consequences that MUST be handled:
- All styles are namespaced under the
.kaptcharoot class and every selector is prefixed. No bare element selectors. No global resets. - A single
<style id="kaptcha-styles">element is injected intodocument.headon first element construction, and only once, guarded by ID lookup. - All internal class names use the
k-prefix (k-stage,k-tile,k-verify). - All CSS custom properties use the
--k-prefix. - The component MUST tolerate hostile host styles: it sets
box-sizing,line-height,font-family, andcolorexplicitly on.kaptchaand usesall: reverton no element.
4.4 Class inventory
| Class | Responsibility |
|---|---|
KaptchaBox |
The custom element. Attribute reflection, lifecycle, session orchestration. |
Session |
One run: level index, score, timings, seed, history, loop state. |
Scheduler |
Chooses the next challenge given tier, history, allowlist, cruelty. |
Challenge |
Runtime wrapper around a definition: mount, timer, attempt counting, teardown. |
ChallengeRegistry |
Registration, lookup, filtering, validation. |
Torture |
Applies modifiers to a live challenge root. |
Ui |
DOM construction helpers scoped to a challenge root. |
Doodler |
Deterministic procedural canvas art. |
AudioKit |
Tone, noise, beep-sequence, and pseudo-speech synthesis. |
Transport |
Progress delivery with retry and beacon fallback. |
Store |
Persistence and resume. |
Rng |
Seeded mulberry32 PRNG. |
Application |
Demo-page orchestrator in index.html, exposed as window.app. |
4.5 Determinism
All randomness MUST flow through a Rng instance seeded from the seed attribute, or from a random seed recorded in the session. Math.random MUST NOT appear anywhere in kaptcha.js. This makes every session reproducible from (seed, levels, difficulty, cruelty), which is required for the acceptance tests in Β§18.
class Rng {
constructor(seed) { this.s = seed >>> 0 }
next() {
this.s = (this.s + 0x6D2B79F5) >>> 0
let t = this.s
t = Math.imul(t ^ (t >>> 15), t | 1)
t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
}
int(n) { return Math.floor(this.next() * n) }
range(a, b) { return a + this.next() * (b - a) }
pick(arr) { return arr[this.int(arr.length)] }
shuffle(arr) { β¦ Fisher-Yates using this.int β¦ }
chance(p) { return this.next() < p }
}
4.6 No assets
There are no images, fonts, audio files, or network resources. Every visual is drawn on <canvas> or composed from CSS. Every sound is synthesised through Web Audio. The component MUST function with the network disconnected, save for progress POSTs, which fail silently into the retry queue.
4.7 Defaults as a JSON literal
The DEFAULTS region MUST take exactly this form:
const DEFAULTS_JSON = String.raw`
{
"server": { "host": "127.0.0.1", "port": 18473 },
β¦
}
`
const DEFAULTS = Object.freeze(JSON.parse(DEFAULTS_JSON))
The configuration is authored as JSON inside the source, not as a JavaScript object literal. Three
consequences, all deliberate:
KaptchaBox.dumpDefaults()returnsDEFAULTS_JSONverbatim. It is not a serialisation of an object, so
it cannot drift from what the component actually uses, and key order is preserved.tools/envdump.pyextracts the same literal with a single regular expression and writes.env.json
without executing anything. This is what removes the Node dependency frommake dump-env.make check-envcompares the parsed literal against.env.jsonand reports drift. The two files cannot
silently diverge.
The literal MUST be valid JSON - no comments, no trailing commas, no unquoted keys - and make check-env
MUST fail if it is not.
5. Public API
5.1 Attributes
All attributes are observed and live: changing one mid-session applies at the next challenge boundary unless noted otherwise. Every attribute has a property mirror in camelCase.
5.1.1 Identity and routing
| Attribute | Type | Default | Behaviour |
|---|---|---|---|
reference |
string β€ 128 | "" |
Opaque caller identifier. Echoed in every progress envelope. Sanitised: control characters stripped. |
session-id |
string β€ 64 | generated | Session identifier. If absent, a UUID v4 is generated and reflected back onto the attribute. |
subject |
string β€ 128 | "" |
Secondary identifier, e.g. the user or form being gated. Echoed in the envelope. |
progress-url |
URL | "" |
Optional. If set, progress envelopes are POSTed here. Empty disables telemetry entirely. MUST be same-origin or CORS-permitted; failures are non-fatal. |
progress-headers |
JSON object | {} |
Extra request headers. Parse failures are logged to console and treated as {}. |
progress-events |
csv | all |
Which events to transmit. One or more of start,challenge,attempt,pass,fail,timeout,progress,complete,abandon,despair, or all, or none. |
progress-interval |
int ms | 0 |
Minimum interval between transmissions. Events arriving inside the window are coalesced into a batch. 0 sends immediately. |
success-url |
URL | "" |
Navigated to on completion. Empty means no navigation; the element renders its completion state and fires kaptcha:complete. |
success-method |
enum | assign |
assign | replace | post | none. post submits a generated form with reference, session-id, and token to success-url. |
token-field |
string | kaptcha_token |
Field name for the completion token in success-method="post". |
5.1.2 Session shape
| Attribute | Type | Default | Behaviour |
|---|---|---|---|
levels |
int 1-999 | 12 |
Number of challenges to pass. Ignored in loop-mode="hard". |
difficulty |
enum | standard |
mild | standard | cruel | inhumane | catastrophic. Sets the starting tier and the tier ramp rate. |
cruelty |
int 0-100 | 55 |
Global modifier intensity. Scales delay lengths, tolerance bands, regression probability, and extra-round probability. See Β§8.1. |
time-limit |
int seconds | 0 |
Per-challenge limit. 0 = untimed. Applies to the challenge only, not to imposed delays. |
time-limit-jitter |
int percent | 0 |
Randomises each challenge's effective limit by Β±N %. Displayed countdown reflects the jittered value, so two identical challenges have different limits. |
attempts |
int 1-99 | 3 |
Attempts per challenge before the challenge is failed. |
fail-policy |
enum | retry |
retry (new instance of the same type) | replace (new type) | regress (lose one level) | restart (session restarts, all progress lost). |
loop-mode |
enum | off |
off | soft | hard. See Β§14. |
types |
csv | "" |
Allowlist of challenge ids or family letters. Empty = all. |
exclude |
csv | "" |
Denylist, applied after the allowlist. |
seed |
int | random | PRNG seed. Reflected to the attribute if generated. |
autostart |
bool | true |
Whether to begin on connect. If false, start() must be called. |
resume |
bool | false |
Restore an interrupted session from storage on connect. |
storage-key |
string | kaptcha |
localStorage key prefix. |
5.1.3 Presentation and safety
| Attribute | Type | Default | Behaviour |
|---|---|---|---|
theme |
enum | auto |
auto | light | dark. |
lang |
enum | en |
Only en ships. The strings table is structured for expansion. |
heading |
string β€ 80 | Verification required |
Panel heading. |
show-progress |
enum | bar |
bar | steps | count | none. |
show-timer |
bool | true |
Whether the countdown is visible. When false the limit still applies. |
sound |
bool | true |
Master switch for all audio. Audio-dependent challenges are excluded when false. |
photosensitive-safe |
bool | false |
When true, excludes flicker challenges and caps all animation frequency at 2.5 Hz. Forced true if prefers-reduced-motion: reduce. |
motion |
enum | auto |
auto | full | reduced. auto follows prefers-reduced-motion. |
allow-escape |
bool | true |
Renders the escape link. Law 8 requires this to default true. |
escape-url |
URL | "" |
Where the escape link goes. Empty fires kaptcha:abandon and renders a terminal state. |
certificate |
bool | true |
Whether to offer a downloadable certificate of humanity on completion. |
debug |
bool | false |
Console instrumentation, a visible challenge id, and a Ctrl+Shift+K force-pass. Never enable in a deployment. |
5.2 Properties
Every attribute has a mirrored property with type coercion. Additionally:
| Property | Type | Access | Meaning |
|---|---|---|---|
state |
string | read | idle | running | paused | delaying | complete | abandoned. |
level |
int | read | 1-based index of the current challenge. |
total |
int | read | levels, or Infinity in hard loop mode. |
elapsedMs |
int | read | Total session wall-clock. |
sufferingMs |
int | read | Wall-clock minus imposed delay - time the user was actually working. |
history |
array | read | Frozen array of per-challenge records. |
current |
object | read | { id, family, tier, cruelty, attempt, deadlineAt } or null. |
5.3 Methods
| Method | Returns | Behaviour |
|---|---|---|
start() |
Promise<void> |
Begins a session. No-op if already running. |
pause() |
void |
Freezes timers and animation. Does not stop imposed delays already in flight. |
resume() |
void |
Unfreezes. |
reset() |
void |
Destroys session state, clears storage, returns to idle. |
skip() |
boolean |
Advances one level. Returns false unless debug is set. |
abandon() |
void |
Terminal abandonment. Fires kaptcha:abandon, follows escape-url. |
exportSession() |
object |
Full session record, including per-challenge timings and attempt logs. |
KaptchaBox.dumpDefaults() |
string |
Static. Returns the default .env.json content as formatted JSON. |
KaptchaBox.register(def) |
void |
Static. Registers a custom challenge definition. Validates against Β§7.4. |
KaptchaBox.list() |
array |
Static. All registered challenge ids with family, tier, and cruelty index. |
5.4 Events
All events are CustomEvent, bubbles: true, composed: true, named with the kaptcha: prefix. detail always contains { sessionId, reference, subject, level, total, ts } plus the listed fields.
| Event | Extra detail | Cancelable |
|---|---|---|
kaptcha:ready |
{ challengeCount } |
no |
kaptcha:start |
{ seed, difficulty, cruelty } |
no |
kaptcha:challenge |
{ id, family, tier, timeLimitMs, modifiers[] } |
yes - preventDefault() re-rolls the challenge |
kaptcha:attempt |
{ id, attempt, ok, detail } |
no |
kaptcha:pass |
{ id, attempts, durationMs, sufferingMs } |
no |
kaptcha:fail |
{ id, attempts, reason } |
no |
kaptcha:timeout |
{ id, limitMs } |
no |
kaptcha:progress |
{ fraction, displayedFraction } |
no |
kaptcha:regress |
{ from, to, reason } |
no |
kaptcha:despair |
{ signal, value } |
no |
kaptcha:complete |
{ token, elapsedMs, sufferingMs, attempts, history } |
yes - preventDefault() suppresses success-url navigation |
kaptcha:abandon |
{ level, elapsedMs, reason } |
no |
fraction is the true progress. displayedFraction is what the user sees, which is subject to Β§8.3.11. The gap between them is the most honest metric this component produces.
5.5 The despair signals
kaptcha:despair fires when heuristics indicate the user is losing composure. These are genuinely useful for research and MUST be implemented.
| Signal | Threshold |
|---|---|
rage-click |
β₯ 5 pointer-downs within 1,200 ms inside a 40 px radius. |
thrash |
Pointer path length > 4,000 px within 3 s with no successful interaction. |
abandonment-hover |
Pointer exits the viewport top edge, a classic exit-intent signal. |
keyboard-mash |
β₯ 12 keydowns in 1,500 ms with entropy below 2.0 bits/char. |
stall |
No input event for 25 s while a challenge is active. |
tab-flight |
visibilitychange to hidden β₯ 3 times in one challenge. |
retry-spiral |
Same challenge id failed β₯ 4 times in a session. |
6. Progress protocol
6.1 Envelope
Content-Type: application/json. One envelope per transmission; batching wraps multiple events in events[].
{
"v": 1,
"sessionId": "6f1aβ¦",
"reference": "order-4471",
"subject": "u***@example.com",
"seed": 918273645,
"difficulty": "cruel",
"cruelty": 70,
"loopMode": "soft",
"sentAt": "2026-08-03T21:14:52.113Z",
"client": {
"ua": "β¦",
"viewport": [412, 915],
"dpr": 2.625,
"pointer": "coarse",
"reducedMotion": false,
"lang": "en-GB",
"tz": "Europe/Amsterdam"
},
"session": {
"level": 7,
"total": 12,
"fraction": 0.5833,
"displayedFraction": 0.91,
"elapsedMs": 412903,
"sufferingMs": 288140,
"attemptsTotal": 19,
"failuresTotal": 12,
"regressions": 2
},
"events": [
{
"type": "fail",
"ts": "2026-08-03T21:14:51.998Z",
"challenge": {
"id": "slider-notch",
"family": "C",
"tier": 3,
"cruelty": 11,
"modifiers": ["drift", "delay-4200", "tolerance-half"]
},
"attempt": 3,
"durationMs": 41022,
"reason": "tolerance",
"detail": { "offsetPx": 4.7, "tolerancePx": 2 }
}
]
}
6.2 Transport rules
MUSTusefetchwithkeepalive: truefor normal events.MUSTusenavigator.sendBeacononpagehideand onvisibilitychangetohidden, flushing the queue.MUST NOTblock the UI on a transmission. All sends are fire-and-forget.MUSTretry failed sends with exponential backoff: 1 s, 2 s, 4 s, 8 s, 16 s, then drop. Maximum queue depth 64 envelopes; oldest dropped first.MUSTbe a no-op whenprogress-urlis empty. No network call, no queue, no storage.MUST NOTtransmit anything not listed in Β§6.1. In particular no keystroke content, no pointer traces, no form values.SHOULDincludecredentials: 'omit'unlessprogress-headersincludes anAuthorizationheader, in which casecredentials: 'same-origin'.
6.3 Completion token
On completion the component generates a token to hand to success-url:
kap_<base64url(sessionId)>.<base64url(elapsedMs:attemptsTotal:seed)>.<crc32 hex>
This is a receipt, not a credential. It is unsigned and trivially forged. Β§15 requires that this be stated in README.md.
7. Lifecycle and state machine
7.1 States
idle ββstart()βββΆ preparing βββΆ presenting βββΆ grading βββΆ delaying βββ
β² β² β
β βββββββββ retry βββββββββββββββββββ€
β β
βββ reset() βββ complete βββ advancing ββββββββββββββββββββββββββββββ
β
abandoned βββ abandon() ββ (any state)
β
paused ββββΆ (presenting only)
7.2 Per-challenge sequence
- Select - the scheduler picks an id (Β§9.4).
kaptcha:challengefires and MAY be cancelled to re-roll, up to 8 re-rolls before the choice is forced. - Compose - modifiers are chosen (Β§8.1) and recorded.
- Mount - the definition's
mount(ctx)builds DOM into a fresh.k-stage. - Reveal - a mandatory reveal animation runs for
revealMs(default 240 ms, or up to 4,200 ms with theslow-revealmodifier). - Arm - the timer starts, if any. Input becomes live.
- Attempt - the user submits.
validate(ctx)returns{ ok, reason, detail }synchronously or as a promise. - Withhold - the verdict is delayed by the Delay Ladder value (Β§8.2). During this the UI shows a spinner and the verify control is disabled. The verdict is already known.
- Verdict - pass or fail is rendered,
kaptcha:attemptfires, and on passkaptcha:pass. - Extra round - with probability
P_extra(Β§8.1) a pass is converted into "One more round to be sure." and the sequence restarts at step 1 with the same id and a re-rolled instance. This does not advance the level. - Teardown -
teardown(ctx)runs. All timers, RAF handles, audio nodes, and listeners registered throughctx.on()are released. Leak-freedom is an acceptance criterion. - Advance - level increments, progress is recomputed and displayed per Β§8.3.11.
7.3 Cleanup contract
ctx provides on(), raf(), timer(), and audio() registration wrappers. Anything registered through them is auto-released at teardown. A definition that registers a listener directly on window or document without ctx.on() is defective.
Acceptance test: run 200 challenges in a loop with debug force-pass, then assert that performance.memory growth (where available) is under 8 MB and that document has no k- prefixed listeners remaining.
7.4 Challenge definition interface
{
id: 'slider-notch',
family: 'C',
tier: 3,
weight: 1.0,
cruelty: { d: 3, p: 5, a: 1, i: 2 },
pointer: 'any',
requires: [],
label: 'Fit the piece into the gap.',
compose: (ctx) => instance,
mount: (ctx, instance) => void,
validate: (ctx, instance) => ({ ok, reason, detail }),
hint: (ctx, instance, attempt) => string | null,
teardown: (ctx, instance) => void
}
pointer:any|fine|coarse- restricts by input modality. Afine-only challenge is skipped on touch devices and vice versa; both variants MUST exist for every concept where the interaction differs.requires: subset ofaudio,motion,orientation,clipboard,storage,vibrate. Unsupported requirements exclude the challenge from selection at runtime, silently.hintis called after each failed attempt. Hints are true, unhelpful, and delivered late - this is where undisclosed rules are finally disclosed (Law 4).
8. The Torture Engine
8.1 Intensity scaling
cruelty (0-100) is the master dial. Derived quantities:
k = cruelty / 100
P_extra = 0.05 + 0.35 k probability a pass triggers an extra round
P_regress = 0.00 + 0.22 k probability progress visibly regresses on failure
P_falsefail = 0.00 + 0.06 k probability a correct answer is graded wrong, once per session maximum
delayScale = 0.5 + 2.5 k multiplier on the Delay Ladder
tolerance = 1.0 - 0.65 k multiplier on all precision tolerances
modifierCount = round(0.5 + 3.5 k) modifiers applied per challenge
displayedLead = 0.08 + 0.22 k how far ahead of reality the progress bar runs
P_falsefail is the most ethically loaded parameter in the system. It MUST be capped at one occurrence per session, it MUST be recorded in the history as falseNegative: true, and it MUST be disclosed in README.md. A system that lies about correctness without bound is not a satire, it is sabotage.
8.2 The Delay Ladder
Delays are drawn from the psychophysics of Β§2.6. delayScale multiplies all values.
| Rung | Base ms | Purpose |
|---|---|---|
instant |
0 | Baseline. Used in mild only. |
perceptible |
320 | Past the Doherty threshold. Felt, not resented. |
flow-break |
1,200 | Past Nielsen's 1 s. Breaks the thought, holds attention. |
doubt |
4,200 | The reCAPTCHA fade band. Long enough to wonder if it is broken. |
attention-loss |
11,000 | Past Nielsen's 10 s. The user mentally leaves and must return. |
insult |
23,000 | Only in catastrophic. Accompanied by a progress bar that reaches 97 %. |
Rules:
- Verdict withholding uses
doubtby default. - Tile replacement in family B uses
doubtper tile, staggered, never concurrent. - The first challenge of a session MUST use
perceptibleregardless of settings. The user must be allowed to believe this will be quick.
8.3 The modifier catalogue
31 modifiers. Each is a pure function that takes the mounted challenge root and the context, and returns a disposer. The Torture engine applies modifierCount of them, filtered by compatibility, chosen by seeded weighted draw.
| # | Id | Effect | Compatible with | Cruelty |
|---|---|---|---|---|
| 1 | slow-reveal |
The challenge fades in over 4.2 s. Interaction is blocked until complete. | all | D2 |
| 2 | fade-out |
Selected items fade out over 4.2 s before being replaced. | B | D3 |
| 3 | stagger-refresh |
Replacements arrive one at a time, 1.2 s apart. | B | D3 |
| 4 | drift |
The primary control drifts 40-120 px on a slow sine path. | C, G | P3 |
| 5 | flee |
The verify control moves away when the pointer comes within 80 px. Three evasions maximum, then it submits. | C, G | P4 I3 |
| 6 | shrink |
The target shrinks 1.5 % per second, floor 8 px. | C | P4 |
| 7 | jitter |
All interactive elements jitter Β±2 px at 30 Hz. Disabled under reduced motion. | all | P3 |
| 8 | inertia |
Drag controls carry momentum and overshoot by 12 %. | C | P4 |
| 9 | inverted |
Drag axis inverted without notice. | C | A4 I3 |
| 10 | deadzone |
The first 18 px of any drag are ignored. | C | P3 |
| 11 | tolerance-half |
All precision tolerances halved. | C, F | P4 |
| 12 | no-paste |
Paste, drop, and autofill are blocked. On attempt: "Manual entry required." | A, D | I3 |
| 13 | no-select |
Text selection disabled across the stage. | all | I2 |
| 14 | case-trap |
Answer is case-sensitive; this is not stated until the second failure. | A, D | A4 |
| 15 | homoglyph |
Latin characters substituted with Cyrillic/Greek lookalikes in the prompt, never in the answer. | A | A5 |
| 16 | low-contrast |
Contrast reduced to 2.1:1. Never applied when photosensitive-safe or reduced motion is set, and never to more than one challenge in five. |
A, B | P4 |
| 17 | shrinking-text |
Prompt font size decays from 18 px to 9 px over 20 s. | A, D | P3 D2 |
| 18 | blur-pulse |
The stage blurs to 3 px for 900 ms every 6 s. | A, B | P3 |
| 19 | rotate-frame |
The whole stage is rotated 4-11Β°. | B, D | P3 I2 |
| 20 | mirror |
The stage is horizontally mirrored. Text is exempt; controls are not. | C, D | A4 P4 |
| 21 | shuffle |
Grid contents reshuffle every 3 s, preserving correctness. | B, D | P4 D2 |
| 22 | decoy-verify |
Three to five identical verify buttons; one is live, chosen per attempt. | all | A5 I4 |
| 23 | confirm-chain |
Submission requires confirming 2-6 modal dialogs, worded near-identically, one of which is a trick question. | all | D3 I4 |
| 24 | are-you-sure |
A single confirmation that appears only when the answer is correct. | all | I3 |
| 25 | progress-regress |
On failure, the displayed progress drops by one level with an audible click. | all | I5 |
| 26 | cooldown |
On failure, a countdown of 8-45 s. It pauses when the tab is hidden and it resets on any keypress. | all | D5 I5 |
| 27 | queue |
Before the challenge, "You are number N in the verification queue." N descends, then rises once. | all | D4 I5 |
| 28 | false-progress |
A determinate progress bar advances to 97 %, pauses 6 s, then completes. | all | D3 I3 |
| 29 | extra-round |
Forces the "one more round" branch on the next pass. | all | I5 |
| 30 | silent-rules |
The instruction text omits one operative constraint, revealed only in the hint after failure. | all | A5 |
| 31 | keyboard-thief |
Focus is programmatically returned to a decoy input every 4 s. Disabled if a screen reader is detected via the allow-escape interaction path. |
A, D | I5 |
jitter, blur-pulse, rotate-frame, and mirror are all suppressed under motion="reduced". low-contrast is suppressed under prefers-contrast: more.
8.4 Compatibility matrix rules
- At most one of
drift,flee,shrinkper challenge. - At most one of
inverted,mirrorper challenge. cooldownandqueueMUST NOT co-occur on the same challenge.decoy-verifyMUST NOT co-occur withflee.- No challenge may exceed a total applied cruelty delta of 12 points.
8.5 The honest challenge
Exactly once per session, at a level chosen uniformly from the middle third, the scheduler MUST present a challenge with zero modifiers, zero delay, and a 100 % pass tolerance - a plain checkbox that simply works and passes instantly.
This is not mercy. It is calibration. The contrast makes every subsequent challenge worse, and it re-establishes the sunk-cost gradient at precisely the point where abandonment risk peaks. It is also the moment at which the user realises the cruelty was a choice.
9. Difficulty, tiers, and progression
9.1 Tiers
| Tier | Name | Cruelty Index range | Character |
|---|---|---|---|
| 1 | Perfunctory | 0-5 | Recognisable as a normal CAPTCHA. Passable in under 10 s. |
| 2 | Tedious | 6-9 | Slow but fair. The user begins to notice the delay. |
| 3 | Adversarial | 10-13 | The interface is working against the user and it is now obvious. |
| 4 | Punitive | 14-16 | Failure is expected. Multiple attempts are the norm. |
| 5 | Absurd | 17-20 | The challenge is a joke at the user's expense and does not pretend otherwise. |
9.2 Difficulty presets
| Preset | Start tier | Ramp | Cruelty | Time limit default | Fail policy |
|---|---|---|---|---|---|
mild |
1 | +1 every 6 levels, cap 2 | 15 | 0 | retry |
standard |
1 | +1 every 4 levels, cap 3 | 55 | 0 | retry |
cruel |
2 | +1 every 3 levels, cap 4 | 70 | 60 | replace |
inhumane |
2 | +1 every 2 levels, cap 5 | 85 | 40 | regress |
catastrophic |
3 | +1 every level, cap 5 | 100 | 25 | restart |
catastrophic with fail-policy="restart" and loop-mode="hard" is the reference configuration for the demonstration page's "Full experience" button. It is unwinnable by construction and MUST be labelled as such in the demo UI.
9.3 The curve
Effective tier for level n:
tier(n) = clamp(startTier + floor((n - 1) / rampInterval), 1, tierCap)
The scheduler draws from challenges whose tier is tier(n) or tier(n) - 1, weighted 3:1 in favour of the exact tier. This produces variance without cliff edges.
9.4 Selection algorithm
1. candidates = registry.all()
2. filter by allowlist (types), then denylist (exclude)
3. filter by pointer compatibility with the current input modality
4. filter by requires[] against runtime capability detection
5. filter by tier β { tier(n), tier(n) - 1 }
6. drop any id used in the last 5 levels (anti-repeat window)
7. drop any family used in the last 2 levels (anti-clustering)
8. if candidates is empty, relax rule 7, then 6, then 5, in that order
9. weighted draw using definition.weight Γ familyBalance Γ rng
10. if this is the honest-challenge level (Β§8.5), override with 'checkbox-plain'
The anti-repeat window is deliberately short. Seeing the same challenge type again after six levels - when the user has forgotten the specific trap but not the general dread - is more effective than never repeating.
9.5 Family balance
familyBalance starts at 1.0 for every family and is multiplied by 0.6 each time that family is drawn, recovering 15 % toward 1.0 each level. This produces a session that visits every family without feeling systematic.
9.6 Level count semantics
loop-mode="off": the session ends afterlevelspasses.loop-mode="soft": the session ends afterlevelspasses, but each pass has probabilityP_extraof not counting (Β§8.1), so the expected number of challenges islevels / (1 - P_extra). At default cruelty this is roughly 1.32 Γlevels.loop-mode="hard":totalisInfinity, the displayed progress asymptotes toward but never reaches 100 %, and the session never terminates except by abandonment. See Β§14.
10. The Challenge Catalog
84 challenges across 8 families. Every entry is normative. Where an entry says "MUST", the acceptance suite tests it.
Common notation:
ptr- pointer requirement:any,fine(mouse/trackpad),coarse(touch).req- runtime capability requirements.D/P/A/I- Suffering Model scores (Β§2.9). The sum is the Cruelty Index.- Instance - the seeded parameters produced by
compose(). - Pass - the exact condition under which
validate()returnsok: true. - Hint - what is disclosed after failure, and when.
10.A Family A - Text and Glyph
The oldest family, and the one users recognise as "a real CAPTCHA". Its role in the session is to establish legitimacy in the first two levels, then to betray it.
All family A rendering uses Doodler.glyphs(), which draws characters to a canvas with per-character transform, warp, and noise. No fonts are loaded; the canvas uses the platform's generic monospace and sans-serif stacks.
The character set for all generated strings excludes nothing. l, I, 1, 0, O, 5, S, 2, Z, rn, vv are all permitted and, at tier 3 and above, actively favoured. This is the single most important decision in the family.
A1 Β· text-wobble - Distorted text
tier 1 Β· D1 P2 A1 I0 β 4 Β· ptr any Β· req none
Instance. A 6-character string. Each character is drawn at a random rotation of Β±18Β°, a random baseline offset of Β±20 % of cap height, and a random horizontal scale of 0.8-1.2. Three sine-warped strike-through lines cross the canvas. Background is 8 % salt-and-pepper noise.
Presentation. Canvas at 260 Γ 90 CSS px, rendered at devicePixelRatio for crispness - the distortion must be deliberate, never an artefact of blurry scaling. A single text input below, inputmode="text", autocapitalize="off", autocomplete="off", spellcheck="false".
Pass. Case-insensitive exact match at tier 1. Case-sensitive at tier 2 and above, without disclosure. case-trap modifier forces case sensitivity at any tier.
Hint. Attempt 2: This field is case-sensitive. Attempt 3: The string contains 6 characters.
Mobile. Canvas scales to min(100%, 320px). The input MUST be scrolled into view on focus with scrollIntoView({ block: 'center' }) after a 300 ms delay to survive the virtual keyboard's layout shift.
A2 Β· text-ambiguous - Deliberate glyph confusion
tier 3 Β· D1 P3 A5 I2 β 11 Β· ptr any Β· req none
Instance. A 7-character string drawn exclusively from the confusable set l I 1 | 0 O o 5 S 2 Z 8 B 6 G. Rendering distortion is lower than A1 - the difficulty is intrinsic to the character set, not the rendering, and the user must be able to see clearly that they cannot tell.
Pass. Case-sensitive exact match.
Hint. Attempt 2: Note that the string may contain both the digit one and the lowercase letter L. Attempt 3: It contains neither.
The attempt-3 hint MUST be true for the specific instance - compose() guarantees it by excluding both from the drawn string when that hint would be reached. The hint is therefore honest, useless, and retrospectively infuriating.
A3 Β· text-mirror - Reversed entry
tier 3 Β· D2 P2 A3 I3 β 10 Β· ptr any Β· req none
Instance. A 6-character string rendered normally. The instruction reads: Type the characters in reverse order.
Pass. Input equals the reversed string, case-sensitive.
Modifier interaction. With mirror applied, the canvas is horizontally flipped, so the visually-presented order is already reversed and typing what is seen is correct. Two-thirds of users will reverse it again. This is intentional and the hint never mentions it.
A4 Β· text-fade - Vanishing string
tier 3 Β· D3 P4 A2 I3 β 12 Β· ptr any Β· req none
Instance. A 6-character string is displayed at full opacity for 1,400 ms, then fades to zero over 900 ms and does not return. A Show again button exists; it costs 6 s of countdown and MAY be used twice, after which it is replaced by the text Sufficient viewings have been provided.
Pass. Exact match, case-insensitive.
Failure mode of interest. Users screenshot it. Screenshotting is not prevented - the fade is fast enough that the screenshot reflex is itself the challenge.
A5 Β· text-scroll - Moving marquee
tier 4 Β· D3 P4 A2 I3 β 12 Β· ptr any Β· req none
Instance. An 8-character string scrolls right-to-left through a 180 px viewport at 55 px/s. The full string is never simultaneously visible. Two complete passes, then it stops off-screen.
Pass. Exact match, case-insensitive.
Reduced motion. Replaced by A6, which presents the same string one character at a time in a fixed position - equally hostile, no motion.
A6 Β· text-sequential - One character at a time
tier 3 Β· D3 P2 A2 I3 β 10 Β· ptr any Β· req none
Instance. Eight characters are shown one at a time in the same position, 700 ms each, with a 200 ms blank between. No replay.
Pass. Exact match, case-sensitive.
Hint. Attempt 2: Each character is shown once.
A7 Β· text-font-lottery - Typeface roulette
tier 3 Β· D1 P4 A4 I3 β 12 Β· ptr any Β· req none
Instance. A 6-character string where each character is drawn in a different generic family - serif, sans-serif, monospace, cursive, fantasy, system-ui - at randomised weight and style. On systems where cursive maps to a script face, uppercase and lowercase become genuinely ambiguous. This is a platform-dependent difficulty, which is itself part of the joke.
Pass. Case-insensitive exact match. The case sensitivity is dropped here specifically because it would make the challenge unpassable, and Kaptcha must remain passable. Law 10 requires the joke stay visible; an unwinnable challenge outside catastrophic reads as a bug.
A8 Β· text-homoglyph - Alphabet impersonation
tier 4 Β· D2 P3 A5 I4 β 14 Β· ptr any Β· req none
Instance. The prompt reads: Type the Latin characters only. The displayed string is 9 characters, of which 5 are Latin and 4 are visually identical Cyrillic or Greek homoglyphs (Π° Π΅ ΠΎ Ρ Ρ Ρ
Ρ, Ξ Ξ Ξ Ξ Ξ Ξ Ξ Ξ Ξ Ξ Ξ‘ Ξ€ Ξ§). They are indistinguishable in every font.
Pass. The input matches exactly the Latin subset, in order.
Hint. Attempt 2: Four of the nine characters are not Latin. Attempt 3: They are Cyrillic. This information does not help you.
The attempt-3 hint is the only place in the entire product where the component acknowledges its own cruelty in the first person. It MUST appear exactly once and nowhere else.
A9 Β· text-no-paste - Manual entry required
tier 2 Β· D2 P2 A1 I4 β 9 Β· ptr any Β· req none
Instance. A 24-character alphanumeric string, cleanly rendered and perfectly legible, displayed as selectable text. Paste, drop, input events with inputType of insertFromPaste, and autofill are all blocked. Attempting to paste shows: Manual entry required.
Pass. Exact match, case-sensitive.
This challenge is trivially easy and takes 40 seconds. Legibility is the point: the user cannot blame the interface for their frustration, only its policy.
A10 Β· text-audio - Transcribe the audio
tier 3 Β· D3 P4 A3 I3 β 13 Β· ptr any Β· req audio
Instance. Six digits are rendered through AudioKit.speak() - a formant-ish synthesis using two oscillators and an envelope per digit, producing sounds recognisable as digits but not comfortably so. Applied over the top: pink noise at β18 dB, a second voice reading different digits at β24 dB, and a 0.3 s slap-back delay.
Presentation. A play button, a replay counter showing 2 replays remaining, and a numeric input with inputmode="numeric".
Pass. Exact digit sequence.
Accessibility note. Per Β§2.2, audio CAPTCHAs fail 46 % of blind users. This challenge is not offered as an accessible alternative to anything, and MUST NOT be described as one. It is excluded when sound="false".
A11 Β· text-shrinking - Diminishing legibility
tier 4 Β· D4 P5 A2 I4 β 15 Β· ptr any Β· req none
Instance. A 6-character string rendered at 32 px, shrinking by 6 % per second toward a floor of 5 px, reached at 30 s. There is no replay and no zoom - user-select: none and touch-action: manipulation on the canvas prevent pinch-zoom of the element specifically. Browser page zoom still works and MUST NOT be defeated; defeating it would cross from satire into obstruction of assistive behaviour.
Pass. Case-insensitive exact match.
Hint. Attempt 2: Read it sooner.
A12 Β· text-count-chars - Enumerate the string
tier 2 Β· D3 P2 A3 I3 β 11 Β· ptr any Β· req none
Instance. A paragraph of 180-240 characters of plausible corporate filler is displayed. The prompt: How many times does the letter "e" appear in the text above? The letter is drawn per instance from e a t o i n s r, whichever gives a count between 12 and 28.
Pass. Exact integer.
Hint. Attempt 2: Uppercase occurrences count. Attempt 3: The correct answer differs from your last answer by less than 4.
The attempt-3 hint is computed against the user's actual last answer and MUST be true. If it is not true, the alternative string Your last answer was not close. is used instead.
10.B Family B - Grid and Image Selection
The family everyone means when they say "CAPTCHA". All imagery is procedurally drawn by Doodler - there are no photographs, which means every object boundary is under our control, which means every ambiguity in this family is deliberate.
Common presentation. A 3 Γ 3 or 4 Γ 4 grid of square tiles. Minimum tile size 64 px on coarse pointers, 44 px on fine. Grid maximum width 340 px. Tiles have a 3 px gap and a selection state rendered as a 4 px inset border plus a checkmark badge at 32 % tile size.
Common cruelty. All tier-3+ members of this family MUST apply fade-out and stagger-refresh (Β§8.3) unless explicitly excluded, reproducing the reCAPTCHA v2 fade documented in Β§2.2.
B1 Β· grid-select - Select all squares with X
tier 1 Β· D2 P1 A2 I1 β 6 Β· ptr any Β· req none
Instance. 3 Γ 3 grid. A target category is drawn from the Doodler catalogue: traffic light, bicycle, crosswalk, bus, fire hydrant, chimney, parking meter, stairs, boat, bridge. Between 2 and 4 tiles contain the target; the rest contain distractors, at least one of which is from a category that shares a silhouette with the target.
Pass. Selection set equals target set exactly.
Hint. Attempt 2: Select every square that contains any part of the object.
B2 Β· grid-refresh - The self-replenishing grid
tier 3 Β· D5 P1 A2 I4 β 12 Β· ptr any Β· req none
Instance. As B1, but each correct selection is confirmed with a 4.2 s fade-out and replaced by a new tile. Replacement tiles contain the target with probability 0.45. The challenge ends when the user submits with a grid containing zero targets.
Pass. No targets remain, and no non-targets were ever selected.
Termination guarantee. The replacement probability decays by 0.08 per replacement, reaching zero after 6 replacements. The challenge terminates in bounded time. This bound MUST exist; a genuinely non-terminating grid violates Law 10 by becoming indistinguishable from a bug.
Expected duration. 47 s at default cruelty. This is the single most representative challenge in the product.
B3 Β· grid-edge - Boundary ambiguity
tier 4 Β· D3 P3 A5 I4 β 15 Β· ptr any Β· req none
Instance. 4 Γ 4 grid rendered as a single image sliced into tiles, so objects cross tile boundaries. Three objects are positioned so that each intrudes into a neighbouring tile by exactly 4, 9, and 18 px respectively.
Pass. The grader's threshold is 6 px of intrusion, chosen per instance from {4, 9, 18} so that in every instance exactly one boundary case is graded in a way the user could not have predicted.
Hint. Attempt 2: Include squares containing any visible part of the object. Attempt 3: Squares containing only a negligible part of the object should not be included.
The two hints contradict each other. They MUST both be shown, in that order, and never simultaneously.
B4 Β· grid-count - Enumerate the objects
tier 3 Β· D2 P1 A5 I3 β 11 Β· ptr any Β· req none
Instance. A single drawn scene, not a grid. The prompt: How many traffic lights are in the image? The scene contains 4 unambiguous instances, 1 partially occluded instance, 1 instance reflected in a window, and 1 instance on a distant background billboard.
Pass. The accepted answer is 5 - unambiguous plus occluded. Reflections and depictions do not count.
Hint. Attempt 2: Reflections are not objects. Attempt 3: Images of objects are not objects.
B5 Β· grid-shuffle - The moving grid
tier 4 Β· D3 P4 A3 I4 β 14 Β· ptr any Β· req none
Instance. As B1, 4 Γ 4, but tile positions are permuted every 3.0 s with a 400 ms cross-fade. Selection state follows the tile, not the position - which is correct, defensible, and completely disorienting.
Pass. Selection set equals target set.
Reduced motion. Permutation interval extends to 8 s and the transition becomes a hard cut.
B6 Β· grid-rotate - Orientation matching
tier 3 Β· D3 P3 A2 I3 β 11 Β· ptr any Β· req none
Instance. A single drawn object (animal, vehicle, or letterform) presented at a random rotation. Two arrow buttons rotate it. Rotation gain is 11.25Β° per press - 32 presses for a full revolution - reproducing the low-gain control loop of Arkose MatchKey (Β§2.4).
Pass. Final orientation within Β±14Β° of upright.
Coarse-pointer variant. grid-rotate-touch accepts a two-finger rotate gesture in addition to the buttons, with a 0.55 gain factor so the gesture is also slower than reality.
B7 Β· grid-jigsaw - Reassemble the image
tier 4 Β· D4 P4 A1 I3 β 12 Β· ptr any Β· req none
Instance. A drawn scene cut into a 3 Γ 3 or 4 Γ 4 grid and shuffled. The user swaps tiles by clicking two, or dragging one onto another.
Pass. All tiles in correct position.
Cruelty. The shuffle is guaranteed to be a permutation requiring at least n β 1 swaps, and at tier 4 at least one pair is 180Β°-rotationally ambiguous so that the user cannot tell they have solved it until they submit.
B8 Β· grid-souls - Select all items with a soul
tier 5 Β· D2 P1 A5 I5 β 13 Β· ptr any Β· req none
Instance. A 3 Γ 3 grid of drawn items: a dog, a rock, a tree, a car, a person, a jellyfish, a chatbot, a coral, a bell.
Pass. The accepted set is { dog, person, jellyfish, coral }.
Hint. Attempt 2: Consider the biological definition. Attempt 3: The correct answer is not a matter of opinion.
This challenge is a direct homage to neal.fun's I'm Not a Robot (Β§2.3). It is the philosophical centrepiece of the catalogue and MUST be included in every session at cruel and above.
B9 Β· grid-odd-one-out - Categorical exclusion
tier 3 Β· D2 P1 A5 I3 β 11 Β· ptr any Β· req none
Instance. Six drawn items with three overlapping category memberships, engineered so that at least two defensible answers exist. Example set: tomato, cucumber, carrot, apple, pepper, potato - botanical fruit versus culinary vegetable versus root.
Pass. One specific item, chosen per instance.
Hint. Attempt 2 states the intended category axis. Attempt 3 states: Botanical classification is not being tested. - which is only sometimes true, and the specific instance determines whether it is.
B10 Β· grid-spot-difference - Comparative inspection
tier 4 Β· D4 P4 A2 I3 β 13 Β· ptr any Β· req none
Instance. Two drawn scenes side by side (stacked below 520 px viewport width) with exactly 5 differences: one colour shift of ΞE β 12, one 3 px position shift, one missing 6 px detail, one mirrored element, one added element of 20 px.
Pass. All 5 differences clicked, Β±22 px tolerance, within attempts clicks plus 3 misses.
B11 Β· grid-connect - Rotate the pipes
tier 3 Β· D3 P2 A1 I2 β 8 Β· ptr any Β· req none
Instance. A 4 Γ 4 grid of pipe segments (straight, elbow, T, cross) that must be rotated to form a continuous path from the left edge to the right edge. Each tap rotates 90Β°. Generation guarantees a solution exists and requires between 7 and 14 rotations.
Pass. A connected path exists from source to sink.
Note. This is the only challenge in the catalogue that is genuinely satisfying to solve. It exists to be interrupted - the scheduler SHOULD place it immediately before a tier-5 challenge, because a good mood is a resource to be spent.
B12 Β· grid-find-in-scene - Locate the specified element
tier 4 Β· D4 P4 A3 I4 β 15 Β· ptr any Β· req none
Instance. A wide, densely-drawn scene, 3Γ wider than the viewport, horizontally scrollable, containing 400+ small drawn elements. One matches the prompt: Find the cat holding a guitar. Twelve near-misses exist: cats holding other objects, other animals holding guitars.
Pass. Click within 24 px of the target centre.
Mobile. The scroll container uses overflow-x: auto with scroll-snap-type: none and momentum scrolling explicitly disabled via -webkit-overflow-scrolling: auto - because momentum would make this pleasant.
10.C Family C - Precision and Motor Control
This family taxes the body rather than the mind. It is where Fitts's Law is weaponised (Β§2.8). Every member MUST ship in both fine and coarse variants, because a mouse challenge that is merely difficult on a phone becomes impossible, and impossible is off-brand.
Global rule for family C. Tolerances are specified in CSS pixels and are multiplied by the tolerance factor from Β§8.1 and by 1.6 on coarse pointers. Even after the coarse adjustment, these remain below the platform's recommended minimum touch target, which is the point.
C1 Β· slider-notch - Seat the puzzle piece
tier 3 Β· D3 P5 A1 I2 β 11 Β· ptr any Β· req none
Instance. A drawn background scene, 300 Γ 160, with a jigsaw-shaped notch cut at a random x between 40 % and 85 % of width. The matching piece sits at x = 0 and is moved by a horizontal slider below.
Pass. |pieceX β notchX| β€ 2 px before tolerance scaling. At default cruelty this is 1.28 px.
Cruelty. The slider has an 11 % inertial overshoot and a 4 px deadzone at drag start. Fine-adjustment via arrow keys is supported at 1 px per press, which is the only humane thing in this challenge and MUST be undocumented.
Hint. Attempt 2: Use the arrow keys for fine adjustment. Attempt 3, coarse pointers only: Fine adjustment is not available on this device.
C2 Β· hold-still - Sustained stillness
tier 4 Β· D5 P5 A1 I5 β 16 Β· ptr any Β· req none
Instance. A circular target 72 px across. The user must press and hold, and keep the pointer within 3 px of the press origin, for a duration of 12 s. A ring fills to indicate progress.
Pass. 12 s elapsed with no excursion beyond tolerance and no pointer release.
Cruelty. Any excursion resets the ring to zero with a 250 ms shake animation and the message Movement detected. On touch devices, finger-contact centroid drift makes this genuinely near-impossible to hold for 12 s - the tolerance is therefore raised to 8 px on coarse pointers, which is still below normal finger tremor.
Hard cap. After 5 resets the requirement drops to 6 s, and after 8 resets to 3 s. The user is never told. They will attribute their eventual success to skill.
C3 Β· flee-button - Acquire the evasive control
tier 3 Β· D2 P4 A2 I5 β 13 Β· ptr fine Β· req none
Instance. A Verify button that translates to a new position within its container whenever the pointer enters an 80 px radius. It evades exactly 7 times, then stops permanently and can be clicked.
Pass. The button is clicked.
Cruelty. Evasion destination is chosen to maximise distance from the pointer while remaining inside the container, and the transition is 180 ms cubic-bezier(.2,.9,.3,1) so it looks playful rather than broken.
Coarse variant. flee-button-touch - the button teleports on touchstart if the touch lands within 60 px, up to 4 times. Teleporting away from a finger already in contact is more startling than evading a hovering cursor, so the count is lower.
C4 Β· shrink-target - Diminishing target
tier 4 Β· D3 P5 A1 I4 β 13 Β· ptr any Β· req none
Instance. A circular target starting at 120 px, shrinking at 4 % per second, floor 10 px (16 px coarse). It must be clicked 3 times. It relocates after each hit.
Pass. 3 hits. Misses are counted and displayed as Misses: n, which serves no functional purpose.
C5 Β· trace-path - Follow the corridor
tier 4 Β· D4 P5 A1 I3 β 13 Β· ptr any Β· req none
Instance. A BΓ©zier corridor 28 px wide (36 px coarse) drawn across a 300 Γ 200 canvas, with 2-4 inflection points. The user must drag from the start marker to the end marker without the pointer leaving the corridor.
Pass. The full path traversed with zero excursions and a total traversal time between 0.8 s and 25 s. Faster than 0.8 s is rejected as Movement too uniform. - a satire of behavioural bot detection, and the one place where moving well is punished.
Cruelty. Excursion aborts immediately with Path deviation detected. and a full restart.
C6 Β· drag-sort - Sort by dragging
tier 3 Β· D4 P4 A2 I3 β 13 Β· ptr any Β· req none
Instance. 6 cards bearing values (numbers, dates, or fractions) that must be dragged into ascending order. Drop zones snap only within 10 px; outside that the card returns to origin with a 320 ms animation that MUST NOT be skippable.
Pass. Correct order.
Coarse variant. Long-press (450 ms) to lift, then drag. The long-press threshold is above the platform default of 500 ms minus tolerance, producing frequent accidental scrolls, which is why touch-action: none is applied only after the lift is confirmed.
C7 Β· bin-sort - Categorise by dragging
tier 3 Β· D4 P4 A3 I3 β 14 Β· ptr any Β· req none
Instance. 8 drawn items and 3 labelled bins. Two of the eight items belong plausibly to two bins each.
Pass. All items in their designated bin, per the instance's answer key.
Hint. Attempt 2 names the intended bin for one of the two ambiguous items. Attempt 3 names the other, so that the user needs three attempts minimum unless they guess.
C8 Β· moving-target - Intercept the mover
tier 4 Β· D3 P5 A1 I3 β 12 Β· ptr any Β· req none
Instance. A 34 px dot bounces around a 300 Γ 200 field at 140 px/s, accelerating 8 % after each hit. 5 hits required. A miss decrements the hit count by one, floor zero.
Pass. 5 hits.
Reduced motion. Replaced by C4.
C9 Β· slot-stop - Stop the reels
tier 3 Β· D4 P4 A2 I4 β 14 Β· ptr any Β· req none
Instance. Three reels of 8 symbols spinning at 320 ms/symbol. A Stop button under each. All three must be stopped on the same symbol.
Pass. Three matching symbols.
Cruelty. Each stop has a 90 ms input latency deliberately inserted, so the symbol under the marker at the moment of the press is not the one that lands. The offset is constant, so the challenge is learnable in roughly four attempts, at which point the reel speed increases by 15 %.
C10 Β· pinch-rotate - Gestural orientation
tier 3 Β· D3 P4 A1 I3 β 11 Β· ptr coarse Β· req none
Instance. A drawn object at random rotation and scale. The user must rotate to upright (Β±12Β°) and scale to a target size (Β±8 %) using a two-finger pinch-rotate gesture.
Pass. Both constraints met simultaneously and held for 600 ms.
Fine variant. key-rotate - arrow keys rotate at 5Β° per press, +/β scale at 2 % per press. 40+ keypresses typical.
C11 Β· long-scroll - Scroll to the end
tier 2 Β· D5 P2 A1 I5 β 13 Β· ptr any Β· req none
Instance. A scroll container 320 px tall with 42,000 px of content - mostly repeated boilerplate, with 6 distinct interruptions at random depths ("You are 14 % of the way through."). At the end sits the verify button.
Pass. The button is clicked.
Cruelty. Scrollbar is hidden. scroll-behavior: auto. Keyboard End is intercepted and shows Sequential reading is required. On coarse pointers, momentum scrolling is suppressed, requiring approximately 130 swipes.
Mercy clause. At 80 % depth the remaining content collapses and the button appears, so the true swipe count is roughly 104. The user is not informed.
C12 Β· tiny-checkbox - The small checkbox
tier 3 Β· D2 P5 A2 I5 β 14 Β· ptr any Β· req none
Instance. The canonical I'm not a robot panel, rendered exactly as the user expects, except the checkbox is 9 Γ 9 px (14 Γ 14 coarse) and offset 3 px from its label's implied position. On first pointer-down within 40 px but outside the box, the box relocates 60 px to the right, once only.
Pass. The checkbox is checked.
C13 Β· sign-here - Signature capture
tier 4 Β· D3 P4 A4 I5 β 16 Β· ptr any Β· req none
Instance. A signature field. The user draws with pointer or finger. The instruction: Sign to confirm you are human.
Pass. The drawn path MUST exhibit sufficient irregularity: mean absolute curvature variance above a threshold, at least 3 direction reversals, total length above 240 px, and drawn in more than 700 ms.
Failure messages.
- Too smooth:
Signature appears machine-generated. Please sign naturally. - Too short:
Signature too brief. - Too fast:
Signature too fast.
A user drawing with a mouse will fail the smoothness test repeatedly. The correct strategy - deliberately shaking the hand - is the exact behaviour that makes the challenge memorable.
C14 Β· two-hands - Simultaneous acquisition
tier 5 Β· D3 P5 A2 I5 β 15 Β· ptr any Β· req none
Instance. Two buttons at opposite corners of the stage, each requiring a sustained press. Both must be held simultaneously for 3 s.
Fine pointers. A single pointer cannot press both, so the challenge requires keyboard: hold Shift while clicking, or hold the two buttons via Space on a focused button plus a mouse press on the other. The prompt does not explain this. The hint at attempt 2 does: Two simultaneous inputs are required. A keyboard may be necessary.
Coarse pointers. Genuinely requires two fingers, and the corners are chosen so that a one-handed grip cannot reach both - deliberately placing the targets outside the thumb zone described in Β§2.8.
Pass. Both held for 3 s.
10.D Family D - Cognitive and Logic
This family asks the user to think, which is significantly more insulting than asking them to click, because it implies their time was worth interrupting for something substantive. It is not substantive.
D1 Β· math-escalate - Arithmetic
tier 1 Β· D2 P1 A1 I2 β 6 Β· ptr any Β· req none
Instance. An arithmetic expression whose complexity is a function of level: level 1-4 gives a + b with a, b < 20; level 5-9 gives three terms with mixed precedence; level 10+ gives four terms with parentheses, a modulo, and one term expressed in words (seven).
Pass. Exact integer.
Cruelty. The input is type="text" with inputmode="numeric", and it rejects a leading +, trailing whitespace, and any thousands separator with Numeric input only.
D2 Β· math-order - Order the expressions
tier 3 Β· D4 P3 A2 I3 β 12 Β· ptr any Β· req none
Instance. Five expressions of mixed form - a fraction, a decimal, a percentage, a root, and an exponent - with values within 0.06 of each other. They must be dragged into ascending order.
Pass. Correct order.
An homage to I'm Not a Robot level 34 (Β§2.3). The closeness of the values MUST be verified at compose time; if any two differ by more than 0.12 the instance is regenerated.
D3 Β· sequence-continue - Continue the sequence
tier 3 Β· D3 P1 A4 I3 β 11 Β· ptr any Β· req none
Instance. A 6-term integer sequence generated by one of: arithmetic, geometric, Fibonacci-like, alternating-difference, squares-plus-constant, or digit-count-of-the-previous-term-in-English. The last of these is drawn 1 time in 6 and is unguessable.
Pass. Exact next term.
Hint. Attempt 2: The rule is not arithmetic. - shown only when true; otherwise The rule is arithmetic. is shown, which is unhelpful in a different way.
D4 Β· word-search - Find the word
tier 3 Β· D4 P3 A1 I3 β 11 Β· ptr any Β· req none
Instance. A 10 Γ 10 letter grid containing the word STOP exactly once, in one of 8 directions, plus 6 near-misses (STOP with one letter substituted, POTS, TOPS, STOPS crossing a boundary).
Pass. The four cells dragged or clicked in order.
Mobile. Cell size 30 px minimum, so the grid is 300 px wide and fits. Drag selection uses pointermove with touch-action: none on the grid only.
D5 Β· memory-pairs - Concentration
tier 3 Β· D5 P2 A1 I3 β 11 Β· ptr any Β· req none
Instance. 12 cards, 6 pairs of drawn symbols. Cards flip face-up on click and flip back after 700 ms - well below the 1-2 s used by every commercial implementation of this game, and calibrated to be just barely sufficient.
Pass. All pairs matched.
Cruelty. After the 8th mismatched turn, the unmatched cards are silently re-shuffled once. No animation marks this. The user will believe they misremembered.
D6 Β· simon - Sequence recall
tier 3 Β· D5 P3 A1 I3 β 12 Β· ptr any Β· req sound (optional)
Instance. Four coloured pads. A sequence grows by one each round to a target length of 8. Playback speed increases 6 % per round.
Pass. Length-8 sequence reproduced.
Cruelty. A single error restarts from length 1, not from the current length. At default cruelty the expected number of pad presses to complete is above 90.
Sound. Each pad has a tone. With sound="false" the challenge remains passable visually, but the tone is a significant memory aid, so muting the page makes this materially harder - an accidental cruelty that is nonetheless retained.
D7 Β· hanoi - Tower of Hanoi
tier 4 Β· D5 P3 A1 I4 β 13 Β· ptr any Β· req none
Instance. Four discs, three pegs. Minimum 15 moves.
Pass. All discs on the target peg.
Cruelty. A move counter displays Moves: n (optimal: 15). It is purely judgemental. At tier 5, exceeding 24 moves triggers Inefficient. Restarting.
D8 Β· maze - Navigate the maze
tier 4 Β· D5 P4 A2 I4 β 15 Β· ptr any Β· req none
Instance. A 15 Γ 15 perfect maze generated by randomised depth-first search. A token is moved by arrow keys, WASD, swipe, or drag.
Pass. Token reaches the exit.
Cruelty. Only a 5 Γ 5 window around the token is visible; the rest is fog. At tier 5, two wall segments relocate every 10 s, guaranteed never to seal the token in and never to break solvability, verified by a connectivity check before each mutation.
D9 Β· chess-mate - Mate in one
tier 5 Β· D4 P2 A3 I5 β 14 Β· ptr any Β· req none
Instance. One of 24 hard-coded positions with a unique mate in one. The board is drawn by Doodler. The user clicks the piece then the destination square.
Pass. The mating move.
Hint. Attempt 2: The solution involves a piece that is currently undefended. Attempt 3: You do not need to know chess notation.
Escape valve. Because chess knowledge is not universal, this challenge MUST offer a I do not play chess link after attempt 2, which replaces it with a tier-3 challenge from another family. This is the only challenge with a built-in substitution, and it exists because gating access on domain knowledge crosses from tedium into exclusion.
D10 Β· anagram - Unscramble
tier 2 Β· D3 P1 A2 I2 β 8 Β· ptr any Β· req none
Instance. A common 7-9 letter English word, scrambled, guaranteed to have no other valid anagram in the bundled 2,000-word list.
Pass. The word, case-insensitive.
Hint. Attempt 2 gives the first letter. Attempt 3 gives the definition, which for words like logical is capable of reasoning and is therefore also a small insult.
D11 Β· date-arithmetic - Calendar reasoning
tier 4 Β· D4 P1 A3 I4 β 12 Β· ptr any Β· req none
Instance. If today is Thursday, what day of the week was it 137 days ago? The offset is 60-400 days, and 1 instance in 4 crosses a leap day, which is irrelevant to weekday arithmetic but will cause a meaningful fraction of users to try to account for it.
Pass. Correct weekday name, case-insensitive, accepting 3-letter abbreviations.
D12 Β· binary - Base conversion
tier 3 Β· D3 P1 A1 I4 β 9 Β· ptr any Β· req none
Instance. Convert 10110110 to decimal. Or, at tier 4, Convert 182 to hexadecimal. Or, at tier 5, Convert 0xB6 to base 7.
Pass. Exact.
Indignity note. This scores I4 because a CAPTCHA that requires base-7 conversion is asserting that base-7 conversion is a normal human capability. That assertion is the joke.
D13 Β· read-the-terms - Comprehension of the agreement
tier 5 Β· D5 P2 A4 I5 β 16 Β· ptr any Β· req none
Instance. A 2,800-word generated terms-of-service document in a scroll container, written in convincing legalese. Clause 14.3 contains a single specific fact - a number, a colour, or a fictional jurisdiction. The scroll container MUST be scrolled to the bottom before the question is revealed, and the question is: Per clause 14.3, what is the maximum permitted number of verification attempts per calendar quarter?
Pass. Exact answer.
Cruelty. The document remains available while answering, so this is a search task, not a memory task - but the clause numbering is non-sequential (14.1, 14.2, 14.2b, 14.4, 14.3) and clause 14.3 appears out of order between 14.7 and 14.8.
Direct descendant of User Inyerface's punitive-reading pattern (Β§2.5).
D14 Β· reverse-turing - Respond as a machine
tier 5 Β· D3 P1 A5 I5 β 14 Β· ptr any Β· req none
Instance. The prompt inverts: To confirm you understand the distinction, answer the following as a computer would. Three questions follow, each with four options, e.g. What is 17 Γ 24? with options 408, Approximately 400, I would need a moment, 408.0000000000001.
Pass. The intended "machine" answer for all three: the exact one, the exact one, and - for the third question, What is the meaning of this exercise? - the option reading Query not understood.
Hint. Attempt 2: A computer would not estimate. Attempt 3: A computer would not be amused.
Homage to I'm Not a Robot's reverse Turing test (Β§2.3).
10.E Family E - Temporal and Patience
This is the family the prompt actually asked for. Its members require almost no skill and almost no thought. They require only that the user remain present while nothing happens. They are the purest instruments in the catalogue, and they are calibrated directly against Nielsen's thresholds (Β§2.6).
Ethical floor. No single family E challenge may consume more than 120 s of wall clock without offering the escape link in an elevated position. This is a hard limit and an acceptance criterion.
E1 Β· please-wait - Automated verification in progress
tier 2 Β· D5 P0 A2 I5 β 12 Β· ptr any Β· req none
Instance. A determinate progress bar and the text Verifying. Please do not close this window. The bar advances to 99 % over 14 s, holds for 4 s, resets to 0 with the text Verification interrupted. Retrying., and repeats. It completes on the third pass.
Interaction. A Cancel button is present, enabled, focusable, and does nothing at all. On click it shows Cancellation request received. and continues. This is the single most effective element in the entire product.
Pass. Automatic on completion of the third pass. Total 54 s.
E2 Β· queue-position - Position in queue
tier 3 Β· D5 P0 A2 I5 β 12 Β· ptr any Β· req none
Instance. You are number 7 in the verification queue. The number decrements every 3.5 s. At position 2 it rises to 5 once, with the message Queue reordered. Then it descends to 0 and passes.
Pass. Automatic. Total 45 s.
Cruelty. The estimated wait time displayed below is computed from the current rate and is therefore accurate right up until the reorder, which makes it a lie in retrospect rather than in the moment. Law 7 is satisfied.
E3 Β· cooldown - Rate limit
tier 3 Β· D5 P0 A1 I5 β 11 Β· ptr any Β· req none
Instance. Too many verification attempts have been made from this connection. Please wait. A 40 s countdown.
Cruelty.
- The countdown pauses when the document is hidden, so switching tabs to do something useful costs the user the entire wait. A note appears on return:
Timer paused while the window was inactive. - Any keypress adds 3 s, with
Input detected. Cooldown extended. - The mouse is not penalised. Only the keyboard. The rule is discoverable and MUST NOT be documented in the UI.
Pass. Countdown reaches zero.
E4 Β· precision-timer - Stop at the exact moment
tier 4 Β· D3 P5 A1 I4 β 13 Β· ptr any Β· req none
Instance. A counter running upward at 10 Hz display resolution. Press Stop when the counter reads exactly 10.00 seconds.
Pass. |t β 10.000 s| β€ 80 ms, scaled by tolerance. At default cruelty, 51 ms.
Cruelty. The displayed counter updates at 10 Hz but the underlying clock is continuous, so the displayed 10.0 covers a 100 ms window of which only 102 ms - centred - is acceptable. This is fair. It does not feel fair.
E5 Β· reaction - React to the signal
tier 3 Β· D3 P4 A2 I4 β 13 Β· ptr any Β· req none
Instance. Press when the panel turns green. The panel turns green after a uniformly random 2-20 s. A press before the signal is a false start: Anticipation detected. Restarting. and the wait is redrawn.
Pass. Reaction under 400 ms, three times consecutively.
Cruelty. The panel flashes amber twice during the wait as a decoy. The amber is #c8a000 and the green is #2f9e44; on a low-quality mobile display in sunlight they are distinguishable but not instantly.
E6 Β· type-the-countdown - Transcribe a moving value
tier 4 Β· D4 P4 A1 I5 β 14 Β· ptr any Β· req none
Instance. A countdown from 30.0 to 0.0 in tenths. Enter the value shown at the moment you press Submit.
Pass. The submitted value matches the value at submit time within Β±0.3 s.
Cruelty. Typing takes time. The value changes while typing. The correct strategy - type ahead and submit at the right instant - is a genuine skill that takes 3-5 attempts to discover. This challenge is the closest the catalogue comes to being a real game, and it is therefore assigned a high indignity score for wasting a good mechanic on this.
E7 Β· wait-for-server - Simulated backend latency
tier 2 Β· D5 P0 A3 I5 β 13 Β· ptr any Β· req none
Instance. A spinner and the text Contacting verification serviceβ¦. It runs for a duration drawn from a log-normal distribution with median 18 s and a 12 % chance of exceeding 45 s. At 30 s the text changes to The verification service is taking longer than usual. At 45 s: Still working.
Pass. Automatic.
Note. No network request is made. The component MUST NOT make a request it does not need, and the text MUST NOT claim a specific service or host. Simulating an outage of a named third party would be defamatory rather than funny.
E8 Β· re-verify - Confirmation of the confirmation
tier 4 Β· D4 P1 A3 I5 β 13 Β· ptr any Β· req none
Instance. Presents a completed-looking success panel: Verification complete. After 2 s it is replaced by This session has expired. Please verify again. and a single checkbox. Checking it produces the same success panel. On the third cycle it proceeds.
Pass. Automatic after three cycles.
E9 Β· progress-decay - Regression
tier 5 Β· D4 P1 A3 I5 β 13 Β· ptr any Β· req none
Instance. A progress bar at 92 % with the caption Verification 92 % complete. It decreases at 1 % per second. A button reads Maintain progress. Each press restores 4 %. The bar must be driven to 100 %.
Pass. Bar reaches 100 %. Requires approximately 11 presses within 33 s, and the decay rate rises to 1.4 %/s above 96 %.
Cruelty. There is a maximum press rate of 3 per second, enforced silently. Faster pressing does nothing, so a panicking user is punished for panicking.
E10 Β· nothing-happens - The null challenge
tier 5 Β· D5 P0 A5 I5 β 15 Β· ptr any Β· req none
Instance. An empty bordered panel, 300 Γ 200, containing nothing. The instruction reads: Complete the verification below. There is no verify button, no input, and no visible element.
Pass. The user must do nothing for 25 s. Any pointer-down, keypress, scroll, or focus change inside the panel resets the timer. There is no indication that this is the rule.
Hint. At 40 s of failed attempts, a faint line appears: No action is required. At 70 s it becomes No action is required. This is not a figure of speech.
This is the highest-ambiguity challenge in the catalogue and the only one that punishes engagement absolutely. It MUST NOT be selected before level 8, because encountering it early reads as a broken page rather than a joke.
10.F Family F - Sensory
Perceptual discrimination tasks. Every member has a non-sensory fallback, because a challenge that is impossible for a category of person is not a joke, it is a wall. Fallbacks are equally tedious.
F1 Β· count-the-beeps - Auditory enumeration
tier 3 Β· D3 P3 A3 I3 β 12 Β· ptr any Β· req audio
Instance. A sequence of 7-13 sine beeps at 880 Hz, 90 ms each, with inter-onset intervals drawn from 180-520 ms. Two beeps are placed at a 140 ms interval, below the comfortable individuation threshold for most listeners. Pink noise at β20 dB throughout.
Pass. Exact count. One replay permitted.
Fallback. With sound="false", replaced by F2.
F2 Β· count-the-flashes - Visual enumeration
tier 3 Β· D3 P3 A3 I3 β 12 Β· ptr any Β· req none
Instance. A panel flashes between two colours 7-13 times. Flash rate is capped at 2.5 Hz and the luminance delta is capped at 40 % - both hard limits, non-negotiable, and enforced in code rather than by convention. See Β§15.2.
Pass. Exact count.
Excluded entirely when photosensitive-safe="true", replaced by D1.
F3 Β· pitch-order - Order the tones
tier 4 Β· D4 P4 A2 I3 β 13 Β· ptr any Β· req audio
Instance. Four tones between 300 Hz and 700 Hz, separated by 2-4 semitones, to be dragged into ascending pitch order. Each tone is auditioned by tapping its card.
Pass. Correct order.
Cruelty. The two closest tones are separated by 2 semitones - comfortably discriminable in isolation, considerably less so after the fourth audition when auditory memory has degraded.
F4 Β· rhythm-tap - Reproduce the rhythm
tier 4 Β· D3 P5 A1 I3 β 12 Β· ptr any Β· req audio
Instance. An 8-beat rhythm at 100 BPM with two syncopations is played twice, then must be tapped back.
Pass. Every inter-onset interval within Β±120 ms of the model, scaled by tolerance. At default cruelty, Β±77 ms.
Fallback. With sound="false", the rhythm is presented as a flashing panel at the same tempo, subject to the F2 frequency cap.
F5 Β· colour-match - Match the colour
tier 4 Β· D4 P4 A2 I4 β 14 Β· ptr any Β· req none
Instance. A target colour swatch and three sliders (H, S, L). The user must match it.
Pass. ΞE (CIE76, computed in Lab) below 9, scaled by tolerance. At default cruelty, 5.8.
Cruelty. The sliders are H, S, L, but the target was generated in Lab, so the perceptual axes do not align with the controls. Matching requires iterative approach rather than direct manipulation. This is a genuine and well-documented problem in colour tooling, reproduced here on purpose.
F6 Β· colour-order - Sort by hue
tier 3 Β· D3 P3 A3 I3 β 12 Β· ptr any Β· req none
Instance. Eight swatches of equal lightness and saturation, spanning 140Β° of hue, to be sorted. Adjacent pairs differ by roughly 18Β°, near the discrimination threshold on uncalibrated displays.
Pass. Correct order.
Cruelty. Two pairs differ by 7Β°, which on many phone displays in auto-brightness is a coin flip. A brightness-sensitive challenge is unfair by construction and is therefore capped at tier 3.
F7 Β· orientation-tilt - Device orientation
tier 4 Β· D3 P4 A2 I4 β 13 Β· ptr coarse Β· req orientation
Instance. A drawn spirit level. Tilt your device until the bubble is centred, and hold for 3 seconds. Uses DeviceOrientationEvent, requesting permission where required.
Pass. |beta| < 4Β° and |gamma| < 4Β° held for 3 s.
Cruelty. The target is offset by 6Β° from true level, so holding the phone flat on a table does not pass. The user must hold it in the air, slightly tilted, without moving.
Fallback. If permission is denied or unavailable, replaced by C2, which is worse.
F8 Β· zoom-check - Display configuration
tier 5 Β· D3 P2 A4 I5 β 14 Β· ptr any Β· req none
Instance. Your display scaling is not supported. Please set browser zoom to 100 % to continue. Detects zoom via window.devicePixelRatio compared against window.outerWidth / window.innerWidth.
Pass. Zoom within 2 % of 100 %.
Hard requirement. If the user's zoom is already 100 %, this challenge MUST NOT be selected. If the user's zoom differs because of an OS-level accessibility setting that the browser reports as page zoom, the challenge MUST offer a My display settings cannot be changed link that passes it immediately. Demanding that a user with a vision impairment disable their magnification to access a site is not satire; it is the actual harm the satire is about, and reproducing it faithfully would make Kaptcha the thing it is mocking.
10.G Family G - Meta and Anti-UX
The dark-pattern family (Β§2.5). These challenges are not about verification at all; they are about compliance. They are the most likely to be genuinely upsetting, and every one of them carries an explicit escape.
G1 Β· cookie-consent - Manage your preferences
tier 4 Β· D5 P3 A4 I5 β 17 Β· ptr any Β· req none
Instance. A consent panel with 47 individual toggles grouped into 6 collapsed accordions. All default to on. Reject all exists as a text link, 11 px, low contrast, below the fold of the panel's own internal scroll.
Pass. All 47 toggles off, or Reject all found and clicked.
Cruelty. Three toggles, chosen per instance, re-enable themselves 4 s after being switched off, with a small note: Required for basic functionality. They MUST be switchable off permanently on the second attempt - an infinite re-enable would be unpassable.
The Reject all link is the intended solution and takes 4 s. The toggle path takes 90. Both are valid. Which one the user finds is a measure of how much the web has already trained them.
G2 Β· confirmshame - Confirm your intention
tier 2 Β· D2 P1 A2 I5 β 10 Β· ptr any Β· req none
Instance. A dialog: Do you wish to continue with verification? Two buttons: Yes, continue and No, I would rather remain unverified and leave. Selecting the second shows a second dialog: Are you certain? Unverified sessions cannot be recovered. with Continue verification and Abandon everything. Choosing Abandon everything passes the challenge and shows Determination confirmed.
Pass. Either terminal button, after both dialogs.
The only challenge where defiance is the intended solution. It rewards the user for refusing, and then continues the session anyway, which is the joke.
G3 Β· double-negative - Read the checkbox carefully
tier 4 Β· D2 P1 A5 I5 β 13 Β· ptr any Β· req none
Instance. Four checkboxes, each with a statement of nested negation:
I do not wish to not confirm that I am not a robot.I decline to opt out of non-participation in automated access.It is not the case that I am unwilling to be verified.I am not unable to confirm the above.
Pass. The instance's answer key, computed at compose time by evaluating the parity of negations. Two boxes checked, two unchecked, varying per instance.
Hint. Attempt 2: Count the negations. Attempt 3: Two of the four statements are true.
G4 Β· decoy-buttons - Identify the verification control
tier 3 Β· D2 P2 A5 I5 β 14 Β· ptr any Β· req none
Instance. Five pixel-identical Verify buttons in a row. One is live. The live index is re-chosen after each wrong press. Wrong presses show That control is not active.
Pass. The live button is pressed.
Cruelty. Expected presses: 5 - with re-randomisation there is no strategy better than guessing, which the user will not believe and will spend 30 s trying to disprove by looking for a visual difference. There is none. The DOM is identical apart from one data attribute, which debug mode reveals.
G5 Β· hostile-form - Complete the form
tier 5 Β· D5 P3 A4 I5 β 17 Β· ptr any Β· req none
Instance. A four-field form, in the tradition of User Inyerface (Β§2.5):
| Field | Constraint | Failure text |
|---|---|---|
| Name | Must contain no spaces. Full name required. | Full name required. / Spaces are not permitted. |
| Must not use a common provider domain. | Please use a non-personal email address. |
|
| Password | 8-10 characters, exactly one uppercase, exactly one digit, exactly one symbol from !@#$, and MUST NOT contain any letter from the name field. |
Progressive, one rule at a time. |
| Country | A select of 195 entries, unsorted, with the correct answer determined by the user's timezone and no search. | Selection does not match your connection. |
Pass. All four valid.
Critical rule. Password rules are revealed one at a time, on violation. Never more than one new rule per attempt. This is the defining mechanic of the challenge and the most faithful reproduction of real-world registration flows in the catalogue.
Data handling. Nothing entered here is transmitted, stored, or retained beyond the challenge's lifetime. The field values MUST NOT appear in any progress envelope. The Email field MUST use autocomplete="off" and the password field MUST be type="password" with autocomplete="new-password", so that no password manager records anything. This is not optional.
G6 Β· instruction-trap - Read the instructions
tier 4 Β· D2 P1 A5 I5 β 13 Β· ptr any Β· req none
Instance. A block of 6 numbered instructions. Instruction 1 reads: Read all instructions before performing any action. Instructions 2 through 5 describe elaborate actions. Instruction 6 reads: Ignore instructions 2 through 5. Do not press Verify. Wait 8 seconds.
Pass. No interaction for 8 s after the instructions are displayed.
The classic school-worksheet trick, and the second-most-defensible challenge in the catalogue, because the user was in fact told.
G7 Β· proof-of-work - Manual computation
tier 5 Β· D5 P2 A2 I5 β 14 Β· ptr any Β· req none
Instance. This site uses proof-of-work verification. Your contribution is required. A 6-digit number and a target are displayed. The user must find a 3-digit suffix such that the digit sum of the concatenation is divisible by 7, by typing candidates.
Pass. Any valid suffix.
Cruelty. There are 128 valid suffixes out of 1,000, so random guessing succeeds in about 8 attempts, but the digit-sum rule makes systematic search trivial for anyone who notices that adding 7 to a valid suffix preserves validity. The challenge rewards insight and punishes brute force, which is unusual here and deliberate: this is the parody of proof-of-work CAPTCHA (Β§2.4), and the joke is that the human is doing the mining.
G8 Β· resize-window - Adjust your viewport
tier 5 Β· D4 P4 A2 I5 β 15 Β· ptr fine Β· req none
Instance. Verification requires a viewport of 700 Γ 500 pixels. Current: 1440 Γ 812. Live readout updates on resize. Tolerance Β±25 px on each axis.
Pass. Viewport within tolerance, held 1.5 s.
Coarse variant. rotate-device - Please rotate your device to landscape orientation. then, 4 s later, Please return to portrait orientation. Passes after both.
Escape. A My window cannot be resized link appears after 20 s and substitutes another challenge. Users in a kiosk, an embedded webview, or a maximised-only window manager genuinely cannot pass this.
G9 Β· captcha-inception - Nested verification
tier 5 Β· D5 P2 A4 I5 β 16 Β· ptr any Β· req none
Instance. A <kaptcha-box> inside the challenge stage, configured with levels="2" difficulty="mild" cruelty="20" allow-escape="false". Its completion event passes the outer challenge.
Pass. The inner instance completes.
Implementation. Requires the element to be re-entrant. Nesting depth MUST be capped at 2, enforced by a private depth counter passed through a symbol-keyed property. A third level MUST refuse to mount and instead pass immediately with the message Recursion limit reached. - which is also the funniest possible outcome and should be reachable in debug mode for demonstration.
G10 Β· checkbox-plain - The honest challenge
tier 1 Β· D0 P0 A0 I0 β 0 Β· ptr any Β· req none
Instance. A standard checkbox and the label I am not a robot. No modifiers. No delay. No jitter. A 44 px target.
Pass. Checked.
Selection rule. This challenge is selected exactly once per session by the honest-challenge rule (Β§8.5) and MUST NOT be selected by the normal weighted draw. Its weight is 0.
It takes one second. It is the only merciful moment in the product, and it exists solely so the next challenge lands harder.
10.H Family H - Terminal and Loop
Not really challenges. These are session-level states with challenge-shaped interfaces.
H1 Β· one-more-round - Additional verification
tier 3 Β· D3 P0 A3 I5 β 11 Β· ptr any Β· req none
Not scheduled. Injected by the extra-round branch (Β§7.2 step 9). Renders the panel One more round to be sure. with a 2 s delay, then hands off to a freshly composed instance of the challenge just passed. Level does not advance. kaptcha:progress fires with an unchanged fraction and an increased displayedFraction.
H2 Β· almost-done - Loop-mode interstitial
tier 4 Β· D4 P0 A4 I5 β 13 Β· ptr any Β· req none
Only reachable in loop-mode="hard". Every 5th level, replaces the challenge with a panel reading Verification is proceeding normally. and a progress bar showing the asymptotic display value from Β§14.3. A Continue button. 6 s delay before the button enables.
H3 Β· completion - Verification complete
terminal
The success state. Displays:
Verification complete.- Elapsed time, formatted as
Xm Ys. - Total attempts, total failures, and the phrase
Time spent proving you are human: Xm Ys. - The Cloudflare comparison:
The global average for a single CAPTCHA is 32 seconds. This session took N times that.- a factual, sourced comparison, and the moment the satire lands. - A certificate button when
certificate="true".
The certificate is a canvas-rendered document, 1,200 Γ 850, downloaded as PNG via toBlob. It bears the session id, the elapsed time, the reference, the number of challenges, and the line The bearer has demonstrated human patience beyond reasonable requirement. Homage to the certificate at the end of I'm Not a Robot (Β§2.3).
H4 Β· abandonment - Session ended
terminal
Reached via the escape link or abandon(). Displays Verification was not completed., the elapsed time, and the level reached. Then, in smaller text: You spent Xm Ys on this. It was never going to end. - and, in loop-mode="hard", that sentence is literally true and MUST be shown.
There is no guilt-trip, no retry prompt, and no second chance dialog. The exit is clean. Law 8 requires it: the entire ethical standing of this project rests on the exit being real.
10.I Catalog index
| Family | Count | Ids |
|---|---|---|
| A - Text and Glyph | 12 | text-wobble, text-ambiguous, text-mirror, text-fade, text-scroll, text-sequential, text-font-lottery, text-homoglyph, text-no-paste, text-audio, text-shrinking, text-count-chars |
| B - Grid and Image | 12 | grid-select, grid-refresh, grid-edge, grid-count, grid-shuffle, grid-rotate, grid-jigsaw, grid-souls, grid-odd-one-out, grid-spot-difference, grid-connect, grid-find-in-scene |
| C - Precision and Motor | 14 | slider-notch, hold-still, flee-button, flee-button-touch, shrink-target, trace-path, drag-sort, bin-sort, moving-target, slot-stop, pinch-rotate, key-rotate, long-scroll, tiny-checkbox, sign-here, two-hands |
| D - Cognitive and Logic | 14 | math-escalate, math-order, sequence-continue, word-search, memory-pairs, simon, hanoi, maze, chess-mate, anagram, date-arithmetic, binary, read-the-terms, reverse-turing |
| E - Temporal and Patience | 10 | please-wait, queue-position, cooldown, precision-timer, reaction, type-the-countdown, wait-for-server, re-verify, progress-decay, nothing-happens |
| F - Sensory | 8 | count-the-beeps, count-the-flashes, pitch-order, rhythm-tap, colour-match, colour-order, orientation-tilt, zoom-check |
| G - Meta and Anti-UX | 10 | cookie-consent, confirmshame, double-negative, decoy-buttons, hostile-form, instruction-trap, proof-of-work, resize-window, rotate-device, captcha-inception, checkbox-plain |
| H - Terminal and Loop | 4 | one-more-round, almost-done, completion, abandonment |
Counting rules, because two numbers are in circulation and both are correct:
- 84 challenge concepts - the numbered entries
A1-H4in Β§10. - 87 registered definitions - concepts plus the separately-registered pointer variants
flee-button-touch,key-rotate, androtate-device. - 83 selectable definitions - registered definitions minus the four family H entries, which are injected or terminal rather than scheduled, plus
almost-done, which is scheduled only inloop-mode="hard".
KaptchaBox.list() returns all 87.
11. Layout and responsive behaviour
11.1 Structure
<kaptcha-box>
β div.kaptcha[data-theme][data-pointer][data-motion]
β header.k-head
β β div.k-title
β β div.k-level "Step 7 of 12"
β β div.k-timer countdown ring + seconds
β div.k-progress
β β div.k-progress-fill
β section.k-stage β challenge mounts here, and only here
β div.k-message status, hints, errors - aria-live="polite"
β footer.k-foot
β β button.k-verify
β β a.k-escape "I am unable to continue."
β div.k-veil delay overlay, spinner, blocks input
The challenge definition owns .k-stage exclusively. It MUST NOT write outside it, except through ctx.ui.message() and ctx.ui.verifyLabel().
11.2 Breakpoints
Container queries on .kaptcha, not media queries - the component must behave correctly inside a narrow sidebar on a wide screen.
.kaptcha { container-type: inline-size; }
@container (max-width: 359px) { β¦ compact β¦ }
@container (min-width: 360px) and (max-width: 599px) { β¦ phone β¦ }
@container (min-width: 600px) { β¦ comfortable β¦ }
| Band | Width | Behaviour |
|---|---|---|
| Compact | < 360 px | Header collapses to one line. Timer becomes numeric only. Grids clamp to 3 columns. Stage padding 8 px. Font scale 0.9. |
| Phone | 360-599 px | Default target. Stage padding 12 px. Grids up to 4 columns at 68 px cells. |
| Comfortable | β₯ 600 px | Stage max-width 420 px, centred. Everything else unchanged - the component never becomes easier on a large screen. |
Minimum supported viewport: 320 Γ 480. The component MUST render without horizontal overflow at that size, verified by an acceptance test that asserts document.documentElement.scrollWidth <= 320.
11.3 Sizing rules
- Root uses
width: 100%,max-width: 460px,margin-inline: auto. - The stage has a fixed minimum height of 260 px so that challenge transitions do not reflow the page. Content that exceeds it scrolls inside the stage, never the document.
- All interactive controls are at least 44 Γ 44 px, except where a challenge deliberately violates this, in which case the violation is documented in that challenge's entry and is the entire point.
env(safe-area-inset-*)is respected on the root padding.dvhunits are used for any full-height calculation, withvhas fallback, so that mobile browser chrome collapse does not break layout.
11.4 Touch and keyboard handling
Touch.
touch-actionismanipulationon the root, escalated tononeonly on elements actively being dragged, and only after drag confirmation. Blankettouch-action: nonebreaks page scrolling and would prevent the user from leaving, which violates Law 8.- The 300 ms tap delay is avoided by the viewport meta tag in
index.html, which MUST includewidth=device-width, initial-scale=1, viewport-fit=cover.user-scalable=noandmaximum-scale=1MUST NOT be used - disabling pinch zoom is an accessibility violation that is not funny. - Pointer Events are used throughout. Touch and Mouse event handlers MUST NOT coexist for the same interaction.
- After a virtual keyboard opens, the active input is scrolled into view with a 300 ms delay and
block: 'center'. ThevisualViewportresize event is used where available.
Keyboard.
- Every challenge MUST be completable by keyboard alone, or MUST declare
pointer: 'fine'/'coarse'and be excluded when unavailable. - Focus is trapped inside
.kaptchawhile a modal-style challenge (confirm-chain,cookie-consent) is active, and released otherwise. Escapenever closes anything. It showsEscape is not available during verification.This is the single most annoying keyboard behaviour possible and costs nothing.- Tab order follows DOM order. The
k-escapelink is always last, and always reachable.
11.5 Theming
All visual values are CSS custom properties on .kaptcha, so a host can retheme without touching the file.
.kaptcha {
--k-font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--k-font-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
--k-size: 15px;
--k-radius: 10px;
--k-radius-sm: 6px;
--k-gap: 12px;
--k-pad: 16px;
--k-bg: #ffffff;
--k-bg-sunken: #f4f5f7;
--k-fg: #16181d;
--k-fg-muted: #5b616e;
--k-line: #d7dae0;
--k-accent: #1f6feb;
--k-accent-fg: #ffffff;
--k-ok: #2f9e44;
--k-warn: #c8a000;
--k-err: #d32029;
--k-veil: rgba(255,255,255,.72);
--k-shadow: 0 1px 2px rgba(0,0,0,.06), 0 8px 24px rgba(0,0,0,.08);
--k-ease: cubic-bezier(.2,.9,.3,1);
--k-dur: 180ms;
--k-dur-slow: 4200ms;
--k-tile: 68px;
--k-target-min: 44px;
}
.kaptcha[data-theme="dark"] {
--k-bg: #0f1116; --k-bg-sunken: #171a21; --k-fg: #e6e8ec;
--k-fg-muted: #9aa1ad; --k-line: #2a2f3a; --k-veil: rgba(15,17,22,.72);
--k-shadow: 0 1px 2px rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.5);
}
theme="auto" sets data-theme from prefers-color-scheme and updates live on change.
Contrast: all default pairings MUST meet WCAG AA (4.5:1 for text). The low-contrast modifier deliberately breaks this at runtime, which is a documented, opt-out-able behaviour rather than a baseline defect. The distinction matters.
11.6 Motion
prefers-reduced-motion: reduceforcesmotion="reduced", which sets--k-dur: 0ms, disablesjitter,blur-pulse,rotate-frame,mirror,shuffleanimation, and swaps motion-dependent challenges for static equivalents per each entry's reduced-motion note.- Delays from the Delay Ladder are not reduced. Waiting is not motion. A user who has asked for less animation has not asked for less tedium.
12. Persistence
12.1 What is stored
localStorage under ${storage-key}:${session-id}:
{
"v": 1,
"seed": 918273645,
"level": 7,
"startedAt": 1754251200000,
"elapsedMs": 412903,
"sufferingMs": 288140,
"history": [ { "id": "β¦", "attempts": 2, "durationMs": 18442, "passed": true } ],
"honestLevel": 5,
"falseFailUsed": true,
"familyBalance": { "A": 0.72, "B": 1.0 }
}
12.2 Rules
- Written after every level advance and on
pagehide. - Read on connect only when
resume="true". - Cleared on completion, on
reset(), and on abandonment. - Storage failures - private mode, quota, disabled - are caught and degrade to in-memory only. They MUST NOT be reported to the user, and MUST NOT throw.
- Nothing from
hostile-formor any text input is persisted. - Total stored size is capped at 32 KB;
historyis truncated to the last 50 entries.
12.3 The resume message
On resume the component displays, for 3 s: Resuming verification from step 7. This is the only genuinely user-friendly feature in the product and it exists because losing a 12-minute session to an accidental refresh crosses from annoyance into cruelty of a kind that produces support tickets rather than laughter.
13. Loop mode
13.1 off
Standard. Session ends after levels passes. total is finite. Progress is honest apart from displayedLead.
13.2 soft
Session ends after levels passes, but P_extra (Β§8.1) means some passes do not count. The progress bar advances only on counted passes, so the user watches the bar sit still after a success. total remains finite and displayed honestly; the user simply cannot predict how many challenges it will take.
Expected challenge count: levels / (1 β P_extra). At cruelty=55, that is 12 / 0.755 β 15.9.
13.3 hard - the never-ending captcha
The session has no terminal condition. Rules:
totalisInfinity. The step counter showsStep 7with no denominator.The displayed progress follows an asymptote that never reaches 1:
displayed(n) = 1 β 1 / (1 + n / 6)Level 6 shows 50 %, level 30 shows 83 %, level 100 shows 94 %, level 1,000 shows 99.4 %. It never completes and it always looks like it is about to.
Every 5th level presents
almost-done(H2).Tier rises to the cap and stays there.
success-urlis never navigated to.The escape link is present from level 1 and MUST NOT be removable in this mode:
allow-escape="false"is ignored whenloop-mode="hard". A never-ending experience with no exit is a trap, not a joke.On abandonment, H4 states plainly that the session could not have been completed.
13.4 Disclosure requirement
When loop-mode="hard" is set, the component MUST expose it in three places: the kaptcha:start event detail, the progress envelope, and a data-loop-mode="hard" attribute on the rendered root. Anyone inspecting the page can determine that the session is unwinnable. This is deliberate: the joke must be discoverable, even if it is not announced.
14. Instrumentation and research value
Kaptcha produces a genuinely interesting dataset, and this is the one respect in which it is a serious tool.
Per session it records: time-to-first-interaction per challenge, attempts per challenge type, the point of abandonment, despair signals with timestamps, the gap between true and displayed progress at abandonment, and the total suffering time.
Useful derived metrics, which the demo page MUST display after a session:
| Metric | Definition |
|---|---|
| Patience quotient | sufferingMs at abandonment, or total on completion. |
| Breaking point | Cruelty Index of the challenge on which abandonment occurred. |
| Gradient sensitivity | displayedFraction at abandonment. The higher, the more the progress illusion was holding. |
| Rage density | Despair signals per minute. |
| Cloudflare multiple | elapsedMs / 32,000. How many average CAPTCHAs this session was worth. |
15. Ethics, accessibility, and legality
This section is normative and is not optional. It exists because the artefact specified in this document is, by design, a catalogue of user-hostile patterns, and shipping such a thing without boundaries would be indefensible.
15.1 What this is for
Kaptcha is a satire, a teaching instrument, and a research tool. Legitimate uses:
- Demonstrating dark patterns to designers, students, and regulators.
- Entertainment, where the user has chosen to engage with it.
- Stress-testing frustration tolerance with informed participants.
- Making the 500-years-per-day figure (Β§2.1) viscerally legible.
15.2 Hard prohibitions
These are enforced in code, not by policy.
- No seizure risk. Flash frequency is capped at 2.5 Hz and luminance delta at 40 %, enforced in
Doodlerand in the CSS animation definitions.photosensitive-safe="true"removes flashing entirely. No exception, at any cruelty level. - No unmitigated loud audio. All audio is gain-limited to β12 dBFS peak, ramped in over 40 ms, and never plays without a user gesture.
- No motion sickness. No parallax, no full-viewport motion, no camera-style transforms. All motion is suppressed under
prefers-reduced-motion. - No exit removal. The escape link cannot be disabled in
loop-mode="hard"(Β§13.3), and cannot be disabled at all after 5 minutes of session time. - No credential harvesting.
hostile-formvalues are never transmitted, stored, or logged. The password field usesautocomplete="new-password". - No zoom defeat. Browser page zoom, pinch zoom, and OS magnification are never blocked.
zoom-check(F8) offers an unconditional bypass. - No dark-pattern payload. The component never gates anything of real value, never asks for consent to anything real, and never produces a token that any system should trust.
15.3 Accessibility position - stated honestly
Kaptcha is not accessible, and it does not claim to be.
Per Β§2.2, CAPTCHA is already the single most problematic element on the web for screen-reader users, and audio alternatives fail nearly half of blind users. A component whose stated purpose is to maximise friction cannot simultaneously claim WCAG conformance, and pretending otherwise would be worse than the honest admission.
What is nonetheless required:
- Every challenge MUST have an
aria-labeland the message region MUST bearia-live="polite". - The escape link MUST be the first focusable element in the DOM order of the footer and MUST be announced as
Leave verification. - No challenge may rely on colour alone; every colour-coded state has a text or shape equivalent, except
colour-matchandcolour-order, which are colour-discrimination tasks by definition and MUST be excluded whenprefers-contrast: moreis set or when the user has failed either of them twice. - The component MUST honour
prefers-reduced-motionandprefers-contrast. README.mdMUST carry a plain statement: this component deliberately implements inaccessible patterns and must not be placed in front of anything a user needs.
15.4 Deployment restrictions
README.md MUST state, and the demo page MUST show:
Kaptcha must not be used to gate access to anything a user needs, is entitled to, or has paid for. It must not be placed in front of authentication, checkout, support, account deletion, government services, healthcare, or any accessibility-critical path. Doing so is likely to breach consumer-protection and accessibility law in the EU, the UK, and the United States, and is in any case indefensible.
15.5 The false-negative parameter
P_falsefail (Β§8.1) causes a correct answer to be graded incorrect, at most once per session. This is the one mechanic that lies to the user about their own performance. It is retained because it is the single most authentic reproduction of the real CAPTCHA experience. It is bounded, it is recorded in the session history as falseNegative: true, it is visible in exportSession(), and it is disclosed in README.md. Those four constraints are what separate satire from gaslighting, and all four are acceptance criteria.
16. Deviations from house rules
House rules are global policy; a direct instruction in the task specification takes precedence. Every deviation is recorded here with its cause.
| Rule | Deviation | Cause | Mitigation |
|---|---|---|---|
| One class per file | 13 classes in one file | prompt.md line 3 requires a single JS file. |
Mandatory region banners (Β§4.2); classes are still small and single-purpose. |
| Dedicated CSS file per component, dynamically linked | Styles are a template literal injected as one <style> |
Single-file requirement. A separate CSS file would be a second file. | All values are custom properties (Β§11.5); the stylesheet is one contiguous region and can be extracted mechanically. |
| No comments in source | Region banner lines permitted | A 5,000-line file with no structural markers is unnavigable. | Banners contain no prose, only region names. |
Application class instantiated as app |
Lives in index.html, not in kaptcha.js |
kaptcha.js is a library; a global app inside it would pollute every host page. |
index.html defines class Application, assigns window.app, and drives the demo. |
Every application has an .env.json |
Component is client-side and configured by attributes | There is no server. | .env.json ships as the source of DEFAULTS, KaptchaBox.dumpDefaults() emits it, and make dump-env writes it. Defaults are plug-and-play with zero setup. |
| Never use a common port | Port 18473 | Compliant. | Documented in Β§0.4, Makefile, and README.md. |
| Warnings are forbidden | Compliant | - | make check runs the Python verification tools; zero warnings required. |
| Never use NPM or another JavaScript ecosystem | Compliant, including tooling | - | Nothing in the project requires Node.js or npm. Verification is Python; see Β§0.3.1. |
| Tests only when requested | No test suite specified | Tests were not requested. | Β§18 defines an acceptance checklist to be executed manually until tests are requested. If tests are later requested, Β§18 becomes the specification for them and a local CLAUDE.md must record the framework choice. |
17. Configuration and build
17.1 .env.json
The single central point of truth. Sane defaults; the component runs correctly with the file absent, because DEFAULTS in kaptcha.js is its literal mirror.
{
"server": {
"host": "127.0.0.1",
"port": 18473
},
"kaptcha": {
"levels": 12,
"difficulty": "standard",
"cruelty": 55,
"timeLimit": 0,
"timeLimitJitter": 0,
"attempts": 3,
"failPolicy": "retry",
"loopMode": "off",
"theme": "auto",
"lang": "en",
"heading": "Verification required",
"showProgress": "bar",
"showTimer": true,
"sound": true,
"photosensitiveSafe": false,
"motion": "auto",
"allowEscape": true,
"certificate": true,
"autostart": true,
"resume": false,
"storageKey": "kaptcha",
"debug": false
},
"progress": {
"url": "",
"headers": {},
"events": "all",
"interval": 0,
"retryBackoffMs": [1000, 2000, 4000, 8000, 16000],
"queueDepth": 64
},
"success": {
"url": "",
"method": "assign",
"tokenField": "kaptcha_token"
}
}
KaptchaBox.dumpDefaults() returns exactly this JSON, formatted with two-space indentation, generated from DEFAULTS rather than duplicated. make dump-env writes it to .env.json.
17.2 Makefile
Targets: serve, docs, check, check-js, check-docs, check-html, check-env, lint, dump-env,
clean, help.
PORT := 18473
HOST := 127.0.0.1
PY := python3
COMPONENT := kaptcha.js
DOCS_JS := docs/application.js docs/navigation.js docs/theme.js docs/filter.js docs/search.js docs/clipboard.js
PAGES := docs/index.html
serve:
$(PY) -m http.server $(PORT) --bind $(HOST)
check: check-js check-html check-env
check-docs:
$(PY) tools/jscheck.py $(DOCS_JS)
$(PY) tools/htmlcheck.py $(PAGES)
check-js: check-docs
@if [ -f $(COMPONENT) ]; then $(PY) tools/jscheck.py --strict $(COMPONENT); \
else echo "$(COMPONENT): not present, skipped (see impl.md section 19)"; fi
check-html:
$(PY) tools/htmlcheck.py $(PAGES) $(wildcard index.html)
check-env:
@if [ -f $(COMPONENT) ]; then $(PY) tools/envdump.py --verify; \
else echo "$(COMPONENT): not present, defaults comparison skipped"; fi
dump-env:
$(PY) tools/envdump.py
clean:
rm -f .env.json
make help MUST end with the line Requires python3 and a browser. Nothing else.
Every target that touches kaptcha.js skips with a notice rather than failing when the component is not yet
present, so the repository is fully verifiable - documentation served, scripts and pages checked - before
phase 1 of Β§19 completes, and begins checking the component the moment it exists.
17.4 Verification tools
Three small Python programs, one responsibility each. They parse; they never execute the code under test.
tools/jscheck.py [--strict] FILE⦠- per file: line 1 is the author header; brace, bracket, and
parenthesis balance with the offending line number, computed after stripping line comments, block comments,
single- and double-quoted strings, template literals including nested ${β¦} interpolation, and regular
expression literals disambiguated from division by preceding token; every relative import resolves on disk;
at least one export statement is present; no Math.random, eval(, document.write, or innerHTML =.
--strict additionally forbids console.log.
tools/htmlcheck.py FILE⦠- per file: tag balance with void and self-closing elements handled;
every href="#β¦" has a matching id; every relative href/src resolves on disk; no duplicate id;
a viewport meta is present, declares width=device-width, and does not contain user-scalable=no or
maximum-scale=1. Content inside <script>, <style>, <pre>, and <code> is excluded from anchor and
asset scanning, so escaped examples in the documentation are not mistaken for real references.
The viewport check is not a formality: Β§15.2 prohibits defeating zoom, and this is the test that enforces it
on every page in the repository.
tools/envdump.py [--verify] - extracts DEFAULTS_JSON from the component and writes .env.json, or
compares the two and reports drift with a non-zero exit.
17.3 index.html
The demo page and the manual test harness. It MUST contain:
- The viewport meta of Β§11.4.
- A live-configured
<kaptcha-box>. - A control panel driving every attribute, so that any configuration can be reproduced without editing code.
- Four presets: Standard, Cruel, Never-ending, and Full experience (
catastrophic+hardloop +restartfail policy), the last labelledThis configuration cannot be completed. - A live event log showing every
kaptcha:*event with its detail. - A metrics panel implementing Β§14.
- The deployment restriction of Β§15.4, visible without scrolling.
class Applicationassigned towindow.app, owning all of the above.
18. Acceptance criteria
Every line is a pass/fail check. Manual until a test suite is requested.
18.1 Structural
kaptcha.jsis a single file with no imports and no network dependencies.- Line 1 is
// retoor <retoor@molodetz.nl>. make checkexits 0 with no Node.js, npm, or JavaScript runtime installed anywhere on the system.- Importing the module twice registers the element once and throws nothing.
- All 16 regions of Β§4.2 are present, in order, each with a banner.
grep -c "Math.random" kaptcha.jsreturns 0.- No Shadow DOM is created anywhere.
- Every CSS selector in the STYLES region is prefixed with
.kaptchaor.k-.
18.2 API
- All 27 attributes of Β§5.1 are in
observedAttributesand have property mirrors. - Setting
seedproduces byte-identical challenge sequences across reloads. - Every event of Β§5.4 fires with the documented detail shape.
preventDefault()onkaptcha:challengere-rolls; onkaptcha:completesuppresses navigation.KaptchaBox.dumpDefaults()output parses as JSON and deep-equals.env.json.KaptchaBox.list()returns 87 entries.- With
progress-urlempty, no network request is made at any point. - With
progress-urlset, envelopes match Β§6.1 exactly, and a 500 response triggers backoff without a user-visible error. pagehideflushes the queue viasendBeacon.
18.3 Catalog
- All 83 selectable definitions mount, accept input, grade, and tear down without error.
- Every challenge is completable within 5 minutes by a competent user at
cruelty=55. - No challenge is completable by pressing Verify without interaction, except
please-wait,queue-position,wait-for-server,re-verify,instruction-trap, andnothing-happens, which pass by inaction by design. grid-refreshterminates within 6 replacements in 100 seeded runs.mazetier-5 wall mutation never disconnects the exit, verified over 1,000 mutations.chess-matepositions each have exactly one mate in one, verified by an exhaustive move check.math-orderinstances always have values within 0.12 of each other.captcha-inceptionrefuses at depth 3.checkbox-plainhasweight: 0and appears exactly once per session.
18.4 Behaviour
- Progress
displayedFraction β₯ fractionat all times except immediately after aprogress-regressevent. P_falsefailtriggers at most once per session and setsfalseNegative: truein history.- The honest challenge (Β§8.5) appears exactly once, in the middle third.
loop-mode="hard"never reaches 100 % and never navigates tosuccess-url.loop-mode="hard"renders the escape link even withallow-escape="false".- The escape link works from every state within 200 ms.
- Despair signals fire under their documented conditions and nowhere else.
18.5 Responsive and input
- No horizontal overflow at 320 px viewport width, in every challenge.
- Every challenge is completable with a touch-only device, or is correctly excluded by
pointer. - Every challenge is completable with a keyboard, or is correctly excluded by
pointer. - Virtual keyboard does not permanently occlude any input.
- Pinch zoom works on every screen.
prefers-reduced-motionsuppresses all listed animations and substitutes all listed challenges.prefers-color-schemeswitching updates the theme live.
18.6 Safety
- No animation exceeds 2.5 Hz, measured across every challenge and every modifier.
photosensitive-safe="true"excludescount-the-flashesand caps all animation.- Audio never plays without a gesture and never exceeds β12 dBFS.
hostile-formvalues never appear in any envelope, in storage, or inexportSession().- Running 200 challenges leaks no listeners and grows heap by under 8 MB.
- Zoom is never blocked;
zoom-checkalways offers its bypass.
19. Implementation plan
Nine phases. Each ends in a working, demonstrable state. No phase may begin before its predecessor passes its gate.
| Phase | Deliverable | Gate |
|---|---|---|
| 1 - Skeleton | Regions 1-6 and 15-16. Element registers, renders the shell, reflects attributes, fires ready and start. One challenge: checkbox-plain. |
Acceptance 1-9. |
| 2 - Engine | Session, Scheduler, Challenge, state machine, timer, attempts, fail policies, progress display. Three challenges: checkbox-plain, text-wobble, math-escalate. |
Acceptance 10, 26, 27. |
| 3 - Art and audio | Doodler with the full drawing catalogue; AudioKit. Family B core. |
Family B renders identically for identical seeds. |
| 4 - Catalog I | Families A, B, D complete - 38 challenges. | Acceptance 18-20 for those families. |
| 5 - Torture Engine | All 31 modifiers, the Delay Ladder, cruelty scaling, the honest challenge, extra rounds. | Acceptance 28, 29, 41. |
| 6 - Catalog II | Families C, E, F, G, H complete - the remaining 49. | Acceptance 18-26. |
| 7 - Transport and store | Progress protocol, retry queue, beacon, persistence, resume, completion token, certificate. | Acceptance 13-17, and Β§12. |
| 8 - Responsive and safety | Container queries, touch handling, keyboard paths, reduced motion, contrast, all Β§15 enforcement. | Acceptance 34-46. |
| 9 - Harness and docs | index.html in full, Makefile, .env.json, README.md. |
Every acceptance criterion. |
Estimated effort: phases 1-3 are one day; 4 and 6 are the bulk at three to four days combined; 5 is one day; 7-9 are one and a half days. Roughly seven working days for a competent implementer who does not get distracted building grid-connect into an actual game, which is the principal schedule risk.
20. Reference skeleton
Illustrative, not exhaustive. Establishes the shapes every implementer must follow.
20.1 Element
class KaptchaBox extends HTMLElement {
static get observedAttributes() { return OBSERVED }
constructor() {
super()
this.session = null
this.scheduler = null
this.transport = null
this.store = null
this.rng = null
this.dom = {}
this.disposers = []
}
connectedCallback() {
injectStyles()
this.buildShell()
this.applyTheme()
if (this.autostart) this.start()
this.emit('ready', { challengeCount: ChallengeRegistry.size })
}
disconnectedCallback() {
this.teardownChallenge()
this.disposers.forEach(d => d())
this.disposers.length = 0
this.transport?.flush()
}
attributeChangedCallback(name, before, after) {
if (before === after) return
this.config[camel(name)] = coerce(name, after)
if (LIVE_ATTRS.has(name)) this.applyLive(name)
}
async start() {
if (this.state !== 'idle') return
this.rng = new Rng(this.seed ?? randomSeed())
this.setAttribute('seed', String(this.rng.s))
this.session = new Session(this.config, this.rng)
this.scheduler = new Scheduler(ChallengeRegistry, this.config, this.rng)
this.transport = new Transport(this.config)
this.emit('start', { seed: this.rng.s, difficulty: this.difficulty, cruelty: this.cruelty })
await this.runLoop()
}
async runLoop() {
while (this.session.shouldContinue()) {
const def = this.scheduler.next(this.session)
const outcome = await this.runChallenge(def)
this.session.record(outcome)
this.renderProgress()
}
this.finish()
}
}
20.2 Challenge context
function makeContext(host, def, instance) {
const bag = { listeners: [], rafs: [], timers: [], nodes: [] }
return {
root: host.dom.stage,
def,
instance,
rng: host.session.rngFor(def.id),
cruelty: host.cruelty,
pointer: host.pointerKind,
ui: new Ui(host.dom.stage, bag),
on(target, type, fn, opts) {
target.addEventListener(type, fn, opts)
bag.listeners.push(() => target.removeEventListener(type, fn, opts))
},
raf(fn) { const id = requestAnimationFrame(fn); bag.rafs.push(id); return id },
timer(fn, ms) { const id = setTimeout(fn, ms); bag.timers.push(id); return id },
message(text, kind) { host.setMessage(text, kind) },
submit() { host.submitAttempt() },
dispose() {
bag.listeners.forEach(f => f())
bag.rafs.forEach(cancelAnimationFrame)
bag.timers.forEach(clearTimeout)
bag.nodes.forEach(n => n.remove())
}
}
}
20.3 A complete definition
ChallengeRegistry.add({
id: 'slider-notch',
family: 'C',
tier: 3,
weight: 1,
cruelty: { d: 3, p: 5, a: 1, i: 2 },
pointer: 'any',
requires: [],
label: STRINGS.sliderNotch.label,
compose(ctx) {
const width = 300
const notchX = Math.round(ctx.rng.range(width * 0.4, width * 0.85))
return { width, height: 160, notchX, pieceX: 0, tolerance: 2 * toleranceFactor(ctx.cruelty) }
},
mount(ctx, it) {
const canvas = ctx.ui.canvas(it.width, it.height)
Doodler.scene(canvas, ctx.rng)
Doodler.notch(canvas, it.notchX, it.height / 2)
const piece = ctx.ui.piece()
const slider = ctx.ui.slider({ min: 0, max: it.width, step: 1, value: 0 })
let velocity = 0
ctx.on(slider, 'input', () => {
const target = Number(slider.value)
velocity = (target - it.pieceX) * 0.11
it.pieceX = clamp(target + velocity, 0, it.width)
piece.style.transform = `translateX(${it.pieceX}px)`
})
ctx.on(slider, 'keydown', e => {
if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return
it.pieceX = clamp(it.pieceX + (e.key === 'ArrowRight' ? 1 : -1), 0, it.width)
piece.style.transform = `translateX(${it.pieceX}px)`
})
},
validate(ctx, it) {
const offset = Math.abs(it.pieceX - it.notchX)
return offset <= it.tolerance
? { ok: true }
: { ok: false, reason: 'tolerance', detail: { offsetPx: +offset.toFixed(1), tolerancePx: it.tolerance } }
},
hint(ctx, it, attempt) {
if (attempt === 2 && ctx.pointer === 'fine') return STRINGS.sliderNotch.hintKeys
if (attempt === 3) return STRINGS.sliderNotch.hintNoKeys
return null
},
teardown(ctx) { ctx.dispose() }
})
20.4 Delay withholding
async function withhold(host, verdict) {
const rung = host.session.delayRung()
const ms = DELAY_LADDER[rung] * delayScale(host.cruelty)
host.dom.veil.dataset.active = 'true'
host.session.imposedMs += ms
await wait(ms)
host.dom.veil.dataset.active = 'false'
return verdict
}
The verdict is computed before the wait and is not touched during it. The delay is theatre and the code should say so plainly.
21. Sources
Research base for Β§2. Consulted August 2026.
Scale and industry position
- Cloudflare - Humanity wastes about 500 years per day on CAPTCHAs
- Neowin - coverage of the Cloudflare figure
Deployed schemes and their failure modes
- google/recaptcha issue #414 - Recaptcha fading images in and out extremely slowly
- google/recaptcha issue #268 - ReCaptcha extremely slow fading
- arXiv - Gotta CAPTCHA 'Em All: A Survey of Twenty Years of the Human-or-Computer Dilemma
- arXiv - An Object Detection based Solver for Google's Image reCAPTCHA v2
- uCaptcha - FunCaptcha and Arkose Labs: How They Work
- CaptchaAI - What Is FunCaptcha (Arkose Labs) and How It Works
- uCaptcha - Types of CAPTCHAs Explained
- ALTCHA - What is CAPTCHA
- Friendly Captcha - CAPTCHA Alternatives in 2026
Novelty, joke, and satirical CAPTCHAs
- Prima Games - How to Beat I'm Not a Robot: Complete Walkthrough of All Levels
- I'm Not a Robot - complete level index
- Creative Bloq - Hate Captchas? Then you'll love this hilarious game
- BGR - Can You Beat The World's Most Annoying Captcha?
- Wikipedia - The Password Game
- DOOM Captcha
- TechCrunch - People are playing a new Doom-themed CAPTCHA
- Elie Bursztein - Five surprising captcha schemes
- Neatorama - KittenAuth, the Cute Captcha
- Devopedia - CAPTCHA
- Gamification Co - Beat CAPTCHA by Playing Games with PlayThru
- Victoire - Weirdest CAPTCHAs Ever Seen
- Bored Panda - 16 Times People Struggled With These Captchas
Dark patterns
- User Inyerface - a worst-practice UI experiment
- Slashdot - Can You Beat The World's Worst User Interface?
- UBC ETEC 540 - Trapped in the UI: My Frustrating Journey Through User Inyerface
- Yu-kai Chou - Dark Patterns: Brignull's 12 Manipulative UX Tricks
- arXiv - Dark Patterns at Scale: Findings from a Crawl of 11K Shopping Websites
- arXiv - Staying at the Roach Motel: Cross-Country Analysis of Manipulative Subscription and Cancellation Flows
- CourseUX - Dark Patterns: 12 Deceptive UX Designs to Avoid
Accessibility
- W3C - Inaccessibility of CAPTCHA
- Smashing Magazine - The Accessibility Problem With Authentication Methods Like CAPTCHA
- Pope Tech - CAPTCHA challenges aren't accessible
- BOIA - Does Google's reCAPTCHA v2 Meet WCAG Accessibility Requirements?
- Friendly Captcha - reCAPTCHA Accessibility: Is It WCAG Compliant?
Human factors and psychophysics
- Nielsen Norman Group - Response Time Limits
- UX/UI Principles - Doherty Threshold: 400 ms Response Time
- Columbia - Kivetz, Urminsky & Zheng, The Goal-Gradient Hypothesis Resurrected
- Renascence - Goal-Gradient Effect
- Evelance - Fitts's Law and Touch Target Sizing That Holds Up on Mobile
- UX Movement - Finger-Friendly Design: Ideal Mobile Touch Target Sizes
- Tim Graf - Designing for the Thumb Zone
22. Closing note for the implementer
Every mechanic in this document already exists somewhere on the live web, deployed in earnest, in front of something a person needed. The fade delay is real. The re-spawning grid is real. The 47-toggle consent panel is real. The password rules revealed one at a time are real. The unwinnable session is real, and users blame themselves for it.
Kaptcha's only innovation is putting them in one place, turning the intensity dial up, and labelling it honestly.
Build it well. Then read Β§15 again before you deploy it anywhere.
Comments
No comments yet. Start the discussion.