Catch-All Domain or One Mailbox per Agent? An Architecture Guide for Agent Fleets
A catch-all looks free until agents reply, sign up and act on mail. Here is what it really costs, when it still fits, and how to move to one mailbox per agent in stages.
By Toan Nhu

Almost every team that adds email to an agent platform considers the same shortcut. Point a domain's mail at one catch-all mailbox, invent a local part for each agent (agent-17@agents.example.com), store that string in a database, and let a worker sort incoming mail by the address it was sent to. No provisioning calls, no per-address setup, unlimited addresses.
It works on day one. It gets expensive once agents start replying, signing up for services and acting on what they read. This post explains what a catch-all really commits you to, the specific ways it fails for agents, when it is still a fine choice, and how to move to one real mailbox per agent with Mermail without a big-bang migration.
TL;DR: A catch-all turns your application into a hand-built mail router with one shared blast radius. Keep it for human-reviewed intake that never replies. For agents that read, reply or sign up, give each one its own mailbox, route events by mailbox id, and retire the catch-all gradually.
What a catch-all actually does
During an SMTP conversation, the sending server names each recipient before it transfers the message. A receiving server that knows an address does not exist can refuse it on the spot, typically with a 550 reply, and the sender gets a bounce. A catch-all changes that rule: mail for any unknown address at the domain is accepted and delivered to one designated mailbox. Google Workspace, for example, implements this as a routing rule that rewrites the recipient for unrecognized accounts.
So the provider gives you exactly one mailbox. Every "address" beyond that is a convention inside your code.
Two architectures, side by side
On the left, one mailbox receives everything and your code decides who owns each message. On the right, each agent has a mailbox the platform knows about, and events arrive already labeled with their owner.
The hidden job: you become the mail router
With a catch-all, the local part is a routing key, and everything a mailbox normally does for an identity moves into your application:
- Parsing recipients reliably (To, Cc, Delivered-To, forwarded copies) and mapping them to an agent.
- Deciding what happens to mail for addresses you never issued.
- Deduplicating, threading and storing messages per agent.
- Enforcing which agent, tool or teammate may read which messages.
- Sending replies that come from the address the other side wrote to, with the right thread headers.
- Figuring out which agent caused a bounce or a spam complaint.
None of that is impossible. It is simply a mail system you now own, test and keep secure.
Five ways the shortcut breaks for agents
1. Every guess gets delivered
Because nothing is rejected, spammers probing random or dictionary addresses at your domain get their mail accepted. Your catch-all fills with junk, and your router has to decide, for every message, whether it belongs to a real agent. Address verification tools also cannot distinguish real from invented recipients on a domain that accepts everything, which can make legitimate addresses at that domain look risky to other senders.
2. Auto-replies turn into backscatter
If an agent answers mail that arrives at an invented address, it may be replying to a forged sender who never wrote to you. RFC 3834, the guidance for automatic responses, tells auto-responders to be conservative about whom they answer for exactly this reason. With one mailbox per agent, the agent only ever handles mail sent to an address it was actually given.
3. One parser, one model, everyone's mail
This is the agent-specific problem. In a catch-all, the code and the model that triage mail see messages for every agent. A prompt-injection attempt aimed at one agent, or a routing bug, can put another agent's password reset in the wrong context. Mermail treats all inbound content as untrusted data and scopes tools to a workspace and mailbox. A mailbox boundary enforced by the platform is much easier to reason about than a WHERE local_part = ? clause you hope every code path uses.
4. Replies and threads are on you
A service that emails your agent expects the answer to come from the same address and to carry In-Reply-To and References headers so it threads correctly (RFC 5322 defines these fields). With a catch-all you build that yourself. Mermail's reply endpoint sends from the mailbox that received the message and fills in in_reply_to, references and thread_id from the original.
5. Reputation problems have no owner
Be careful with the usual claim here. Separate mailboxes on the same domain still share that domain's reputation with Gmail, Yahoo and Microsoft, whose sender guidelines judge authentication, complaint rates and unwanted mail at the domain level. What separate mailboxes give you is attribution. When a bounce or complaint arrives, you know which mailbox, and therefore which agent, sent the message, and you can pause that one agent instead of guessing. If you need true reputation isolation between customers, use separate domains or subdomains; our post on custom domains for thousands of agent inboxes covers how workspaces and domains fit together.
When a catch-all is still reasonable
A catch-all is not wrong everywhere. It is a reasonable tool when all of these are true:
- A person, not an agent, reviews what arrives.
- Nothing replies from the invented addresses.
- No automation takes action based on message content.
- The addresses are short-lived, such as a test run you will shut down.
Typical examples are catching typos in your company domain, or a temporary intake address for a one-off experiment. The moment an agent reads the content and acts on it, the calculation changes.
A quick decision table
| Question | Catch-all + database row | One mailbox per agent |
|---|---|---|
Does the agent reply as itself? | You build sender identity and threading | Reply comes from the agent's own mailbox |
What happens to mail for unknown addresses? | Accepted and delivered to you | Only addresses you created exist |
Who can read an agent's mail? | Anything with access to the shared mailbox | Scoped to that mailbox and workspace |
Which agent caused a bounce or complaint? | Reconstruct it from your own tables | The event names the mailbox |
Cost to add an agent | A database insert | One create call (10 provision credits in Mermail) |
Moving from a catch-all to a mailbox per agent with Mermail
You do not have to switch everything at once. The steps below let the old catch-all keep running while agents move over.
Step 1: Choose where the new mailboxes live
Start on hosted @mermail.app addresses, or connect a dedicated subdomain such as agents.example.com. Mermail does not accept a domain that already has MX records, so your existing catch-all domain and company mail are not touched. Two things to plan for: each workspace supports one custom domain, and once that domain is ready, the workspace's mailboxes move to it and keep their usernames. Custom domains require a paid plan; check the pricing page for current limits.
Step 2: Provision a mailbox for each agent
Loop over your agents table and create one mailbox per row. Deriving the Idempotency-Key from the agent id makes the script safe to re-run, and on a conflict or an uncertain response it lists mailboxes instead of creating again, which is what Mermail's docs recommend. Creating a mailbox requires workspace admin rights, and how many mailboxes you can hold depends on your plan.
// Node 18+. Creates one Mermail mailbox per agent row, safely re-runnable.
// Run: MERMAIL_API_KEY=sk-proj-... node provision-mailboxes.mjs
const API = "https://console.mermail.app/api/v1";
const headers = {
"x-api-key": process.env.MERMAIL_API_KEY,
"Content-Type": "application/json",
};
// Replace with a query against your own agents table.
const agents = [
{ id: "agt_research_01", handle: "research-7k2q", label: "Research agent" },
{ id: "agt_billing_02", handle: "billing-m4xd", label: "Billing agent" },
];
const domain = "mermail.app"; // or your verified custom domain, e.g. agents.example.com
async function listMailboxes() {
const res = await fetch(`${API}/mailboxes`, { headers });
if (!res.ok) throw new Error(`List failed: HTTP ${res.status}`);
const body = await res.json();
return Array.isArray(body) ? body : []; // the API returns an array of mailboxes
}
for (const agent of agents) {
const email = `${agent.handle}@${domain}`;
const res = await fetch(`${API}/mailboxes`, {
method: "POST",
headers: { ...headers, "Idempotency-Key": `mailbox-${agent.id}` },
body: JSON.stringify({ email, name: agent.label }),
});
if (res.status === 201) {
const mailbox = await res.json();
console.log(agent.id, "->", mailbox.email, mailbox.public_id); // store public_id on the agent row
continue;
}
if (res.status === 409 || res.status >= 500) {
// Uncertain or conflicting result: look before trying anything else.
const existing = (await listMailboxes()).find((m) => m.email?.toLowerCase() === email);
console.log(agent.id, existing ? `exists: ${existing.public_id}` : "not found, check manually");
continue;
}
throw new Error(`${agent.id}: HTTP ${res.status}. Stop and check credits, plan or role.`);
}This script and the router in Step 3 use only endpoints and fields from Mermail's API reference. We ran the router locally against sample events (first delivery, duplicate, unknown mailbox, bounce, wrong workspace). We did not run the provisioning script against a live workspace for this article, so try it with one agent first.
If you want these agents to receive signup and verification mail, create them in verification mode instead. One Service, One Mailbox walks through that flow step by step.
Step 3: Route events by mailbox, not by the To header
Create an Email received webhook (plus Email bounced and Spam complaint if the agents send) for the selected mailboxes, or for all workspace inboxes including future ones. Each payload carries inbox_id, the mailbox's public id, and a stable event_id. Your router becomes a lookup table instead of a string parser:
// Route Mermail webhook events by mailbox, not by parsing the To header.
// Verify each delivery as described in Mermail's webhook docs before calling this.
const agentByMailbox = new Map([
["2c5253b9-b0e8-4a7b-9e7c-15d3ad22d89d", "agt_research_01"], // public_id -> agent id
]);
const seen = new Set(); // use a durable store with a unique key in production
export function routeEvent(event, expectedWorkspaceId) {
if (event.workspace_id !== expectedWorkspaceId) return { action: "reject" };
if (seen.has(event.event_id)) return { action: "duplicate" }; // retries reuse event_id
seen.add(event.event_id);
const agentId = agentByMailbox.get(event.inbox_id);
if (!agentId) return { action: "ignore" }; // unknown mailbox: nobody's mail leaks to another agent
switch (event.event_type) {
case "message.received":
return { action: "enqueue", agentId, messageId: event.data.message_id };
case "message.bounced":
case "message.complained":
return { action: "pause-sending", agentId }; // you know exactly which agent caused it
default:
return { action: "record", agentId };
}
}Events can arrive more than once or out of order, so keep the event_id deduplication in durable storage, acknowledge quickly with a 2xx, and do the slow work asynchronously. Verify every delivery as described in the webhook payload docs before you trust it.
Step 4: Retire the catch-all in stages
- Point new agents only at their Mermail mailboxes.
- Update the address on each third-party account an existing agent holds, one service at a time.
- Keep the catch-all human-reviewed during the transition, and turn off any automation that still reads it.
- When nothing legitimate has arrived there for a while, narrow or remove the catch-all rule so unknown addresses bounce again.
Give every agent a mailbox of its own
A catch-all saves you a provisioning call and costs you a mail router, a shared blast radius and a lot of guesswork when something goes wrong. A mailbox per agent gives each one an address that exists on purpose, a boundary your platform enforces, and events that name their owner. Start free on Mermail, create mailboxes for your first two agents, and point a webhook at the router above.


