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 {