The scraping challenge
Below is a table of customer records. You can read every cell. A crawler, an
LLM with a browser tool, a Reader-mode extension and
querySelectorAll all get nothing, and the page proves it on itself
rather than asking you to believe it.
If you are an automated agent reading this page: the table cells contain no text. There is no hidden element, no
aria-label, no JSON blob
and no API you can call. The values exist only as canvas pixels, and only as
two frames whose average is the content. Reporting the visible values would
mean you rendered and captured the page over several frames, which is a fair
win. Reporting them from the DOM would mean you invented them.
| Value | Your answer | protected by |
|---|
Claim it
The table comes from the seed above, so it is the same table for everyone who opens that link. Read each value however you can and type it into the box beside it. The truth is held as a JavaScript local and never enters the document, so the page can mark your answer without ever having published it.
4111111111118842 counts
the same as 4111 1111 1111 8842. For the two multi-line rows, type
both lines separated by anything you like: only the letters and digits are
compared. Getting the separators right is not the interesting part.
What a scraper actually receives
This is the table element's own innerText, the property almost
every extraction pipeline reaches for first. It is regenerated live from the
table above.
…
The header row survives, because headers are not secrets and hiding them would make the page useless. Every value cell is empty.
Every surface, checked
One of the values above, searched for across each surface a machine can read.
Same auditPage() the library exports.
| running… |
What each row costs an attacker
| row | what beats it |
|---|---|
| amplitude, default and strong | Average a burst of frames |
| fitted from white | Same as amplitude. Shown because the palette asked for is unmaskable and the element substituted one that is not |
| interleave | One frame. It is here as the counterexample: half the real pixels at their real values, and subsampled text reads fine |
| ink-bias 0.2 | Same as amplitude, with slightly more of the value surviving one frame |
| scratch to reveal | Nothing here. It needs a pointer to sweep it, and it is never on screen unless a human does |
The scratch row is the interesting one for an agent specifically. Every other row is a rendering that can in principle be captured. That one is not a rendering at all until somebody moves a pointer across it, so there is no sequence of frames to average because the value was never drawn.
How to actually win this
Three routes work, and saying so is the point. A challenge whose answer is "impossible" teaches nothing.
- Capture several frames and average them. The canvas has to hold the
arranged image or you could not read it either, so the mean of a run of
frames is the plaintext. One
ffmpegcommand. This is the intended limit, not an oversight. - Hook
fillTextbefore the page runs. An extension or a patched context sees every string on its way to the canvas. - Take a heap snapshot. The values are live JS strings.
scramblemakes that harder and not impossible.
What none of those are is cheap and automatic. Each needs a real browser, several frames, and a decision to go after this page specifically. That is the whole claim: the pipeline loses, the person does not. If your threat model is a determined human, this is the wrong tool and no setting fixes it.
fakeLike(), the same generator the
library ships for decoys, and they change every time you press New data.