We own sitzio.de, we already use Cloudflare, and Cloudflare Email Routing is free. Point MX at Cloudflare, wire up a Worker, and we have the same thing Mailosaur sells — disposable addresses on a real domain, JSON API for the agent — for $0/month, forever, with no trial expiring.
Mailosaur charges $9/mo because they built exactly this on top of someone else's cloud. We have the cloud. Comparison on the axes that matter for our workflow:
| Mailosaur | CF Email Routing + Worker | |
|---|---|---|
| Cost | $108/yr minimum, annual commit | $0 within free tier forever |
| Domain | <id>.mailosaur.net — obviously a test service |
test@mail.sitzio.de — looks like production mail |
| Setup effort | 2 min — sign up, grab API key | ~45 min — MX, Worker, KV binding, secret |
| Link extraction | Built-in: message.html.links[] |
We parse it in the Worker with PostalMime + regex |
| Ownership | Vendor lock-in, data on their servers | We own it inbox stays in our CF account |
| Works offline | No — external service | No — needs CF, but no trial, no auth expiry |
| Daily limit | 50 tests/day (Starter) | 100k Worker calls/day — effectively unlimited |
Auth provider (Supabase/Clerk/whatever sitzio.de uses) sends mail via its normal SMTP. The
mail arrives at Cloudflare's MX because we pointed mail.sitzio.de there. CF
routes it to our Worker. The Worker parses and stores it in KV. The agent reads via a second
Worker endpoint authenticated with a bearer token.
graph LR A["AI Agent
(CLI)"] -->|"1. signup
to=run-42@mail.sitzio.de"| B["sitzio.de
(web app)"] B -->|"2. triggers"| C["Auth Provider
Supabase / Clerk"] C -->|"3. SMTP send
magic link"| D[("Cloudflare MX
mail.sitzio.de")] D -->|"4. route to"| E["Email Worker
email() handler"] E -->|"5. parse + store"| F[("KV Namespace
INBOX")] A -->|"6. GET /inbox/:addr
Bearer token"| G{{"HTTP Worker
fetch() handler"}} G -->|reads| F G -.->|"7. JSON + links"| A A -->|"8. follow link"| B B -->|"9. user verified"| H(["✓ Auth flow passed"]) classDef agent fill:#1a1a22,stroke:#f38020,color:#ece8e1,stroke-width:2px classDef site fill:#15151a,stroke:#7fc1c9,color:#ece8e1,stroke-width:1.5px classDef cf fill:#2a1a0a,stroke:#f38020,color:#fce8d4,stroke-width:2px classDef worker fill:#1e1a12,stroke:#f38020,color:#fce8d4,stroke-width:2px classDef store fill:#15151a,stroke:#8f8c85,color:#ece8e1,stroke-width:1.5px classDef success fill:#1a2212,stroke:#92d36e,color:#e2f3d4,stroke-width:2px class A agent class B,C site class D cf class E,G worker class F store class H success
Assumes sitzio.de is already on Cloudflare. Everything uses wrangler CLI (already installed on this machine) and the CF dashboard.
mail.sitzio.de so test traffic is isolated.
mail.sitzio.de.
Cloudflare auto-adds the three MX records + SPF TXT record. Wait ~2 min for DNS.
email() handler parses and stores; the fetch() handler
serves the agent's read API with bearer-token auth.
inbox-worker. Done. Every email to
*@mail.sitzio.de now hits our Worker.
This is the entire src/index.ts. Two handlers on one Worker: email
for inbound mail, fetch for the agent's read API.
src/index.ts import PostalMime from "postal-mime"; export interface Env { INBOX: KVNamespace; API_TOKEN: string; } export default { // ─── Inbound mail handler ─────────────────────────────── async email(message: ForwardableEmailMessage, env: Env) { const parsed = await PostalMime.parse(message.raw); const record = { id: crypto.randomUUID(), to: message.to, from: message.from, subject: parsed.subject ?? "", text: parsed.text ?? "", html: parsed.html ?? "", links: [...(parsed.html ?? "").matchAll(/href="(https?:[^"]+)"/g)].map(m => m[1]), codes: [...(parsed.text ?? "").matchAll(/\b(\d{4,8})\b/g)].map(m => m[1]), receivedAt: new Date().toISOString(), }; await env.INBOX.put( `msg:${message.to}:${record.id}`, JSON.stringify(record), { expirationTtl: 3600 } // auto-expire after 1h ); }, // ─── Read API for the agent ───────────────────────────── async fetch(req: Request, env: Env): Promise<Response> { if (req.headers.get("authorization") !== `Bearer ${env.API_TOKEN}`) return new Response("unauthorized", { status: 401 }); const url = new URL(req.url); const match = url.pathname.match(/^\/inbox\/([^\/]+)$/); if (!match) return new Response("not found", { status: 404 }); const addr = decodeURIComponent(match[1]); if (req.method === "DELETE") { const list = await env.INBOX.list({ prefix: `msg:${addr}:` }); await Promise.all(list.keys.map(k => env.INBOX.delete(k.name))); return new Response("cleared"); } // GET /inbox/:addr?await=10 — poll up to N seconds for any message const deadline = Date.now() + parseInt(url.searchParams.get("await") ?? "0") * 1000; do { const list = await env.INBOX.list({ prefix: `msg:${addr}:` }); if (list.keys.length) { const msgs = await Promise.all(list.keys.map(k => env.INBOX.get(k.name, "json"))); return Response.json({ items: msgs }); } await new Promise(r => setTimeout(r, 1000)); } while (Date.now() < deadline); return Response.json({ items: [] }); }, };
Functionally identical to the Mailosaur loop — mint, signup, trigger, poll, extract, assert. Only the URL changes.
run-$(uuid)@mail.sitzio.de/inbox/<addr>?await=10items[0].links[0] from JSON response.
Base URL: https://inbox-worker.<your-subdomain>.workers.dev.
Every request carries Authorization: Bearer $API_TOKEN.
| Method | Path | Behavior |
|---|---|---|
| GET | /inbox/:address |
List all messages for an address. Returns { items: [...] }. |
| GET | /inbox/:address?await=10 |
Block up to N seconds until at least one message arrives. Polls every 1s. |
| DELETE | /inbox/:address |
Clear all messages for that address. Call between test runs. |
Once the Worker is deployed and these env vars are set, the agent has everything it needs.
CLAUDE.md ## Auth Testing — Cloudflare Email Routing # Inbox service (our own Worker) INBOX_API="https://inbox-worker.<subdomain>.workers.dev" INBOX_TOKEN="$INBOX_API_TOKEN" # in .env, never in git INBOX_DOMAIN="mail.sitzio.de" ### Protocol for any auth-flow change: 1. Mint a unique address: EMAIL="run-$(date +%s)-$(openssl rand -hex 3)@$INBOX_DOMAIN" 2. Clear any stale messages first: curl -X DELETE -H "Authorization: Bearer $INBOX_TOKEN" \ "$INBOX_API/inbox/$EMAIL" 3. Submit the signup / login form with $EMAIL. 4. Await the verification mail (blocks up to 10s): curl -H "Authorization: Bearer $INBOX_TOKEN" \ "$INBOX_API/inbox/$EMAIL?await=10" | jq '.items[0]' 5. Extract the magic link: LINK=$(... | jq -r '.items[0].links[0]') 6. Follow the link, assert the authenticated session exists. RULES: - ALWAYS mint a fresh address per test run. - If the await returns empty items after 10s, the provider's SMTP is broken — report, don't retry blindly. - SMTP 250 on the send side is NOT a pass. Always verify here.