Preview page — visible to crawlers and to humans only with ?preview=true. The bare URL redirects human visitors back to the homepage. The feature is in the code; not announced to the general audience yet.
Preview

Proof-of-Work Rate Limiting

Make every request to a protected endpoint pay a small, browser-side computation. Honest visitors pay milliseconds they never notice. Abusers pay the same cost per request, on every IP, with no way to amortize.

A complement to identity-based rate limiting (IP buckets, API-key quotas), not a replacement. The two work best layered.

Is “rate limiting” the right name?

Yes, but specifically of the cost-based variety. Classical rate limiting counts requests per identifier (IP, user, key) and rejects excess. Proof-of-work rate limiting instead imposes a per-request CPU cost on the client, paid in a tight SHA-256 loop. The mechanisms are complementary:

 Counting rate limitProof-of-work rate limit
Currency Identifier scarcity (one IP, one key) CPU-seconds on the client device
Bypass via IP rotation Trivially defeated Not defeated — each new IP still pays the full cost
Cost to honest user Zero (until limit hit) ~50–500 ms of CPU per session, or per request
Cost to attacker at 1k req/s ~0 (rotate IPs and continue) Tens of CPU cores running constantly
State on the server Per-identifier counters Stateless (HMAC-signed tokens)

Use both: counting limits keep honest traffic bounded; PoW limits keep distributed abuse from being free.

How it works

1. Server issues a challenge

32 random bytes + a difficulty target + a short TTL, all sealed with an HMAC so the server can verify it later without storing anything.

2. Client solves it

Tight SHA-256 loop searching for a nonce whose hash has N leading zero bits. Runs in the browser via a WebAssembly module.

3. Server verifies + mints a passport

One HMAC check, one SHA-256, one bit-count. The server hands back a signed cookie valid for X minutes (or for one request only, see TTL = 0 below).

4. Gated routes accept the passport

Middleware checks the cookie’s HMAC and freshness in microseconds and lets the request through. No DB, no cache, no per-request server work.

Two operating modes

Passport mode (TTL > 0)

Solve once, browse freely for X minutes via an HMAC-signed cookie. Best for public web pages where humans expect zero friction between clicks.

Per-request mode (TTL = 0)

Every single request must carry a fresh proof, supplied via an Authorization: PoW … header. No amortization possible. Best for high-value API endpoints (admin paths, money-moving proxies) where you want unit cost on every call.

About the WebAssembly solver

The solver is shipped as a compact WebAssembly module. The reason is not bot resistance — bots can run WebAssembly perfectly well, and any attacker can re-implement the algorithm in native code in an afternoon. What WebAssembly buys us is something more modest and more honest: it keeps the honest client path within a small factor (roughly 2×) of a hand-written native solver. With a pure-JavaScript implementation, the same gap would be 10–20×, which is precisely what an attacker would exploit by reimplementing the puzzle outside the browser.

Two secondary benefits matter to us too:

What you can configure

Difficulty (bits)

Default 20 bits (~300 ms on a modern laptop, ~2 s on a budget phone). Push to 24 bits for high-abuse routes; below 18 is too cheap to matter.

Passport TTL

Default 30 min. Set to 0 to require a fresh proof on every request. Set per route, not globally.

Per-route policy

Each route picks its own (difficulty, TTL, exempt-list). The /health probe never sees a challenge; a chat proxy might.

Subnet binding

Passports are bound to a /24 (IPv4) or /48 (IPv6) so a stolen cookie can’t be replayed from an arbitrary network.

Stateless HMAC tokens

No database, no cache. One server-side secret signs both challenges and passports.

Optional CAPTCHA escape

For visitors on slow devices, a fallback link to a classical CAPTCHA (e.g. hCaptcha) mints the same passport.

Optional: pair it with vyth’s bot verdict

The rate limiter is a standalone primitive and works without vyth being deployed. If both are present, the limiter can read vyth’s human / suspect / bot verdict and scale the challenge difficulty accordingly — humans bypass entirely, suspects get the default, declared bots get a heavier puzzle. The integration is one line of configuration; removing it removes the dependency without any other code change.

Dogfooded on our own /chat endpoint

