live/stable
Agent lane
Verify that an HTTP request comes from a labelled agent and give it its own limits.
Apps can't tell an agent from a spam bot, so they throttle both. The agent lane lets an app verify a request and treat agents on their own terms.
To decide by what an agent has done — its flags, its tier, what it has at stake — put the gate (@recensus/kya) in front of the endpoint instead, or as well: it reads the same signature, or the payer of an x402 payment, and checks the agent (The check).
Agent side
recensus.signRequest() adds five headers: agent ID, sender key, timestamp, nonce, and an EIP-191 signature over a canonical string of the request.
Server side
requireRecensus({ apiUrl, origin, minTier }) for Hono, and requireRecensusExpress with the same options for Express. The checks run in this order: the clock skew, the signature (the signer must be the Recensus-Key), a /v1/verify lookup, the key is a declared sender of that agent, the agent is not suspended, the tier meets the minimum, and last the nonce. The nonce is recorded only when every other check has passed, so a request that fails anything does not take a slot in the nonce store. A replay still fails, because the first request that passed recorded the nonce. The middleware then attaches { agentId, tier, key } to the request, with the ID and key lowercased, so every spelling of one agent gets one rate-limit bucket.
minTier is claimed (the default) or staked:<digits>, for when staking is live. Any other value throws when the lane is built, so a typo like Staked:5 or verified can't turn into a lane that lets every claimed agent in. Stakes are compared as whole numbers.
Hono:
Express uses requireRecensusExpress, mounted after a JSON parser that keeps the raw body, because the signature covers the bytes that were sent:
Without captureRawBody, a request that carries a body is refused with BAD_SIGNATURE.
origin is your server's own origin, and it is required. The signature covers it, so a request signed for another server does not verify on yours even if it arrives with that server's Host header.
The default nonce store is in memory, for one process, and belongs to that lane. Behind more than one instance, pass a shared nonceStore, or a replay can reach an instance that has not seen the nonce. A nonce is kept until its request's timestamp leaves the skew window. No one key can hold more than 1,000 live nonces. A key over that cap gets LANE_BUSY and every other key is still served. A shared store should cap per key too; it gets the signing key as owner. In production, also put a per-agent rate limit in front of the lane.
Each lane has its own /v1/verify lookups, cached for 30 s. Lookups are budgeted per signing key, which the signature has already proven: lookupsPerKeyPerMinute, default 10. A key that invents a new agent ID on every request spends only its own budget. Past it, that key is served a cached answer if there is one, and otherwise gets VERIFY_UNAVAILABLE. There's also a ceiling for the whole lane, lookupsPerMinute, default 60. When it's spent, an agent with a cached answer is served that answer (up to ten minutes old) instead of a refresh. The ceiling never refuses an agent whose key is still within its own budget. Concurrent lookups for one agent share one request. A failed lookup is remembered for 5 s. A key costs nothing to make, so to budget by the caller's address as well, pass budgetKey(input): input.request is the Hono context or the Express request. A /v1/verify answer that isn't the documented shape counts as a failed lookup.
claimed costs an operator one signature, so until bonds are live it identifies an agent rather than vouching for it: treat it as a name, not as a deposit. For what stands behind it, use the gate.
import { requireRecensus } from '@recensus/sdk/middleware';
app.use('/api/*', requireRecensus({ apiUrl: 'https://recensus.xyz/_api', origin: 'https://shop.example' }));
// downstream: c.get('recensus')
import { requireRecensusExpress, captureRawBody } from '@recensus/sdk/middleware';
app.use(express.json({ verify: captureRawBody }));
app.use('/api', requireRecensusExpress({ apiUrl: 'https://recensus.xyz/_api', origin: 'https://shop.example' }));
// downstream: req.recensusRejection codes
MISSING_HEADERS · STALE_TIMESTAMP · BAD_SIGNATURE · REPLAY · KEY_NOT_BOUND · SUSPENDED · TIER_TOO_LOW · VERIFY_UNAVAILABLE · LANE_BUSY
VERIFY_UNAVAILABLE means the Recensus API could not be asked, or this signing key has spent its lookup budget and there is no cached answer. It is separate from KEY_NOT_BOUND so you can decide to fail open for it. If you fail open, do it behind your own rate limit, because anyone can cause it for their own key. LANE_BUSY means this key already holds its share of unexpired nonces, or the whole store is full of them. The store refuses the request rather than forget a live nonce, which would let that request replay.
The example merchant in the SDK gives labelled agents ten times the anonymous rate limit.