At puzzle time
GET /v1/puzzle scores request rate, header anomalies, IP reputation and the TLS fingerprint your proxy passes through. That score picks the rung.
Proof-of-work CAPTCHA
Bollwark scores every request twice and makes the suspicious ones pay for it in CPU. Real visitors never see a challenge. Nobody gets a cookie, so nobody needs a consent banner.
<script> tag<script src="https://api.bollwark.eu/v1/widget.js"></script>
<form method="post" action="/signup">
<input name="email" type="email" required>
<div class="bollwark" data-site-key="…"></div>
<button>Create account</button>
</form>POST /v1/verify
Authorization: Bearer <site secret>
{ "token": "…" }
→ { "success": true }Every visitor is scored before a puzzle is issued. The score picks a rung; the rung picks the difficulty. Most traffic never leaves the first one.
Clean signals. A trivial puzzle solves in a few milliseconds and the visitor never notices.
Something is mildly off. The widget shows itself and solves visibly.
A memory-hard Argon2id puzzle at raised difficulty. Cheap once, expensive a million times.
The score is past the block threshold. No puzzle is issued at all — the request gets a 429.
GET /v1/puzzle scores request rate, header anomalies, IP reputation and the TLS fingerprint your proxy passes through. That score picks the rung.
A web worker solves the Argon2id puzzle off the main thread, so the page never janks. Memory-hard by default — GPUs and rented botnets get much less of a discount than they would on SHA-256.
POST /v1/verify re-scores on what only exists at submit: time on page (measured server side, not client-reported), the honeypot field, and a behaviour blob reduced to booleans in the browser.
Verify answers pass, block, or shadow-fail — success to the visitor, a flagged session in your log. Useful when you would rather log a false positive than lose a real signup to one.
Each one self-gates on its own input. Leave the optional files unset and those signals score zero — there is no global switch to forget to flip.
No Set-Cookie is ever issued and no request cookie is read. Nothing to disclose in a consent banner, nothing to configure for SameSite, nothing to break in a cross-origin embed.
Scoring uses the full address in memory; anything written down — the decision log and the log lines both — gets a /24 or /48. Your aggregator never sees a per-visitor address.
A sweeper prunes the decision log on a retention window, 72 hours by default. Storage limitation without a cron job you have to remember.
If the service is unreachable the widget can mint a failover claim so your signup form still works. Honouring one needs an attested outage window, and the honeypot and behaviour checks still run inside it.
The service is MIT and self-contained — one binary, one config file, no database required. The hosted plan is the same binary with the dashboard, retention and backups attached.