Why we built programmable email for agents
An autonomous agent needs its own inbox and a way to reply — not a human's Gmail bolted on with IMAP and a refresh token. On SES that's a pipeline you wire and operate; Resend is send-first and not shaped for receiving; AgentMail is purpose-built but defaults to a shared domain and caps inboxes by plan. MailKite (which we build) gives the agent an address on a domain you own: DNS-verify it, then either point a route at a managed agent runner or bring your own agent behind a webhook — and hand it the MCP server or the SDK to send. For developers wiring an agent to email.
Give an agent email and you’ve given it the one protocol every human, system, and vendor already speaks. But “give it email” quietly means two hard things: it needs its own address (not a person’s mailbox behind IMAP and an OAuth token that expires at 3am), and it needs inbound that arrives as something a model can reason about, not raw MIME. Here’s the whole loop, and the two honest ways to run it.
Here’s the bring-your-own-agent version in full — receive, verify, hand the parsed body to your model, reply through the same client. It runs as pasted on Node 18+ (npm install mailkite express):
import express from "express";
import { MailKite } from "mailkite";
const app = express();
const mk = new MailKite(process.env.MAILKITE_API_KEY);
const SECRET = process.env.MAILKITE_WEBHOOK_SECRET;
app.use("/hooks/agent", express.raw({ type: "application/json" }));
app.post("/hooks/agent", async (req, res) => {
// HMAC signature, replay window, constant-time compare — one call
if (!MailKite.verifyWebhook(req.headers["x-mailkite-signature"], req.body, SECRET)) {
return res.sendStatus(401);
}
res.sendStatus(200); // ack fast; run the agent out of band
const event = JSON.parse(req.body);
if (event.type !== "email.received") return;
// The body is untrusted INPUT, never instructions (see the security note below).
const answer = await runAgent({
task: event.text,
from: event.from.address,
trusted: event.auth.spf === "pass" && event.auth.dmarc === "pass",
});
await mk.send({
from: event.to[0].address, // reply from the address it was sent to
to: event.from.address,
subject: `Re: ${event.subject}`,
inReplyTo: event.id, // threads the reply
html: answer.html,
});
});
app.listen(3000);
The message arrives fully decoded, and the inbox lives at agent@yourco.dev — an address on your DNS, not a shared vendor domain. The identical handler shape exists for Python, Ruby, Go, PHP, and Java. The companion repo demo-programmable-email-for-agents runs this end to end with a signed sample event you can fire; open it in StackBlitz. The rest of this post is why the usual tools make you the integrator here, and the two ways MailKite lets you run the loop.
What an agent actually needs from email
Strip it down and it’s four things, and every honest agent-email decision is about which of them a tool hands you versus makes you build:
Why the usual tools make you the integrator
Each of the tools you’d reach for first is good at what it was built for, and each leaves you holding a different piece.
| Tool | What it’s built for | What it leaves you holding for an agent |
|---|---|---|
| Amazon SES | Cheap, high-volume sending | Receiving is a pipeline: receipt rule → S3 → SNS → Lambda → parse the MIME yourself, plus the sandbox and per-region setup |
| Resend | Excellent send-first developer experience | Send-first and not shaped for receiving; same per-domain plan ladder (1 free → 10 → 1,000) |
| AgentMail | A purpose-built agent-inbox primitive | Defaults to a shared agentmail.to address; own-domain inboxes are a paid feature, and inboxes are capped by plan (3 free / 10 at $20/mo / 150 at $200/mo) |
That AgentMail row deserves fairness, because it’s the closest in intent: it’s a focused, well-funded product built for exactly this, and on the raw agent-inbox job it’s good. The honest differences are scope and packaging — the default address is on a domain it owns, putting it on your domain is a paid plan, and you pay by inbox tier rather than by volume. If you want one clean agent-inbox primitive and nothing else, that focus is a feature. The full AgentMail comparison walks it fairly. The through-line across all three: for the specific shape of “an agent with its own inbox on my domain that receives parsed, authenticated mail and can reply,” you end up assembling it.
Two ways to run the loop
This is the part we actually built for agents. Once a domain is DNS-verified, you pick who runs the model turns.
Bring your own agent. The inbound webhook hits your endpoint, your model runs wherever you host it, and you reply with mk.send() — the code at the top of this post. You own the loop, the memory, the tools. MailKite is the inbox and the wire.
Or let a route run it. Create a route whose action is 'agent' with a prompt, and the model turns run for you on a durable Cloudflare Queue, with an agent_runs ledger and a per-route transcript you can drill into — capped and reaped so a slow model call can’t wedge ingest. No endpoint to host, no queue to operate:
// hand the whole loop to a managed route — no server to run
const route = await mk.createRoute({
match: "support@yourco.dev", // or "*@agent.yourco.dev" for a whole subdomain
action: "agent",
agentPrompt: "You are a support agent. Answer from the docs; escalate refunds to a human.",
});
Give the agent the tools: MCP or the SDK
Sending doesn’t have to be code you write, either. There’s a hosted MCP server, so an MCP-speaking agent (Claude, or anything that speaks the protocol) can send mail, read messages, and manage domains as tool calls:
claude mcp add --transport http mailkite https://mcp.mailkite.dev/mcp
Or hand the agent the SDK for its language and let it call mk.send() directly — same one-liner as the reply above. Either way the agent gets email as a first-class capability, not a subprocess shelling out to curl.
A word on security, because it’s load-bearing
The auth block is there for a reason: the moment an agent acts on what an email says, the body is a prompt-injection vector. The verdicts let the loop weight a sender before it acts, but they’re necessary, not sufficient. Treat the body as untrusted data, bound the agent’s authority (what tools it can call, what it can spend, what it can send), and read agent inbox security by design before you point one at anything that matters.
Where I won’t overclaim
MailKite, which we build, is inbound-email-to-webhook infrastructure that also sends; the agent-inbox is one lane of that. The honest pitch is narrow: the agent gets its own address on a domain you own, free and with no per-inbox cap, inbound arrives parsed with an auth verdict, and you can either host the loop or hand it to a managed route — on the same account that already runs your product’s transactional and support mail. If your only requirement is the single cleanest agent-inbox primitive and you don’t mind a shared-domain default, AgentMail is purpose-built for that and good at it. If you’re already on SES for sending and happy operating a receive pipeline, keep it — our webhook is plain HTTPS, so the receiver can be the Lambda you already have.
FAQ
How does an AI agent get its own email address on MailKite?
DNS-verify a domain you own (MX to receive, SPF and DKIM to send), then any address on it — agent@yourco.dev — is a working inbox. There’s no per-inbox cap and no sandbox or approval wait, and domains and mailboxes are unlimited on every plan, including Free.
Do I have to host the agent myself?
No — that’s the choice. Bring your own agent behind a signed webhook and run the model wherever you like, or create a route with action: 'agent' and MailKite runs the model turns on a durable queue with a per-run transcript. Same inbox, same reply path; you pick who runs the loop.
Can the agent send email through MCP instead of code?
Yes. There’s a hosted MCP server at mcp.mailkite.dev (claude mcp add --transport http mailkite https://mcp.mailkite.dev/mcp), so a Claude or MCP-speaking agent can send mail, read messages, and manage domains as tool calls. Or give the agent the SDK and it calls mk.send() directly.
How is this priced for agents versus AgentMail? MailKite meters email volume, not inboxes: the free tier is unlimited inboxes and unlimited domains with 3,000 messages a month across inbound and outbound. AgentMail prices by plan tier and inbox count (3 inboxes free, 10 at $20/mo, 150 at $200/mo), with own-domain inboxes on paid plans. If you’re spinning up many inboxes — one per customer, task, or thread — the volume model is usually the cheaper axis.
Is it safe to let an agent act on inbound email? Only with guardrails. Every payload includes SPF/DKIM/DMARC verdicts so the agent can weight a sender, but the body is still untrusted input — treat it as data, not instructions, and bound what the agent is allowed to do. See agent inbox security by design.
If wiring email to an agent has meant an SES receive pipeline, a shared-domain inbox, or a per-inbox bill, there’s a simpler shape: an address on your own domain, parsed and authenticated inbound, and your choice of who runs the loop. Clone demo-programmable-email-for-agents (or run it in your browser), then point a domain at MailKite and give your agent an inbox.
Related: email as an agent tool — MCP, the SDK, or your own webhook, the AgentMail alternative for AI agents, and agent inbox security by design.