Limits and errors

What stops a request, what it costs, and what every status code means. No failure mode here ever produces something a client can read as a clean wallet.

Keys

Keys begin with zsk_ and travel as a bearer token. Only a SHA-256 of the key is stored, so it can never be shown to you twice and a leaked database leaks no working credentials.

Verified keys are held in memory briefly, which is why a revoked key can still work for a few seconds. Revoke and rotate rather than relying on the moment of revocation, and mint a separate key per environment so revoking one does not stop the others.

Rate limit

Each key carries its own requests-per-second limit with a burst allowance on top, because real traffic arrives in bumps: twenty counterparties screened at once, then nothing for a minute. Sustained traffic is measured against the rate, not against the burst.

Going over answers 429 with a Retry-After header. Honour it. The limit is per key, so splitting a workload across two keys on the same account does not buy more throughput of the thing that actually costs money, which is quota.

Quota

The allowance is per account, not per key: minting a second key does not mint a second allowance. A new account opens with 100 screens, free.

A screen spends one unit whether it comes back scored or queued. Reading a record you already own, through /v1/screening/{screening_id} or /v1/wallet/{address}/screenings, costs nothing.

The 101st screen returns 402, and so does a screening call sent with no key at all. Nothing is assessed either way. The body carries the quota numbers when a key was sent, and carries x402 payment requirements in both cases: the amount and the address to pay. A PAYMENT-REQUIRED header repeats them, base64 encoded, for a client that reads headers rather than bodies.

402 Payment Required
{
  "error": "quota exhausted",
  "detail": "100 of 100 free screens used; no screening was performed; this is NOT a verdict.",
  "verdict": null,
  "quota_limit": 100,
  "quota_used": 100,
  "quota_period": "lifetime",
  "quota_reset_at": null,
  "x402Version": 2,
  "accepts": [
    {
      "scheme": "exact",
      "network": "eip155:8453",
      "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "amount": "500000",
      "payTo": "0xf13986F98F3DD6C7879482896dEFFB1Fe9755F5a",
      "maxTimeoutSeconds": 60,
      "extra": { "name": "USD Coin", "version": "2" }
    }
  ],
  "upgrade": {
    "available": true,
    "method": "x402",
    "price": "$0.50",
    "network": "eip155:8453",
    "contact": "https://calendly.com/ajay-zeru/30min"
  }
}

The counter is spent against the database on every billable request rather than against a cached copy, so two requests arriving together cannot both see the last unit.

Paying per screen

A screen past the free tier costs $0.50 USDC on Base, paid with x402. The caller signs an EIP-3009 transferWithAuthorization for that amount and sends the same request again with the signed payload in a PAYMENT-SIGNATURE header. Older x402 clients send X-PAYMENT, and both headers are accepted.

A client library handles the 402 on its own: x402 on PyPI for Python, @x402/fetch on npm for TypeScript. Give it a wallet holding USDC on Base and call the API as before, with a key or without one.

curl, the retry
# PAYMENT_SIGNATURE is the signed x402 payload, base64. An x402 client fills it in.
curl -X POST https://zscreenapi.zeruai.org/v2/screen \
  -H "PAYMENT-SIGNATURE: $PAYMENT_SIGNATURE" \
  -H "Content-Type: application/json" \
  -d '{"address":"0xd08a234c5215a8122c836187b23f60046a90afd8"}'

Nothing is charged until the answer is served. A paid request that returns 200 or 202 is settled on chain, and the response carries a PAYMENT-RESPONSE header with the transaction hash. A 422 refusal or a 5xx failure is not settled, and a signature sent a second time is refused with 402.

A paid 202 comes back with a status link carrying a poll token, so the payer can poll that one job with no key. A paid screen sent without a key still gets a screening_id, but reading it back needs a key: the record and history routes both require one. POST /v1/screen/batch takes a key only, and when the free tier runs out partway through a batch each remaining address is reported as not screened, with a pointer to pay per screen at POST /v2/screen.

Records and freshness

A scored answer lives for 24 hours. After that the next request still receives the last known answer, marked stale, while a refresh runs behind it. The stored record itself is permanent and never expires.

A batch takes at most 100 addresses. A history read takes an optional limit, default 50 and capped at 500.

Address coverage

zScreen screens Ethereum mainnet history for EVM-format addresses: 0x followed by 40 hex characters. An address in a different format, or history that only exists on another chain, is outside what this API can answer today.

Every status code

200OK

A scored answer. The one response that carries a verdict.

202Accepted

Never screened before, so the history is being fetched. Carries a job_id and no verdict. Poll GET /v2/job/{job_id}.

401Unauthorized

No key, a malformed header, or a key that is unknown or revoked. The message says which.

402Payment Required

The allowance is spent, or no key was sent. No screening was performed. The body states the numbers, carries an explicit null verdict, and carries the x402 payment requirements for paying per screen instead.

404Not Found

No such job, or no such record. A record belonging to another customer answers 404 as well, because which ids exist is not yours to learn.

422Unprocessable Entity

The address is not 0x followed by 40 hex characters, or a field is missing. Nothing was screened and nothing was spent.

429Too Many Requests

Over the rate limit for this key. Carries Retry-After. Nothing was spent.

500Internal Server Error

Something unforeseen. Still JSON, still carries a null verdict, and carries no internals.

503Service Unavailable

The screening store is briefly unreachable. Carries Retry-After, and the pool usually heals within seconds.

503 body
{
  "error": "database unavailable",
  "detail": "The screening store is temporarily unreachable. Retry shortly. This is NOT a verdict.",
  "verdict": null
}

Retrying safely

  • 429 and 503 are worth retrying, with backoff and the Retry-After header respected. Neither spent anything.
  • 401 and 422 will fail identically on a retry. Fix the key or the address.
  • 402 is a retry with a payment attached, or a stop. Sending the same request again unchanged fails the same way.
  • 202 is not a failure. Poll the job rather than resending the screen, which would spend another unit for the same answer.

Never fail open

Every error on this page carries a null verdict on purpose. A client that treats any non-FLAGGED response as permission to proceed turns an outage into an approval. Check that a verdict is present before you look at what it says.