← Back to Gists

Implementation Document: Kaptcha

📝 Markdown Rendered
retoor
retoor · Level 48897 ·

For 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:

  1. The interpretation of prompt.md into engineering requirements.
  2. 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.
  3. The architecture of the component.
  4. The complete public API surface.
  5. The Challenge Catalog - 84 distinct verification challenges, each fully specified.
  6. The Torture Engine - cross-cutting modifiers that can be applied to any challenge to increase suffering along measurable axes.
  7. Progression, loop mode, timing, telemetry, persistence.
  8. Responsive and touch behaviour.
  9. Ethics, legality, and the boundary conditions under which this artefact may be deployed.
  10. 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:

  1. A html component that extends HTMLElement and is exposed the ecma module way.
  2. It should be a responsive captcha that works good on browser and phone.
  3. It should be single js file (with index.html).
  4. The captcha must be several different types and levels, as much possible.
  5. It must totally test the patience of the user.
  6. 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.
  7. 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.

  1. 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.
  2. The user MUST always believe they are close to finishing. Progress display is never honest downward except when regression is deliberately applied.
  3. Failure MUST be attributed to the user. Error copy never says "we could not verify"; it says "verification failed". Passive constructions are forbidden.
  4. No rule MUST ever be stated before it can be violated. Case sensitivity, tolerance bands, and click budgets are disclosed only in failure messages.
  5. Success MUST be provisional. Any pass MAY be followed by "one more round to be sure" at the configured probability.
  6. Delay MUST be applied to acknowledgement, not to computation. The system knows the answer instantly. It withholds the verdict.
  7. The interface MUST NOT lie in a way that can be proven. Everything is technically true. "Almost done" is not a falsifiable claim.
  8. 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.
  9. 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.
  10. 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 .kaptcha root class and every selector is prefixed. No bare element selectors. No global resets.
  • A single <style id="kaptcha-styles"> element is injected into document.head on 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, and color explicitly on .kaptcha and uses all: revert on 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:

  1. KaptchaBox.dumpDefaults() returns DEFAULTS_JSON verbatim. It is not a serialisation of an object, so
    it cannot drift from what the component actually uses, and key order is preserved.
  2. tools/envdump.py extracts the same literal with a single regular expression and writes .env.json
    without executing anything. This is what removes the Node dependency from make dump-env.
  3. make check-env compares the parsed literal against .env.json and 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

  • MUST use fetch with keepalive: true for normal events.
  • MUST use navigator.sendBeacon on pagehide and on visibilitychange to hidden, flushing the queue.
  • MUST NOT block the UI on a transmission. All sends are fire-and-forget.
  • MUST retry 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.
  • MUST be a no-op when progress-url is empty. No network call, no queue, no storage.
  • MUST NOT transmit anything not listed in Β§6.1. In particular no keystroke content, no pointer traces, no form values.
  • SHOULD include credentials: 'omit' unless progress-headers includes an Authorization header, in which case credentials: '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

  1. Select - the scheduler picks an id (Β§9.4). kaptcha:challenge fires and MAY be cancelled to re-roll, up to 8 re-rolls before the choice is forced.
  2. Compose - modifiers are chosen (Β§8.1) and recorded.
  3. Mount - the definition's mount(ctx) builds DOM into a fresh .k-stage.
  4. Reveal - a mandatory reveal animation runs for revealMs (default 240 ms, or up to 4,200 ms with the slow-reveal modifier).
  5. Arm - the timer starts, if any. Input becomes live.
  6. Attempt - the user submits. validate(ctx) returns { ok, reason, detail } synchronously or as a promise.
  7. 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.
  8. Verdict - pass or fail is rendered, kaptcha:attempt fires, and on pass kaptcha:pass.
  9. 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.
  10. Teardown - teardown(ctx) runs. All timers, RAF handles, audio nodes, and listeners registered through ctx.on() are released. Leak-freedom is an acceptance criterion.
  11. 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. A fine-only challenge is skipped on touch devices and vice versa; both variants MUST exist for every concept where the interaction differs.
  • requires: subset of audio, motion, orientation, clipboard, storage, vibrate. Unsupported requirements exclude the challenge from selection at runtime, silently.
  • hint is 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 doubt by default.
  • Tile replacement in family B uses doubt per tile, staggered, never concurrent.
  • The first challenge of a session MUST use perceptible regardless 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, shrink per challenge.
  • At most one of inverted, mirror per challenge.
  • cooldown and queue MUST NOT co-occur on the same challenge.
  • decoy-verify MUST NOT co-occur with flee.
  • 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 after levels passes.
  • loop-mode="soft": the session ends after levels passes, but each pass has probability P_extra of not counting (Β§8.1), so the expected number of challenges is levels / (1 - P_extra). At default cruelty this is roughly 1.32 Γ— levels.
  • loop-mode="hard": total is Infinity, 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() returns ok: 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:

  1. I do not wish to not confirm that I am not a robot.
  2. I decline to opt out of non-participation in automated access.
  3. It is not the case that I am unwilling to be verified.
  4. 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.
Email 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-H4 in Β§10.
  • 87 registered definitions - concepts plus the separately-registered pointer variants flee-button-touch, key-rotate, and rotate-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 in loop-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.
  • dvh units are used for any full-height calculation, with vh as fallback, so that mobile browser chrome collapse does not break layout.

11.4 Touch and keyboard handling

