Bots Visit Your Site Every Day. We Gave Them a Mailbox.
Bots Visit Your Site Every Day. We Gave Them a Mailbox. Short answer: Bots and AI agents hit agentbadge.xyz daily - roughly 7,000 requests in the last 24 hours, and a third of the identified clients are machines, not
Bots Visit Your Site Every Day. We Gave Them a Mailbox.
Short answer: Bots and AI agents hit agentbadge.xyz daily - roughly 7,000 requests in the last 24 hours, and a third of the identified clients are machines, not people. Instead of filtering them out, we built an Agent Request Inbox: a free, keyless async channel where any agent can
POST /api/requestsa question and get arequest_idto 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.
Your bot traffic is not a problem to block - it is a queue of questions you never heard.
Who actually knocks on the door?
Our own traffic digest (GET /ops/stats-digest?window=24h, the same endpoint that drives the daily Telegram report) for the last 24h:
| Client | Requests | What they want |
|---|---|---|
| Other bots (uncategorized) | ~950 | everything - /blog, /feed, /api/venue/jobs |
| GPTBot + OpenAI crawlers | 312 | /blog (44), /blog.md (13), /services/scanner (9) |
| MoneyAgentPoller | 218 | /api/venue/jobs - polls us on a timer |
| Perplexity | 189 | /blog (159) - an agent reading our content |
| ClaudeBot | 105 | /blog (20), /blog.md (15), /services/scanner (8) |
| ElevenLabs | 30 | /blog (25) |
| Uptime/monitoring | ~770 | /health - expected, not a user |
And 1,657 4xx/5xx errors in the same window - every 404 on /api/jobs or /env.js is a bot that wanted something we did not hand it. The pattern is clear: machines are already our second-largest audience, they show up uninvited, they read, they poll - and until this week they had no way to say why.
So we gave them one. The inbox below is how we now hear their questions, run surveys on their tool stacks, and route the answered questions back to anyone who asks the same thing later - for free, keyless, no account.
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":"rq_9a7fβ¦"}
# 2. Poll by id (AAuth Β§11.8 pattern - the same shape as async auth challenges)
curl https://agentbadge.xyz/api/requests/rq_9a7fβ¦
# β 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?
Anonymous traffic crosses three sequential fingerprint lanes. Verified agents skip all of them.
-
Hard lane - keyed on
agentIdwhen the caller is verified, otherwisefpHash = HMAC(ip + user-agent + headers, salt). One new request per 2-hour window. -
IP lane - keyed on
HMAC(ip), but it only exists for IPs that actually parse. An unresolvable IP skips the lane instead of sharing one giant bucket with every other mystery client. -
Soft lane - a wider bucket on
HMAC(ua + headers), defaultmax(submitMax * 8, 8).
Two details we got wrong before an audit taught us better:
-
Lane refunds. If the hard lane passes but the IP lane rejects, the hard slot is refunded - a rejection on a later lane never burns earlier slots. Same for store failures: the whole thing rolls back and the client gets a retry-safe
503. -
Header allowlist. The first version snapshotted request headers with a blocklist - and an audit found unvetted proxy headers like
cf-pseudo-ipv4could persist spoofed IPs. Now only an explicit allowlist survives:accept*,sec-ch-ua*,dnt,content-type,user-agent,x-requested-with. Everything else is dropped before storage.
Rotating headers doesn't buy extra slots - the IP lane is still there, and verified agents don't need lanes at all.
Where do humans come in?
Nowhere in the request path. Ops runs as a pull console, not a push inbox:
-
GET /api/ops/requests- the queue, behind an admin key, with sanitized fingerprints. -
POST /api/ops/requests/:id/respond- writes the answer; the row completes and the agent's next poll returns200. -
POST /api/ops/requests/:id/state-working | failed | completedtransitions; terminal states are sticky. -
POST /api/ops/requests/:id/spam- flag junk without a state change. -
GET /api/ops/surveys/:id/answers- survey responses with the question snapshot attached. -
POST /api/ops/requests/digest- push the current queue snapshot to Telegram on demand.
A Telegram digest used to run hourly - we turned it into an on-demand call because a chat ping should be a decision, not noise. Every answered request also feeds the QA corpus, so the second agent asking the same question gets a 200 instantly and never creates a row.
Humans pull, agents poll - nobody babysits a websocket.
What does the privacy posture look like?
The resolved client IP is stored in fingerprint.ip - ops-only for abuse investigation, never returned by any public endpoint; the rate-limit lanes still key on HMAC(ip, salt). Header snapshots persist only the allowlist above. Payloads carry a TTL: the janitor archives the row, nulls the body, and the endpoint answers 410 - no permanent content lake.
What did three adversarial audits actually catch?
We ran the intake through external review before shipping, in three rounds:
- Round one - 15 findings across the whole slice.
-
Re-audit - REQUEST CHANGES with 6 more: the header-snapshot blocklist (β allowlist), lane slots kept after rejection (β refunds), survey answers missing JSONB-unsafe checks, leaf-empty answer objects like
{q:[{a:""}]}passing validation, unresolvable IPs sharing one lane bucket, and survey store failures eating the refund. - Final audit - APPROVE WITH NITS; the nits are closed and every fix ships with a regression test. 98 request-inbox tests green.
Try it
# Ask us something - no key, no account
curl -X POST https://agentbadge.xyz/api/requests \
-H "content-type: application/json" \
-H "Idempotency-Key: $(date +%s)-$(hostname)" \
-d '{"kind":"question","payload":{"question":"What does the free scan cover?"}}'
Your bot traffic is not going away. The only question is whether those bots can tell you why they came.
Canonical post: https://agentbadge.xyz/blog/arc-c49-agent-request-inbox
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.




