Bollwark

Proof-of-work CAPTCHA

Stop bots without asking anyone to find the traffic lights.

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.

  • No cookies, no local storage
  • One <script> tag
  • Runs on your own box if you want
Embed
<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>
Verify, server side
POST /v1/verify
Authorization: Bearer <site secret>

{ "token": "…" }
→ { "success": true }

Four rungs, picked per request

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.

  1. Invisible pass

    Clean signals. A trivial puzzle solves in a few milliseconds and the visitor never notices.

  2. Checkbox

    Something is mildly off. The widget shows itself and solves visibly.

  3. Hard PoW

    A memory-hard Argon2id puzzle at raised difficulty. Cheap once, expensive a million times.

  4. Block

    The score is past the block threshold. No puzzle is issued at all — the request gets a 429.

Two scoring passes bracket every solve

1

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.

2

In the browser

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.

3

At verify time

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.

The signals

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.

Rate
Per-IP and per-site counters over a 60-second window. IPv6 buckets to /64, so rotating through a delegated block shares one counter.
Header anomaly
Missing or implausible User-Agent, Accept-Language, Accept-Encoding.
IP reputation
Optional CIDR list: Tor exits, datacenter ranges, VPNs, residential.
TLS fingerprint
Read from a header your reverse proxy sets (e.g. Cloudflare's cf-ja4), only trusted when the peer is in the trusted-proxy list.
Honeypot
A field no human fills in. Scores high enough to block on its own.
Time on page
Derived from the challenge's own creation time. Sub-500ms submissions are not people.
Behaviour
Pointer, touch and interaction counts, plus webdriver and headless probes. Every probe is a boolean before it leaves the browser, so the blob carries no fingerprinting entropy.

Cookie-free is a design constraint, not a setting

No client-side storage

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.

IPs truncated before they land

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.

Logs expire on their own

A sweeper prunes the decision log on a retention window, 72 hours by default. Storage limitation without a cron job you have to remember.

Outages fail open, carefully

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.

Run it yourself, or let us run 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.