Proof of Work
vrfy.lol uses proof-of-work instead of API keys for abuse prevention. When you hit the free tier limit, solve a SHA-256 challenge and include the solution in your request. No accounts. No signup. No tokens.
How it works
- Make a request to
POST /orPOST /batchas normal. - If you exceed the free tier (10/hour or 50/day per IP), you get a
429response with apowchallenge object. - Find a nonce where
SHA-256(challenge + ":" + nonce)has N leading zero bits. - Resubmit your request with the
powfield containing the challenge and nonce. - Server verifies in microseconds. Request proceeds with no rate limit.
The challenge object
When rate-limited, the response body includes:
{
"error": "rate_limited",
"pow": {
"algorithm": "sha256",
"challenge": "a9f3...hex string...",
"difficulty": 20,
"expires": 1719403500
}
}
| Field | Description |
|---|---|
algorithm | Always sha256 |
challenge | Hex-encoded HMAC — unique to your IP + 5-minute window |
difficulty | Number of leading zero bits required (default 20 for single, higher for batch) |
expires | Unix timestamp — challenge is valid until this time |
Submitting a solution
Include the pow object in your request body alongside email:
{
"email": "user@example.com",
"pow": {
"challenge": "a9f3...the challenge you received...",
"nonce": "your solution nonce"
}
}
Solving the challenge
Find a nonce string where SHA-256(challenge + ":" + nonce) starts with difficulty leading zero bits. This is the Hashcash algorithm — brute-force incrementing a counter until the hash matches.
With difficulty 20, expect ~1 million iterations (~2–8 seconds on modern hardware). Each extra bit doubles the work.
JavaScript (Web Crypto)
async function solvePow(challenge, difficulty) {
const encoder = new TextEncoder();
let nonce = 0;
while (true) {
const input = challenge + ':' + nonce;
const hash = new Uint8Array(
await crypto.subtle.digest('SHA-256', encoder.encode(input))
);
if (countLeadingZeroBits(hash) >= difficulty) {
return String(nonce);
}
nonce++;
}
}
function countLeadingZeroBits(hash) {
let bits = 0;
for (const byte of hash) {
if (byte === 0) { bits += 8; continue; }
bits += Math.clz32(byte) - 24;
break;
}
return bits;
}
Bash (openssl)
solve_pow() {
local challenge="$1" difficulty="$2" nonce=0
local target=$((difficulty / 4)) # hex chars
local zeros=$(printf '%0*d' "$target" 0)
while true; do
hash=$(printf '%s:%d' "$challenge" "$nonce" \
| openssl dgst -sha256 -binary | xxd -p -c 64)
if [ "${hash:0:$target}" = "$zeros" ]; then
echo "$nonce"; return
fi
nonce=$((nonce + 1))
done
}
Batch difficulty scaling
For POST /batch, difficulty scales with batch size:
difficulty = 20 + floor(log2(batch_size))
2 emails = 21 bits. 10 emails = 23 bits. 20 emails = 24 bits. This keeps the cost proportional to the work the server does.
Design notes
- Stateless. Challenges are deterministic — derived from your IP + a 5-minute time bucket. The server never stores them.
- Clock-edge tolerance. The server accepts solutions for the current AND previous time bucket, so challenges don't expire at the boundary.
- Nonce replay protection. Each nonce can only be used once within its challenge window.
- No accounts. PoW replaces API keys entirely. Compute is the credential.
Rate limits
| Tier | Limit |
|---|---|
| Free | 10 requests/hour + 50/day per IP |
| With PoW | Unlimited |
Client libraries
All official clients solve PoW transparently — you never need to implement this yourself unless you're calling the API directly.
- CLI & Libraries — Go, Node, Python, and bash clients with built-in PoW
- API Reference