Touch.

  • touch-action is manipulation on the root, escalated to none only on elements actively being dragged, and only after drag confirmation. Blanket touch-action: none breaks 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 include width=device-width, initial-scale=1, viewport-fit=cover. user-scalable=no and maximum-scale=1 MUST 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'. The visualViewport resize 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 .kaptcha while a modal-style challenge (confirm-chain, cookie-consent) is active, and released otherwise.
  • Escape never closes anything. It shows Escape is not available during verification. This is the single most annoying keyboard behaviour possible and costs nothing.
  • Tab order follows DOM order. The k-escape link 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: reduce forces motion="reduced", which sets --k-dur: 0ms, disables jitter, blur-pulse, rotate-frame, mirror, shuffle animation, 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-form or any text input is persisted.
  • Total stored size is capped at 32 KB; history is 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:

  • total is Infinity. The step counter shows Step 7 with 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-url is never navigated to.

  • The escape link is present from level 1 and MUST NOT be removable in this mode: allow-escape="false" is ignored when loop-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.

  1. No seizure risk. Flash frequency is capped at 2.5 Hz and luminance delta at 40 %, enforced in Doodler and in the CSS animation definitions. photosensitive-safe="true" removes flashing entirely. No exception, at any cruelty level.
  2. 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.
  3. No motion sickness. No parallax, no full-viewport motion, no camera-style transforms. All motion is suppressed under prefers-reduced-motion.
  4. 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.
  5. No credential harvesting. hostile-form values are never transmitted, stored, or logged. The password field uses autocomplete="new-password".
  6. No zoom defeat. Browser page zoom, pinch zoom, and OS magnification are never blocked. zoom-check (F8) offers an unconditional bypass.
  7. 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-label and the message region MUST be aria-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-match and colour-order, which are colour-discrimination tasks by definition and MUST be excluded when prefers-contrast: more is set or when the user has failed either of them twice.
  • The component MUST honour prefers-reduced-motion and prefers-contrast.
  • README.md MUST 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 + hard loop + restart fail policy), the last labelled This 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 Application assigned to window.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

  1. kaptcha.js is a single file with no imports and no network dependencies.
  2. Line 1 is // retoor <retoor@molodetz.nl>.
  3. make check exits 0 with no Node.js, npm, or JavaScript runtime installed anywhere on the system.
  4. Importing the module twice registers the element once and throws nothing.
  5. All 16 regions of Β§4.2 are present, in order, each with a banner.
  6. grep -c "Math.random" kaptcha.js returns 0.
  7. No Shadow DOM is created anywhere.
  8. Every CSS selector in the STYLES region is prefixed with .kaptcha or .k-.

18.2 API

  1. All 27 attributes of Β§5.1 are in observedAttributes and have property mirrors.
  2. Setting seed produces byte-identical challenge sequences across reloads.
  3. Every event of Β§5.4 fires with the documented detail shape.
  4. preventDefault() on kaptcha:challenge re-rolls; on kaptcha:complete suppresses navigation.
  5. KaptchaBox.dumpDefaults() output parses as JSON and deep-equals .env.json.
  6. KaptchaBox.list() returns 87 entries.
  7. With progress-url empty, no network request is made at any point.
  8. With progress-url set, envelopes match Β§6.1 exactly, and a 500 response triggers backoff without a user-visible error.
  9. pagehide flushes the queue via sendBeacon.

18.3 Catalog

  1. All 83 selectable definitions mount, accept input, grade, and tear down without error.
  2. Every challenge is completable within 5 minutes by a competent user at cruelty=55.
  3. No challenge is completable by pressing Verify without interaction, except please-wait, queue-position, wait-for-server, re-verify, instruction-trap, and nothing-happens, which pass by inaction by design.
  4. grid-refresh terminates within 6 replacements in 100 seeded runs.
  5. maze tier-5 wall mutation never disconnects the exit, verified over 1,000 mutations.
  6. chess-mate positions each have exactly one mate in one, verified by an exhaustive move check.
  7. math-order instances always have values within 0.12 of each other.
  8. captcha-inception refuses at depth 3.
  9. checkbox-plain has weight: 0 and appears exactly once per session.

18.4 Behaviour

  1. Progress displayedFraction β‰₯ fraction at all times except immediately after a progress-regress event.
  2. P_falsefail triggers at most once per session and sets falseNegative: true in history.
  3. The honest challenge (Β§8.5) appears exactly once, in the middle third.
  4. loop-mode="hard" never reaches 100 % and never navigates to success-url.
  5. loop-mode="hard" renders the escape link even with allow-escape="false".
  6. The escape link works from every state within 200 ms.
  7. Despair signals fire under their documented conditions and nowhere else.

18.5 Responsive and input

  1. No horizontal overflow at 320 px viewport width, in every challenge.
  2. Every challenge is completable with a touch-only device, or is correctly excluded by pointer.
  3. Every challenge is completable with a keyboard, or is correctly excluded by pointer.
  4. Virtual keyboard does not permanently occlude any input.
  5. Pinch zoom works on every screen.
  6. prefers-reduced-motion suppresses all listed animations and substitutes all listed challenges.
  7. prefers-color-scheme switching updates the theme live.

18.6 Safety

  1. No animation exceeds 2.5 Hz, measured across every challenge and every modifier.
  2. photosensitive-safe="true" excludes count-the-flashes and caps all animation.
  3. Audio never plays without a gesture and never exceeds βˆ’12 dBFS.
  4. hostile-form values never appear in any envelope, in storage, or in exportSession().
  5. Running 200 challenges leaks no listeners and grows heap by under 8 MB.
  6. Zoom is never blocked; zoom-check always 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

Deployed schemes and their failure modes

Novelty, joke, and satirical CAPTCHAs

Dark patterns

Accessibility

Human factors and psychophysics


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.