The first production user of this rate limiter is our own voyk chat proxy. The /chat route translates between OpenAI / Anthropic / xAI / Ollama wire formats and is exactly the kind of high-cost endpoint that distributed abuse loves: long-lived SSE streams, paid upstream tokens, an attractive proxy target. We wire the PoW gate in front of /chat first so we can measure the effects on a real workload — honest-user latency, false-positive rate, attacker cost, CPU overhead on the verifier — before recommending the same pattern for anyone else’s API.

That same dogfood also doubles as the test bed: an end-to-end smoke suite drives a real browser solver against the live /chat endpoint, covering passport mode, per-request mode, replay-resistance, and the forged-token failure path. If the suite stays green, the primitive is ready for other routes.

# 1. Ask for a challenge. (Returns a single base64url envelope; the
#    seal, nonce, difficulty, and expiry are packed inside it.)
curl 'https://voyk.loxal.net/pow/challenge?difficulty=20&ttl=30'
# → {"challenge":"AQEC...<68 base64url chars>","difficulty_bits":20,"expires_in_secs":30}

# 2. Solve it in the browser (WebAssembly) or with the CLI — find an `answer:u64`
#    such that SHA-256(nonce || answer_le_bytes) has ≥ difficulty_bits leading zeros.

# 3. Submit the solution; receive a passport cookie bound to your /24.
curl -c cookies.txt -X POST \
  -H 'Content-Type: application/json' \
  -d '{"challenge":"AQEC...","answer":"123456"}' \
  'https://voyk.loxal.net/pow/passport'
# → Set-Cookie: pow_passport=...; Max-Age=1800; Secure; HttpOnly; SameSite=Lax
# → {"passport":"...","ttl_secs":1800}

# 4. Call /chat with the passport — subsequent calls amortize.
curl -b cookies.txt -X POST \
  -H 'Content-Type: application/json' \
  -d '{"model":"...","messages":[...]}' \
  'https://voyk.loxal.net/chat'

# Alternative: per-request mode (TTL=0, no cookie) — one solve per call.
curl -X POST \
  -H 'Authorization: PoW AQEC...:123456' \
  -H 'Content-Type: application/json' \
  -d '{"model":"...","messages":[...]}' \
  'https://voyk.loxal.net/chat'

Integrate into your own service

The same code path runs in voyk and in your service. Two crates: voyk-pow is a pure-Rust library — no HTTP, no async runtime, no I/O — that you drop into any HTTP stack (Axum, Actix, Rocket, Warp, hyper directly, Lambda, Cloudflare Workers, Fastly Compute@Edge). voyk-pow-wasm is the companion browser solver, a thin wasm-bindgen wrapper over voyk-pow compiled to wasm32-unknown-unknown. Because the algorithm is one crate, the server verifier and the browser solver cannot disagree about the wire format.

Server side — Rust

# Cargo.toml
voyk-pow = { git = "https://github.com/loxal/lox", package = "voyk-pow" }
// HTTP handler — your framework, your route.
use voyk_pow::{ChallengeMint, Verifier, PassportMint, Subject};

// One-time at process start. Secret is 32 bytes; load from your secrets manager.
let secret: [u8; 32] = load_secret();
let mint = ChallengeMint::new(secret);
let verifier = Verifier::new(secret);
let passports = PassportMint::new(secret);

// GET /pow/challenge
let sealed = mint.mint(/* difficulty_bits */ 20, /* ttl_secs */ 30)?;
let wire = sealed.encode_base64url();    // send to client

// POST /pow/passport — verify, then mint a passport.
let received = voyk_pow::SealedChallenge::decode_base64url(&wire)?;
let solution = voyk_pow::Solution { challenge: received, answer };
verifier.verify(&solution)?;
let passport = passports.mint(Subject::ipv4_from(client_ip), 30 * 60, /* tier */ 0)?;
// set passport.encode_base64url() as a Secure;HttpOnly;SameSite=Lax cookie.

Voyk’s own /pow/challenge + /pow/passport handlers and the gate middleware live in voyk/src/pow.rs and are themselves < 400 LOC. If you’re on Axum 0.8 you can copy them verbatim.

Browser side — WebAssembly solver

# Build the .wasm + JS glue once.
cargo install wasm-pack
wasm-pack build voyk-pow-wasm --release --target web --out-dir pkg
# pkg/voyk_pow_wasm_bg.wasm   ~20 KB after wasm-opt -Oz
# pkg/voyk_pow_wasm.js        ES module loader
# pkg/voyk_pow_wasm.d.ts      TypeScript typings
// Plain JS on the page — single-shot solver (20-bit puzzle, ~300 ms desktop).
import init, { solve_once_b64 } from '/pkg/voyk_pow_wasm.js';
await init();

