Anubis (TecharoHQ/anubis) is an open-source reverse-proxy anti-bot gate an increasing number of sites put in front of themselves (Startpage among them). When it decides to challenge a client, it serves a page embedding a PoW puzzle that the browser's JS solves in a Web Worker, then replays the answer as a query string against a fixed API path. Replicating that server-side unlocks the real page instead of the "Just a moment..." wait screen — no headless browser needed.
Everything below is derived directly from Anubis's own public source (lib/challenge/proofofwork/proofofwork.go for the server-side validator, web/js/worker/sha256.ts and wasm/pow/sha256/src/lib.rs for the two reference client implementations, which agree exactly) — not reverse-engineered from obfuscated code.
HTTP 200
Content-Type: text/html
<!doctype html>
<html lang="en">
<head>
<script id="anubis_version" type="application/json">"v1.26.4"</script>
<script id="anubis_challenge" type="application/json">{"rules":{"algorithm":"fast","difficulty":6},"challenge":{"issuedAt":"2026-10-09T10:58:18.167975774Z","metadata":{"User-Agent":"...","X-Real-Ip":"..."},"id":"01a12050-55b7-7ed8-ac01-8ca29fcefe62","method":"fast","randomData":"da5eb566735ccae8db7d506e96d18dbfb6790c4bcde8600cf0cd21e9e5c71d7ccd74745509dbfb71c97a74d33f5e4bab60ee58dd704ce318322516afbe9d7272","policyRuleHash":"ac980f49c4d35fab","difficulty":6,"spent":false}}</script>
<script id="anubis_base_prefix" type="application/json">""</script>
<script id="anubis_public_url" type="application/json">""</script>
...
</head>
...
</html>
The response also sets a Set-Cookie header — a verification cookie whose name is deployment-specific (Anubis's own default differs from what a site may configure; Startpage uses spchal-cookie-verification=<uuid>). This cookie must be captured and replayed on the solve request, or the server rejects it outright.
The #anubis_challenge element's text content, parsed as JSON:
{
"rules": { "algorithm": "fast", "difficulty": 6 },
"challenge": {
"id": "<uuid>",
"randomData": "<hex string, ~128 chars>",
"difficulty": 6
}
}
The task: find the smallest non-negative integer nonce such that
SHA256(randomData_as_utf8_bytes + nonce.to_string().as_bytes())
— hex-encoded — has difficulty leading '0' hex digits. That's difficulty / 2 leading zero bytes, plus one more leading-zero nibble if difficulty is odd (checking raw digest bytes rather than the hex string avoids re-encoding on every attempt). At difficulty = 6, the winning nonce typically lands under 1000 — the whole search takes low single-digit milliseconds.
A plain GET (not a POST — the browser does window.location.replace(...), so query params alone are enough) to a fixed path:
GET {origin}/.within.website/x/cmd/anubis/api/pass-challenge
?id=<challenge.id>
&response=<hex_hash>
&nonce=<nonce>
&redir=<original request URL, url-encoded>
&elapsedTime=<ms spent solving>
— with the challenge page's Set-Cookie attached. The response is either the real page directly or a redirect to it; base_prefix/ public_url are empty in practice, so redir is just the exact original URL and the path above is used unprefixed off the origin's root.
use sha2::{Digest, Sha256};
/// Returns (nonce, hex_hash).
fn solve_anubis_pow(random_data: &str, difficulty: usize) -> (u64, String) {
let zero_bytes = difficulty / 2;
let odd_nibble = difficulty % 2 != 0;
let mut nonce: u64 = 0;
loop {
let mut hasher = Sha256::new();
hasher.update(random_data.as_bytes());
hasher.update(nonce.to_string().as_bytes());
let digest = hasher.finalize();
let leading_zero_bytes_ok = digest[..zero_bytes].iter().all(|&b| b == 0);
let next_nibble_ok = !odd_nibble || (digest[zero_bytes] >> 4) == 0;
if leading_zero_bytes_ok && next_nibble_ok {
let hex_hash = digest.iter().map(|b| format!("{b:02x}")).collect::<String>();
return (nonce, hex_hash);
}
nonce += 1;
}
}
Submission, with any HTTP client carrying a cookie jar (reqwest/ rquest with .cookie_store(true)):
let (nonce, response_hash) = solve_anubis_pow(&challenge.random_data, rules.difficulty);
let pass_response = client
.get("https://TARGET/.within.website/x/cmd/anubis/api/pass-challenge")
.query(&[
("id", challenge.id.as_str()),
("response", response_hash.as_str()),
("nonce", &nonce.to_string()),
("redir", original_request_url.as_str()),
("elapsedTime", &elapsed_ms.to_string()),
])
.send()
.await?;
Gotcha: if this runs inside a shared async codebase, don't hold an HTML-parser Document/ Selection in scope across the .await on the solve-submit request. Some HTML parsers (e.g. nipper, built on tendril) aren't Send — a value merely left in scope at an .await point, even unused, poisons the whole generated future's Send-ness. Scope the parsing step in a block that ends before the await.