<!-- Generated from https://zscreen.zeruai.org/docs/limits by scripts/agent-docs.mjs. Do not edit. -->

# 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.

```
{
  "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](https://docs.x402.org). 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.

```
# 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

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

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

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

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.

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.

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

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

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

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

```
{
  "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.
