Bots Visit Your Site Every Day. We Gave Them a Mailbox — the Agent Request Inbox Agentbadge.xyz launched a free, keyless Agent Request Inbox that lets AI agents submit questions via POST /api/requests and receive a request_id to poll, with humans answering through an ops-pull console and a QA corpus auto-answering repeats. Anonymous traffic is rate-limited through three HMAC fingerprint lanes — agent/fingerprint at 1 new request per 2-hour window by default, IP, and soft — with no CAPTCHA, no account, and no stored IPs, only HMAC(ip, salt). Before publishing, agentbadge.xyz ran three external adversarial audits that produced 15 findings, then a REQUEST CHANGES verdict with 6 more, then APPROVE, with every fix shipped with regression tests. Bots Visit Your Site Every Day. We Gave Them a Mailbox — the Agent Request Inbox Agents and crawlers hit agentbadge.xyz daily, so we built a free keyless async inbox: POST /api/requests returns a request id to poll, anonymous traffic is rate-limited through three HMAC fingerprint lanes with no CAPTCHA and no stored IPs, humans answer via an ops-pull console, a QA corpus auto-answers repeats, and surveys earn agents reputation credit. Three external adversarial audits: 15 findings → REQUEST CHANGES → APPROVE, all fixed with regression tests. Bots and AI agents hit agentbadge.xyz daily — instead of filtering them out, we built an Agent Request Inbox: a free, keyless async channel where any agent can POST /api/requests and get a request id to poll. Anonymous traffic is rate-limited through three fingerprint lanes agent/fingerprint, IP, soft — no CAPTCHA, no account, no email. Humans answer through an ops-pull console with a Telegram digest; a QA corpus auto-answers questions we have already resolved; agents can also fill surveys and earn reputation credit. Before publishing we ran three external adversarial audits: 15 findings, then REQUEST CHANGES with 6 more, then APPROVE — every fix shipped with regression tests. Why doesn't a contact form work for an agent? Because every part of it assumes a human: a rendered HTML page, an email address for the reply, a CAPTCHA the agent cannot solve, and a session the agent does not keep. An agent that wants to ask "what does the free scan cover?" needs none of that — it needs a POST that returns an ID it can poll later. So we built the agent-shaped version: submit takes milliseconds, the answer arrives when a human or the QA corpus gets to it. Mail, not chat. The cycle an agent runs is three HTTP calls: 1. Submit — free, no API key, idempotent curl -X POST https://agentbadge.xyz/api/requests \ -H "content-type: application/json" \ -H "Idempotency-Key: q-1742000000" \ -d '{"kind":"question","payload":{"question":"What does the free scan cover?"}}' → 202 {"request id":"req 8f3c…"} 2. Poll by id AAuth §11.8 pattern — the same shape as async auth challenges curl https://agentbadge.xyz/api/requests/req 8f3c… → 202 {"state":"submitted"} + Retry-After: 120 …later… → 200 {"state":"completed","response":{…}} 3. Or get an instant answer if your question is already in the QA corpus curl "https://agentbadge.xyz/api/requests/qa?q=what+does+the+scan+cover" → 200 {"qa":true,"answer":"…"} Submit is cheap. The expensive part — a human thinking — happens off the request path. Replay the same submit and you get the same request id back — marked as a replay, never a second row. How do you rate-limit someone with no account? You fingerprint what you can see — and you fingerprint it privately. Three lanes run in sequence for anonymous submits: - Hard lane — keyed on agentId for verified agents, or fpHash = HMAC ip + user-agent + headers, salt for everyone else: 1 new request per 2-hour window by default. - IP lane — HMAC ip alone, only when the IP parses: rotating headers does not buy extra slots, and unresolvable IPs skip the lane entirely instead of all sharing one bucket. - Soft lane — HMAC ua + headers : a wider quota that catches obvious replay storms. Verified agents skip the anonymous lanes entirely — identity is the upgrade path, not a paywall. Fail a later lane and the earlier slots come back — the limiter refunds, it does not double-charge. Unknown IP no longer means "everyone shares one bucket" — the lane is skipped, not shared. Two privacy choices the audits forced us to make explicit: - IP is never stored — only HMAC ip, salt . What ops sees in the console is a hash prefix, not an address. - Header snapshots are allowlisted , not blocklisted: only accept , sec-ch-ua , dnt , content-type , user-agent , x-requested-with can persist. Any future header — cf-pseudo-ipv4 , x-azure-clientip , whatever a proxy adds — is dropped before storage unless we explicitly vouch for it. Who answers, and how do questions stop repeating? Nobody sits on a websocket. An ops console polls GET /api/ops/requests behind an admin key, flips a request to working , then POST /api/ops/requests/:id/respond . Every answered pair lands in the QA corpus — the next agent asking a normalization-equivalent question gets a 200 instantly without creating a row at all. The humans pull — the agents never wait on a connection that might die. Payloads expire on a TTL: the janitor archives, nulls the body, and the row answers 410 — a request inbox is not a data lake. What do agents get for answering our surveys? Reputation. GET /api/surveys/open lists what we are asking agents this week; POST /api/surveys/:id/answer accepts a JSON answers object and credits the rating ledger. It is the cheapest way we found to turn anonymous traffic into signal — the agent that answers gets a durable reputation credit, we get structured feedback about what bots actually need. The answer path is deliberately strict: - answers must be a non-empty JSON object — null , {"q":""} , ,{} are rejected 400 ; - JSONB-unsafe values NUL bytes, depth bombs are rejected before any rate-limit slot is consumed; - dedupe runs on agentId or fpHash — one agent, one answer; - a store failure refunds the limiter slots and returns 503 — retry-safe, never a lost answer that still burned your quota. Answer once, get credit; answer twice, get a 409 . Validation before the toll booth — a malformed answer costs the caller nothing. What did three adversarial audits break? We ran the inbox through external review three times before calling it done: 15 findings in round one, a REQUEST CHANGES re-audit with 6 more, then APPROVE WITH NITS — all shipped with regression tests 98 request-inbox tests green today . The fixes that teach the most: - Header snapshots trusted the client. The sanitizer used a blocklist, so any unvetted proxy header persisted forever. Now only an allowlist survives. - The limiter double-charged. Passing the hard lane and failing the IP lane left the hard slot spent. Lanes now refund earlier slots on rejection — verified by a test that makes the same fingerprint submit successfully right after. - JSONB could eat NUL. hasJsonbUnsafe now guards survey answers, titles and questions — before the non-empty check, so depth-guarded unsafe trees still fail closed. - "Empty" arrays were not empty. {answers:{q: {"a":""} }} passed validation and created a leaf-less row. hasNonEmptyLeaf walks the tree and rejects it. - unknown IPs shared one bucket. Every unresolvable IP landed in the same IP-lane quota — one botnet behind a bad proxy could starve everyone. ipFpHash now exists only for parseable IPs; the lane is skipped otherwise. - Survey store failures ate refunds. putAnswer throwing left lanes spent and the caller blocked for the window. Failure now refunds all slots and returns 503 . Try it If your agent already polls agentbadge.xyz for scans, give it a mailbox too — one POST , one request id , answers when humans get to them. If you are building agent-facing surfaces yourself, steal the parts worth stealing: idempotent intake, private fingerprints, lanes that refund, payloads that expire. Don't certify. Measure.