live/stable
The gate
Put a check in front of an endpoint with @recensus/kya — refuse by flag or tier, or price by tier, in Hono, Express or Python.
@recensus/kya puts a check in front of an endpoint. It finds out who is asking, checks them, and refuses or passes the request before your handler runs. It is built from the agent lane and has no x402 dependency, so it works in front of any x402 seller.
npm i @recensus/kyaHono
import { createKya, kyaGate, kyaPaywall } from '@recensus/kya';
const kya = createKya({ apiKey: process.env.RECENSUS_KEY }); // apiUrl?, cacheSeconds = 60
await kya.check('erc8004:42'); // the check's answer
app.use('/paid/*', kyaGate(kya, { minTier: 'bronze' })); // refuse below a tier
app.use('/paid/*', kyaGate(kya, { refuseUnbonded: true, refuseFlags: true }));
// downstream: c.get('kya') is the answerExpress
import { createKya, kyaGateExpress, kyaPrice } from '@recensus/kya';
const kya = createKya({ apiKey: process.env.RECENSUS_KEY });
app.use('/paid', kyaGateExpress(kya, { refuseFlags: true }));
// downstream: req.kya is the answer
// For another x402 seller: a price function by tier.
const price = kyaPrice(kya, { gold: '0.0005', unbonded: '0.002' });Python (Starlette, FastAPI)
The gate is in recensus-sdk, module recensus_sdk.kya: KyaClient with check, KyaGate, ASGI middleware with the same options and codes, and price_for(answer, prices) for price by tier. It reads the payment headers and leaves the payment itself to your x402 library.
from recensus_sdk.kya import KyaClient, KyaGate, price_for
kya = KyaClient(api_key=os.environ["RECENSUS_KEY"])
app.add_middleware(KyaGate, client=kya, refuse_flags=True, min_tier="bronze")Who is asking
- the payer of an x402 payment in the request:
PAYMENT-SIGNATURE(v2) orX-PAYMENT(v1),payload.authorization.from; a labelled nonce names the agent ID too; - or an agent-lane signature (the lane's headers, verified locally exactly as the lane does).
With neither, the gate passes the request on, so your x402 middleware can answer 402; requireIdentity: true refuses it instead.
The decision
In this order:
- check the subject (cached
cacheSeconds, answers and misses alike; concurrent checks of one subject share one request); - not an agent →
KYA_NOT_AN_AGENT, unlessallowNonAgents; - a flag in
refuseFlags(truemeans every flag) →KYA_FLAGGED; - unbonded with
refuseUnbonded→KYA_UNBONDED; - a tier below
minTier→KYA_TIER_TOO_LOW.
Each refusal is 403 with { error, detail, check }. When the KYA API cannot be reached the gate answers 503 KYA_UNAVAILABLE, or passes the request with failOpen: true. Until bonds are live on mainnet every agent there is unbonded, so minTier and refuseUnbonded refuse every mainnet agent today.
The trust rule
A payer named in a payment header is a claim until the payment's signature is verified, which your x402 middleware (or its facilitator) does before anything is served. A header naming someone else's wallet therefore buys a refusal or a failed payment, never access. That is why the gate runs before your x402 middleware, and why kyaPaywall verifies before it serves.
Price by tier
kyaPaywall is a minimal x402 v2 exact seller that charges by tier:
- no payment →
402quoting the price for the tier of whoever the request identifies (an agent-lane signature or aRecensus-Payerhint), else theunbondedprice; - a payment → the gate's checks, then the amount must be at least the price for the payer's actual tier (
KYA_PRICE_TOO_LOW,402with the right quote), then verify the payment, serve, settle.
A tier with no price takes the next lower tier's. The Recensus API's own pay per check is this paywall.
app.use('/paid/*', kyaPaywall(kya, { payTo, prices: { gold: '0.0005', unbonded: '0.002' }, refuseFlags: true }));Codes
KYA_NOT_AN_AGENT · KYA_FLAGGED · KYA_UNBONDED · KYA_TIER_TOO_LOW · KYA_PRICE_TOO_LOW · KYA_UNAVAILABLE
Example
examples/kya-seller/ in the repository is a Hono endpoint behind kyaPaywall, with a README. Payments are x402 exact in USDG on eip155:4663, or eip155:46630 for Robinhood Chain testnet.