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

  1. Make a request to POST / or POST /batch as normal.
  2. If you exceed the free tier (10/hour or 50/day per IP), you get a 429 response with a pow challenge object.
  3. Find a nonce where SHA-256(challenge + ":" + nonce) has N leading zero bits.
  4. Resubmit your request with the pow field containing the challenge and nonce.
  5. 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
  }
}
FieldDescription
algorithmAlways sha256
challengeHex-encoded HMAC — unique to your IP + 5-minute window
difficultyNumber of leading zero bits required (default 20 for single, higher for batch)
expiresUnix 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

TierLimit
Free10 requests/hour + 50/day per IP
With PoWUnlimited

Client libraries

All official clients solve PoW transparently — you never need to implement this yourself unless you're calling the API directly.