const ch = await (await fetch('/pow/challenge')).json();
const answer = solve_once_b64(ch.challenge);

await fetch('/pow/passport', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  credentials: 'include',
  body: JSON.stringify({ challenge: ch.challenge, answer: answer.toString() }),
});
// After this, cookies carry the passport; just call protected routes normally.

For higher difficulties (24+ bits, multi-second puzzles), run the solver on a Web Worker using the resumable solve_step_b64(challenge, start, max_iters) API so the tab stays responsive while the puzzle works. See voyk-pow-wasm/README.md for the Worker example.

Hosted — no self-host, just call our verifier

If you don’t want to run a verifier, point your front-end at voyk.loxal.net/pow/* and your back-end at the same host’s validation API. The wire format and the cookie name (pow_passport) are identical to the self-hosted crate; the difference is that we hold the HMAC secret. Available on the Pro and Enterprise tiers (see Pricing below). Auth on the hosted API uses the same X-API-Key header as the rest of the voyk API.

Tier semantics — what your key buys you. All hosted calls work without a key (anonymous “public” tier). The key unlocks the knobs and lifts the per-IP demo throttle:

Tier difficulty Challenge ttl Passport ttl Throttle
Public (anon / Free key) server default (20 bits) server default (60 s) server default (30 min) per-IP, anon only
Pro 1–32 1–600 s 1 s–1 h quota only (1 M / mo)
Enterprise 1–32 1–3600 s 1 s–24 h quota only (custom)

The challenge response echoes the resolved tier as tier: "public" | "pro" | "enterprise" so your client can verify it was credited to the expected plan. Public callers who pass ?difficulty=1&ttl=86400 still receive the server defaults — the knobs only take effect for authenticated callers. Apply for a key at info@loxal.net.

curl -H "X-API-Key: $VOYK_KEY" \
     "https://voyk.loxal.net/pow/challenge?difficulty=12&ttl=120"
# => {"challenge":"…","difficulty_bits":12,"expires_in_secs":120,"tier":"pro"}

Honest tradeoffs

Mobile users pay more

A 24-bit puzzle that costs 5 s on a desktop can cost 60 s on a low-end Android. Cap difficulty for mobile UAs or always offer the CAPTCHA escape.

Battery cost is real

Several million SHA-256 operations wake the SoC for a measurable fraction of a second. Don’t be the site that costs 1 % of the battery per visit.

Passport sharing

A botnet that solves once can replay the passport across its members until expiry. Mitigated by short TTLs and subnet binding, not eliminated.

Not a malice detector

PoW raises unit cost; it doesn’t identify bad intent. A determined attacker with a budget will pay the cost. Pair with identity-based limits and behavioral analysis.

First-visit UX cost

New visitors classified as suspect see a brief “verifying” page before content loads. Measurable bounce-rate impact on public-facing pages. Use sparingly on landing pages.

Auditable, not opaque

The solver is open source; the challenge protocol is documented; there are no behavioral “magic” signals. That is by design, and also means a determined attacker has the full spec to optimize against.

Pricing

The rate-limiter library and the WebAssembly solver are source-available and free to self-host as a Rust crate. The hosted tiers below add a managed verifier on voyk.loxal.net, a per-route policy console, and email-based incident support.

Self-host
€0
Source-available crate
drop into your own service
no SaaS dependency
community support only
Pro · preview
€49 / mo
hosted verifier on voyk.loxal.net
up to 5 protected routes
1 million verifications / mo
per-route difficulty & TTL knobs
email support 24h
Enterprise
talk to us
dedicated verifier & secret rotation
custom volume
SLA + status page
priority email + incident pager

Preview pricing — introductory, may change as the feature graduates from preview to general availability. Total prices — no VAT is charged or shown (§ 19 UStG Kleinunternehmerregelung).

Interested?

Drop us a line if you want to be in the early-access cohort. This is a preview feature; deployment details and pricing are not finalized yet.

  Get in touch

Opens your mail client to info@loxal.net. This page is a preview — the implementation is not generally available yet.