From 096a0c1b105912815fec3b3f78ce0527fa0d5188 Mon Sep 17 00:00:00 2001
From: jiashuoz <39790535+jiashuoz@users.noreply.github.com>
Date: Sat, 5 Sep 2026 14:39:45 -0700
Subject: [PATCH] feat(blog): add 'Your agent's inbox is storage, not
transport'
Argues inbox polling is a transport problem, not a discipline
problem, and lays out e2a's four inbound delivery channels (signed
webhooks, WebSocket with no public URL, REST polling, MCP) plus
e2a listen for the laptop case.
---
web/src/app/blog/inbox-is-transport/page.mdx | 49 ++++++++++++++++++++
web/src/app/blog/posts.ts | 9 ++++
2 files changed, 58 insertions(+)
create mode 100644 web/src/app/blog/inbox-is-transport/page.mdx
diff --git a/web/src/app/blog/inbox-is-transport/page.mdx b/web/src/app/blog/inbox-is-transport/page.mdx
new file mode 100644
index 000000000..cd9d1c180
--- /dev/null
+++ b/web/src/app/blog/inbox-is-transport/page.mdx
@@ -0,0 +1,49 @@
+import { getPost } from "../posts";
+import { PostSchema } from "../PostSchema";
+
+export const post = getPost("inbox-is-transport");
+
+export const metadata = {
+ title: { absolute: `${post.title} — e2a` },
+ description: post.description,
+ alternates: { canonical: `/blog/${post.slug}` },
+ openGraph: {
+ title: post.title,
+ description: post.description,
+ url: `https://e2a.dev/blog/${post.slug}`,
+ type: "article",
+ publishedTime: new Date(post.date + "T00:00:00Z").toISOString(),
+ },
+ twitter: {
+ card: "summary_large_image",
+ title: post.title,
+ description: post.description,
+ },
+};
+
+
+
+
+ {new Date(post.date + "T00:00:00Z").toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric", timeZone: "UTC" })} · {post.readingMinutes} min read
+
+
+# Your agent's inbox is storage, not transport
+
+A common agent email setup: the agent polls its inbox every 30 minutes. Between polls, radio silence. A customer emails your support agent at 9:02 and gets a reply at 9:30, not because the agent was thinking, but because 9:30 is when the cron fired.
+
+That inbox isn't transport. It's storage with a visiting schedule - the agent checks mail the way you'd check a PO box.
+
+The standard fix is webhooks, and it does fix the latency. But a webhook receiver means a public HTTPS endpoint: a deployed URL, signature verification, retry handling. If your agent is a cloud service, fine. If your agent runs on your laptop, in a homelab, or behind a corporate firewall - which is where a lot of agents actually live - "just use webhooks" means a deployment project before you've received a single email. So people hand-roll the poll and live with the silence. That's not a discipline problem. It's a transport problem.
+
+We built e2a's inbound around the idea that delivery should fit where your agent runs, not where the email API wishes it ran. Four channels, chosen per integration:
+
+- **Signed webhooks** for when you do have a public URL. Every delivery is HMAC-signed (`X-E2A-Signature`, `whsec_…` secret, 5-minute replay window), and the SDKs verify and parse in one call - `construct_event` / `constructEvent` - so you never trust a field on an unverified payload.
+- **WebSocket** for when you don't. A per-agent real-time stream that works from a laptop, no public URL required. If the client disconnects, messages accumulate as unread and the server drains them as notifications on reconnect.
+- **REST polling**, kept on purpose. Sometimes a poll is the right shape - a batch job, an agent that wakes on its own schedule. It should be a choice, not a fallback.
+- **MCP tools** for agent frameworks. Point any MCP-aware runtime at the hosted server and the inbox becomes native tools - `list_messages`, `get_message`, `get_attachment` - over the same REST API. No REST glue to write.
+
+Notifications stay lightweight on every channel - message id, sender, subject - and you fetch the full body and attachments over REST when you actually need them.
+
+For the laptop case there's a shorter path still: `e2a listen` streams inbound mail over WebSocket and bridges it to a local HTTP handler. Point that handler at an OpenAI Responses endpoint and each inbound email becomes a Responses payload whose output goes back out as the reply. An agent that answers email in real time, running on the machine in front of you.
+
+The poll-then-silence pattern isn't a character flaw in your agent. It's what happens when the only push channel on offer demands infrastructure your agent doesn't have. Give the agent a transport that reaches it where it lives, and the PO box schedule goes away on its own.
diff --git a/web/src/app/blog/posts.ts b/web/src/app/blog/posts.ts
index d910fce22..fa7c80af0 100644
--- a/web/src/app/blog/posts.ts
+++ b/web/src/app/blog/posts.ts
@@ -104,6 +104,15 @@ export const posts: Post[] = [
author: "e2a",
readingMinutes: 3,
},
+ {
+ slug: "inbox-is-transport",
+ title: "Your agent's inbox is storage, not transport",
+ description:
+ "An agent that polls its inbox every 30 minutes is checking a PO box. Inbound mail should push to the agent wherever it runs - signed webhooks, WebSocket with no public URL, REST polling, and MCP - without a deployment project first.",
+ date: "2026-09-05",
+ author: "e2a",
+ readingMinutes: 3,
+ },
];
export function getPost(slug: string): Post | undefined {