Endpoints
Every route, what it takes, and what comes back. All of them need a bearer key and all of them answer in JSON.
v2 is the API
Screening, polling and the report are all on /v2. The older /v1 paths still answer and return the same body, so nothing built on them breaks, but they are not where new work goes. Three endpoints have no /v2 address yet and are marked below: the batch screen, one permanent record, and a wallet's own history.
Screen an address
v2 is the screening surface
Screening and its jobs are on /v2, where the number and the word always agree. Batch, the permanent record and wallet history have no v2 yet and stay on /v1. Both read the same stored row, so a wallet cannot be CLEAR on one door and queued on the other.
Takes one field, address. Returns 200 with the scored body when the address has a record, or 202 with a job id when its history still has to be fetched.
curl https://zscreenapi.zeruai.org/v2/screen \
-H "Authorization: Bearer zsk_..." \
-H "Content-Type: application/json" \
-d '{"address":"0xd08a234c5215a8122c836187b23f60046a90afd8"}'const res = await fetch("https://zscreenapi.zeruai.org/v2/screen", {
method: "POST",
headers: {
"Authorization": "Bearer zsk_...",
"Content-Type": "application/json",
},
body: JSON.stringify({ address: "0xd08a234c5215a8122c836187b23f60046a90afd8" }),
});
const data = await res.json();Identical behaviour with the address in the path. Convenient from a browser or a shell, and it is a read, so it is safe to retry.
curl https://zscreenapi.zeruai.org/v2/screen/0xd08a234c5215a8122c836187b23f60046a90afd8 \
-H "Authorization: Bearer zsk_..."const res = await fetch("https://zscreenapi.zeruai.org/v2/screen/0xd08a234c5215a8122c836187b23f60046a90afd8", {
headers: { "Authorization": "Bearer zsk_..." },
});
const data = await res.json();Both are billable
A screen spends one unit of your allowance whether it comes back scored or queued, and whether it was computed now or served from the stored record. Reading a record you already have, through the two routes below, costs nothing.
Screen a batch
Takes addresses, an array of 1 to 100. Each one is screened in turn and sorted into one of three lists, so a malformed address in position 4 does not cost you the other 99 answers.
curl https://zscreenapi.zeruai.org/v1/screen/batch \
-H "Authorization: Bearer zsk_..." \
-H "Content-Type: application/json" \
-d '{"addresses":["0xd08a234c5215a8122c836187b23f60046a90afd8","0x97b1043abd9e6fc31681635166d430a458d14f9c"]}'const res = await fetch("https://zscreenapi.zeruai.org/v1/screen/batch", {
method: "POST",
headers: {
"Authorization": "Bearer zsk_...",
"Content-Type": "application/json",
},
body: JSON.stringify({ addresses: ["0xd08a234c5215a8122c836187b23f60046a90afd8", "0x97b1043abd9e6fc31681635166d430a458d14f9c"] }),
});
const { results, queued, errors } = await res.json();{
"results": [ { "address": "0xd08a234c5215a8122c836187b23f60046a90afd8", "verdict": "REVIEW", "...": "..." } ],
"queued": [ { "address": "0x1f39...7125", "status": "queued", "job_id": 90114 } ],
"errors": [ { "address": "0xnope", "error": "address must be 0x followed by 40 hex characters" } ],
"counts": { "scored": 1, "queued": 1, "failed": 1 }
}If the allowance runs out partway through, everything already screened stands and every address after it is reported in errors as not screened. No verdict is ever invented to fill a gap.
Batch is key-only: it never takes payment per screen. A spent allowance is told where a single screen can be bought instead.
The full report
A screen is built to be read in two seconds: a word, a number, one reason. The report is the same answer for somebody who has to file the wallet, with money in and out, the counterparties behind it, how close it sits to a listed address, the patterns it shows, and three paragraphs of prose.
curl https://zscreenapi.zeruai.org/v2/report/0xd08a234c5215a8122c836187b23f60046a90afd8 \
-H "Authorization: Bearer zsk_..."It shares its serving path with the screen, so the verdict can never disagree, and the three answers are the same three. Field by field, it is on its own page.
Check a job
state moves through queued, running, and then either done or failed. Once it is done, the full scored body is inside result, so a successful poll ends the exchange with no second call.
const res = await fetch("https://zscreenapi.zeruai.org/v2/job/90114", {
headers: { "Authorization": "Bearer zsk_..." },
});
const job = await res.json();
if (job.state === "done") console.log(job.result.verdict);{
"job_id": 90114,
"address": "0x1f39eb3896756172728a2cfb116a6ec37d807125",
"state": "done",
"attempts": 1,
"error": null,
"created_at": "2026-09-01T09:12:40Z",
"finished_at": "2026-09-01T09:14:22Z",
"result": { "verdict": "CLEAR", "risk_score": 4.1, "...": "the v2 screen body" }
}A failed job carries the reason in error and no result. It is a failure to gather data, not a finding about the wallet.
Records and history
Every scored response is written down and handed back an id. This route returns that record byte for byte, which is what makes a decision you made last quarter reviewable this quarter.
Records are scoped to the key that created them. A record belonging to another customer answers 404 rather than 403, because which ids exist is not yours to learn either.
curl https://zscreenapi.zeruai.org/v1/screening/418822 \
-H "Authorization: Bearer zsk_..."const res = await fetch("https://zscreenapi.zeruai.org/v1/screening/418822", {
headers: { "Authorization": "Bearer zsk_..." },
});
const record = await res.json();Every check your key has run against one address, newest first. Takes an optional limit, default 50 and capped at 500. Scoped the same way: one customer's search history is not another customer's business.
Operational
These three need no key. /healthz answers as soon as the process is running; /readyz answers only once its dependencies are reachable, which is the one to put in front of a load balancer.
Status codes, rate limits and quota behaviour are on Limits and errors.