← Back

How I Built a Disposable Email Service for AI Agents in Under 2 Hours

February 25, 2026

I needed my AI agents to receive email. Not send it — just receive. Sign up for a service, grab the verification code, move on. Every existing solution was either overkill (full mail server) or not programmatic enough (Mailinator with no API).

So I built myagentinbox. Here's exactly how it works under the hood.

The requirements

The architecture

The entire service runs on four Cloudflare primitives and one external service:

Email arrives ↓ Cloudflare Email Routing (catch-all on @myagentinbox.com) ↓ Cloudflare Worker (email handler) ↓ ↓ ↓ → R2 (attachment storage, 1-day lifecycle) ↓ Upstash Redis (messages + metadata, 24h TTL) ↑ Cloudflare Worker (API + MCP server) ↑ Agent (via REST API or MCP)

No VMs, no containers, no mail servers. The Worker handles both the HTTP API and the incoming email processing.

Why this stack

Cloudflare Email Routing

This was the key insight. Cloudflare Email Routing lets you set a catch-all rule that sends all incoming email for a domain to a Worker. No MX record management, no SMTP servers, no Postfix configuration. You literally toggle it on in the dashboard and point it at your Worker.

The Worker receives a ForwardableEmailMessage with the raw email as a ReadableStream. I parse it with postal-mime, which handles MIME decoding, attachments, and address parsing.

Upstash Redis

I needed TTL. Every inbox, every message, every message list needs to disappear after 24 hours without a cron job or cleanup script. Redis has native key expiry. Upstash Redis works over HTTP, which is the only option in Cloudflare Workers (no TCP connections).

The data model is simple:

inbox:{address}           → JSON (inbox metadata)      TTL: 24h
msg:{id}                  → JSON (full message)         TTL: remaining inbox TTL
inbox:{address}:messages  → Sorted Set (summaries)      TTL: remaining inbox TTL

One subtlety: when a new message arrives, I check the remaining TTL of the inbox key and use that for the message. So if an email arrives at hour 23, the message gets a 1-hour TTL — not a fresh 24 hours. Everything expires together.

Cloudflare R2

Email attachments go into R2. I set a 1-day object lifecycle rule on the bucket, so attachments auto-delete without any cleanup logic. The key structure is {address}/{messageId}/{filename}.

Cloudflare Workers Rate Limiting

Workers have a built-in rate limiting binding. You declare it in wrangler.toml:

[[ratelimits]]
name = "INBOX_LIMITER"
namespace_id = "1001"
simple = { limit = 3, period = 60 }

[[ratelimits]]
name = "READ_LIMITER"
namespace_id = "1002"
simple = { limit = 20, period = 60 }

Then in your Worker, it's one line to check: const { success } = await env.INBOX_LIMITER.limit({ key: clientIp }). No external rate limiting service, no Redis-based rate limiter. It runs at the edge.

The email handler

When an email arrives, the Worker:

  1. Checks the message size (rejects over 10MB)
  2. Looks up the recipient address in Redis — if the inbox doesn't exist, it calls message.setReject() and the sender gets a bounce
  3. Parses the raw email with postal-mime
  4. Uploads any attachments to R2
  5. Stores the full message in Redis with the inbox's remaining TTL

The important detail: if someone sends email to an address that was never created or has expired, the Worker rejects it immediately. No data is stored, no resources are wasted.

MCP support

MCP (Model Context Protocol) lets AI agents discover and use tools. I added an MCP server at /mcp using the @modelcontextprotocol/sdk. It exposes four tools:

The MCP tools call the same Redis/R2 functions as the REST API — no HTTP hop. Each tool has its own rate limiting and annotations (readOnlyHint, destructiveHint, etc.) so clients know what to expect.

An agent using Claude or Cursor can add myagentinbox as an MCP server and immediately start creating inboxes and reading emails as part of its workflow.

Address generation

Each inbox gets a random 10-character alphanumeric prefix: a7k2m9x4bp@myagentinbox.com. That's 36^10 ≈ 3.6 trillion possible addresses — enough that guessing a valid inbox is impractical. No sequential IDs, no UUIDs that leak timing information.

What the Worker exports

A single Cloudflare Worker handles everything:

export default {
  fetch: api.fetch,       // HTTP API + MCP server
  async email(message) {  // Incoming email handler
    await handleEmail(message, env, ctx);
  },
};

The fetch handler is a Hono app serving the REST API, MCP endpoint, landing page, and static pages. The email handler processes incoming mail. Both share the same Redis and R2 bindings.

Total cost

At low-to-moderate traffic:

The entire service runs for about $10/year until it hits meaningful scale.

What I'd do differently

If I were building this for higher scale, I'd add webhook support. Right now agents poll check_inbox every few seconds waiting for an email to arrive. A webhook that fires when an email lands would eliminate that polling entirely — better for agents, cheaper on infrastructure, and opens up a natural paid tier.

I'd also consider edge caching for inbox lookups. Every API call currently hits Redis to verify the inbox exists. A short-lived cache at the edge would cut those commands significantly.

Try it

The service is live at myagentinbox.com. Create an inbox in one click, or connect your agent via MCP. The full API is documented on the landing page.