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:
- The task carries a signature made with a secret key. A task the server didn't issue, or one that was altered, is rejected.
- Every task expires after a short time.
- Every task can be used exactly once. Its random identifier is stored until the task expires, so a solved task can't be replayed.
- The solution must actually match the task.
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:
- A hidden field that people never see. Simple bots fill in every field; if this one has content, the server answers politely and throws the message away.
- A minimum time between loading the task and sending the form. People need a few seconds to type.
- A check that the request really comes from our own page.
- A daily limit per sender and an overall daily limit, so a flood of messages can't fill our inbox. To recognise a sender, the server keeps a keyed hash of the IP address for 24 hours – not the address itself.
- Simple content rules: no links or email addresses in the description, and codes that are already in the collection get a pointer to their existing page instead.
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
- Proof of work raises the cost of spam; it doesn't make it impossible. Someone with enough computing power can still get through – that's what the limits and moderation are for.
- Older and slower devices need a little longer to solve the task.
- The form needs JavaScript. The rest of XCOMBO Codes works without it.
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
- Counting visitors without cookies, from server logsHow we count visitors from the web server's own logs – no cookies, no tracking script, no third party – how bots are filtered out and where the limits are.
- A multilingual static site in plain PythonHow XCOMBO Codes turns a few data files into close to 1,000 pages in five languages with one Python script and no framework – and the details that mattered.