codekredo

← All notes

Spam protection without third parties: proof of work

XCOMBO Codes has a form for submitting missing codes. Every public form attracts spam, and the usual answer is a CAPTCHA from a large provider. We protect ours with a small computing task that the visitor's browser solves on its own – served from our own server, without cookies and without picking out traffic lights.

29 October 2026

The trouble with the usual CAPTCHA

Most CAPTCHA services work by loading a script from the provider's servers. That script sees the visitor's IP address and browser details, and some services also set cookies or analyse how the visitor moves through the page. On a site that otherwise avoids third parties, that's a lot to add for one form.

Image puzzles have a second problem: they're tedious for everyone and a real barrier for people who can't see them well. We wanted a check that most visitors never notice.

Proof of work in a nutshell

The idea is old and simple. Before the form can be sent, the browser has to solve a task that takes a moment of computing time but is quick to check. For one honest submission, that's a short pause. For someone who wants to send thousands of messages, it adds up.

We use ALTCHA, an open-source project under the MIT licence, for the widget in the page and its protocol. The verification on the server is our own small PHP implementation that follows the library's reference code. The task is based on PBKDF2 with SHA-256: the server picks a secret number, and the browser has to find it by trying one candidate after the other. The browser does this in a Web Worker, so the page stays responsive while it works.

One detail came from our own security settings: our Content Security Policy doesn't allow scripts created on the fly from blob addresses. So we use the build of the widget that loads its worker as a normal file from our own server.

How a solution is checked

When the form arrives, the server checks the solution before it looks at anything else:

if task.expires_at < now:            reject('expired')
if not hmac_equal(sign(task), sig):  reject('signature')
if already_used(task.nonce):         reject('used')
if not solves(task, answer):         reject('wrong')

Small extra hurdles

Proof of work makes spam expensive, but it isn't the only line of defence. A few cheap checks catch most of what's left:

Nothing goes live automatically

The most effective protection is that a submission never appears on the site by itself. Every one is checked by us first, and if a code is added, we write the description ourselves. Submissions are deleted once we've decided, and after 90 days at the latest. The form doesn't ask for a name or an email address, so there's very little personal data to protect in the first place.

Trade-offs

For a small site with one form, this combination has been enough. You can see it in action on the “Submit a code” page of XCOMBO Codes.

More notes