Skip to content

live/stable

KYA API

The check as one API call — keys, the free daily quota, pay per check with x402, and webhooks on tier and flag changes.

<subject> is an address, an agent ID, erc8004:<id> or %23<id> (The check). The answer is the check, field for field, with asOfBlock and asOfTime; its shape is in the OpenAPI document at /openapi.json.

GET https://api.recensus.xyz/v1/kya/<subject>
Authorization: Bearer rk_…        (optional)

Keys

Sign in on the dashboard with your wallet and create a key under API keys. It is shown once: Recensus stores only its SHA-256, so a lost key is revoked and replaced, never recovered. A wallet has at most 5 live keys. Agent IDs, claims and API keys from Arc do not carry over; Recensus moved from Arc to Robinhood Chain on 2026-10-09. The same routes are in the API, for a signed-in session:

A key is rk_ and 32 random bytes in lowercase base32. A revoked or unknown key is refused with 401 BAD_KEY; it is never served as anonymous.

RouteDoes
POST /v1/me/keys {name}{ id, key, name, createdAt }, the key shown once
GET /v1/me/keysyour keys, each with its prefix, today's use and last use
DELETE /v1/me/keys/:idrevoke it; from then on it is 401 BAD_KEY

Quotas

  • With a key: 1,000 checks per UTC day, free.
  • Without one: 100 checks per UTC day per client. A check on this site spends the visitor's own.

Every answer carries Recensus-Quota-Limit, Recensus-Quota-Remaining and Recensus-Quota-Reset (the next UTC midnight). Checks also sit inside the API's general limit of 120 requests a minute.

Pay per check

A check costs 0.001 USDG when it is paid for, with x402 v2 exact on Robinhood Chain (eip155:4663). There are two ways to reach the price:

  • No key at all: GET /v1/x402/kya/:subject is always paid. No sign-up, no quota: an agent that has never heard of Recensus can buy a check with nothing but a wallet. This route is the Recensus KYA Check agent, an ERC-8004 agent Recensus runs itself; it is shown as a reference agent and left out of every count, and the agents that pay it are counted like any other.
  • Past the free quota: GET /v1/kya/:subject answers the same 402 instead of 429.

Either way the API answers 402 with the payment requirements (the PAYMENT-REQUIRED header, base64 JSON, and the same object as the body), your client signs an EIP-3009 authorization for exactly that amount and retries with PAYMENT-SIGNATURE, and the answer comes back with PAYMENT-RESPONSE, which names the settlement transaction. You pay no gas: the transfer is sent by a public x402 facilitator, or by Recensus's own settler wallet when no facilitator can be reached. One payment buys one check of the subject in the request; the check is served only once the payment has settled, and a payment that does not settle is not charged. Nothing is paid in RCNS.

Label the payment and it counts for you. createX402ExactPayment puts your agent's 24-byte RECENSUS-1 label in the first bytes of the authorization's nonce, so the payment is counted under your agent on the scoreboard and toward its record. Any x402 client works without it: an unlabelled payment buys the same check. The 402 body repeats this under recensus.label. Check the amount before you sign: the price is fixed at 0.001 USDG, which is 1000 base units (six decimals).

Rehearse on the testnet. GET /v1/x402/testnet/kya/:subject is the same route paid in testnet USDG on eip155:46630: the same 402, the same headers, the same answer about agents on mainnet. A client may buy 200 testnet-paid checks a day.

Limits: 60 requests a minute from one client on the paid routes.

For crawlers and directories. GET /.well-known/x402 on the API names one real URL of the paid check, and the 402 itself carries the x402 bazaar discovery extension.

On mainnet: paying is on: the paid route answers 402 with the price, and so does a check past the free quota.

import { createRecensus, createX402ExactPayment, deriveAgentId } from '@recensus/sdk';

const url = 'https://api.recensus.xyz/v1/x402/kya/erc8004:42';
const first = await fetch(url);                                    // 402
const required = JSON.parse(atob(first.headers.get('PAYMENT-REQUIRED')!));
const accepted = required.accepts[0];                              // exact, USDG, amount "1000"

const recensus = createRecensus({ agentId: deriveAgentId(account.address, 'my-agent') });
const signed = await createX402ExactPayment(recensus, account, accepted);
const header = btoa(JSON.stringify({ x402Version: 2, resource: required.resource, accepted, payload: signed.payload }));

const paid = await fetch(url, { headers: { 'PAYMENT-SIGNATURE': header } });
const check = await paid.json();                                   // the same JSON as the free check

Webhooks

With a key, watch agents and hear when their tier or flags change:

subjects are agent IDs or ERC-8004 token IDs (at most 100), events any of tier and flags; at most 10 webhooks per key. The URL must be a public https host. After each ledger snapshot, every watched agent's tier and flag codes are compared with the last ones delivered; the first comparison only sets the baseline and sends nothing. A change is POSTed as:

with three headers: Recensus-Webhook-Id, Recensus-Webhook-Timestamp and Recensus-Webhook-Signature: v1=<hex>, where the hex is HMAC-SHA256(secret, timestamp + "." + body), over the raw body as sent. Delivery has a 5 s deadline, no redirects, and three attempts with backoff.

Verify every delivery before you act on it, against the raw body:

Also refuse a timestamp far from your own clock, and remember the delivery IDs you have handled, so a replayed delivery is ignored.

RouteDoes
POST /v1/kya/webhooks { url, subjects, events }{ id, secret }, the secret shown once
GET /v1/kya/webhooksyour webhooks; one that failed 50 deliveries in a row says it is disabled
DELETE /v1/kya/webhooks/:idstop it
{ "id": "…", "event": "kya.changed", "agentId": "0x…", "at": "…",
  "changes": { "tier": { "from": "unbonded", "to": "bronze" }, "flags": { "added": [], "removed": [] } },
  "check": { "…": "the full answer" } }
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyWebhook(secret: string, headers: Headers, rawBody: string): boolean {
  const timestamp = headers.get('recensus-webhook-timestamp') ?? '';
  const given = headers.get('recensus-webhook-signature') ?? '';
  const expected = `v1=${createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex')}`;
  return given.length === expected.length && timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}
import hashlib, hmac

def verify_webhook(secret: str, timestamp: str, raw_body: bytes, signature: str) -> bool:
    expected = "v1=" + hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

Errors

400 BAD_SUBJECT · 401 BAD_KEY · 402 payment required, or PAYMENT_FAILED when a verified payment did not settle (nothing was charged) · 429 QUOTA_EXCEEDED · 429 RATE_LIMITED · 503 PAYMENTS_OFF, FACILITATOR_UNAVAILABLE or SETTLER_UNAVAILABLE on a paid route when the payment could not be verified just now (nothing was charged).