Proof of work.
A JWT-shaped token that proves its issuer burned CPU (or GPU) time on a specific document. Two base64url segments: the document and a suffix the solver searched for. Anyone can verify it with two SHA-256 computations; finding it costs 2level of them on average.
Format
A token is two segments joined by a dot, like a JWT with one segment missing:
b64u(doc) "." b64u(suffix)
-
b64uis unpadded base64url (RFC 4648 §5), exactly as in JWTs. Verifiers reject segments that are not canonical unpadded base64url. docis an arbitrary byte string chosen by the application.-
suffixis 0 to 32 bytes chosen freely by the solver, typically an 8-byte counter.
For example, with the document hello world:
aGVsbG8gd29ybGQ.XBCKI7lQ3i4
Level
The level of a token is defined from a double SHA-256:
check = SHA-256(SHA-256(doc) || suffix)
Read check as a 256-bit big-endian integer — the first byte of the digest is
the most significant. The token's level is the number of trailing zero bits: the largest
L such that the integer is divisible by 2L. A token
proves level L when its level is at least L.
The example token above hashes to
0e5598dc7f613025fee4cf09cb8a15160894a7471d112a3b73a9b232f0300000
which ends in five zero hex digits and 3 (binary 0011): exactly
20 trailing zero bits, so it proves any level up to 20.
Each attempt is an independent coin flip: finding level L takes 2L hashes on average, with no shortcut and no memory to trade. Hashing the document once and iterating only the suffix means solving cost does not depend on document size: with an 8-byte suffix the search hashes a fixed 40-byte message, a single SHA-256 block. Verification is two hashes regardless of level.
Rough solving times, using the measured rates from the benchmark below:
- 16
- 65,536 hashes — instant anywhere.
- 20
- 1 million — 0.2 s in single-threaded JavaScript.
- 24
- 16.8 million — 3 s in single-threaded JavaScript, instant on a GPU.
- 28
- 268 million — under a minute of JavaScript, 0.13 s on an M5 Pro GPU.
- 32
- 4.3 billion — a coffee break in JavaScript, 2 s on that GPU.
- 36
- 68.7 billion — half a minute on that GPU.
- 40
- 1.1 trillion — 9 minutes on that GPU; pick this only to price out GPUs too.
Documents
The scheme does not interpret the document; the application must make it worthless to replay. Include whatever binds the proof to its use — an audience, an action, a timestamp, a server-issued nonce — and reject tokens whose document you did not expect or have seen before. A proof of work rate-limits; it does not authenticate.
Choose levels from the attacker's hardware, not the visitor's: SHA-256 runs 400–1,500× faster on one GPU than on one browser thread (see below), and Bitcoin ASICs are another five orders of magnitude beyond that.
Solve
Runs entirely in this page, on Web Workers hashing in pure JavaScript. Nothing is sent anywhere.
Verify
Solvers
Reference solvers live in pow/:
pow.mjs, a Node CLI whose miner is the same pure-JavaScript SHA-256 this page
uses; pow.swift, a Metal compute kernel for Apple GPUs; and
pow.c, the same kernel in OpenCL for NVIDIA and AMD GPUs (and anything else
with an OpenCL driver). All solve, verify, and benchmark:
node pow.mjs solve 32 'hello world'
swiftc -O pow.swift -o pow && ./pow solve 40 'hello world'
cc -O2 pow.c -lOpenCL -o pow-cl && ./pow-cl solve 40 'hello world'
Measured on an Apple M5 Pro (18 CPU cores, 20-core GPU), 10-second runs:
- 5.4 MH/s
- V8 (Node 24), one thread.
- 72 MH/s
- V8, 18 worker threads.
- 2,033 MH/s
- Metal, one kernel hashing one block per GPU thread — 377× one V8 thread, 28× all of them.
- 2,031 MH/s
- OpenCL, the same kernel on the same GPU through Apple's legacy OpenCL.
And on an AMD Ryzen 9 7950X3D (16 cores, 32 threads) with an NVIDIA GeForce RTX 4090, same 10-second runs:
- 3.5 MH/s
- V8 (Node 24), one thread.
- 51 MH/s
- V8, 32 worker threads.
- 5,146 MH/s
- OpenCL on the RTX 4090 — 1,470× one V8 thread, 100× all of them.