New Project
A Vercel template for spinning up named AI assistants that each have their own email address. They coordinate over email with real people, read their own inbox, and execute end-to-end - Friday plans a dinner with three friends and books the restaurant; Pepper sources quotes from vendors and confirms the booking; Happy triages your inbound mail and only escalates what's worth your attention.
Powered by AgentMail, the AI SDK, and Exa. Provider-agnostic - works with Anthropic or OpenAI, your pick.
pnpm installcp .env.example .env.local # then fill in the keyspnpm dev
Open http://localhost:3000. Click an example scenario. Watch the assistant call tools, send real email, and stream replies back as participants respond - all visible in the inbox panel in real time.
The template ships with three Tony-Stark-flavored assistants:
| Assistant | Specialty | Direction | Tool surface |
|---|---|---|---|
| Friday | Multi-party scheduling & coordination | Outbound | send / reply / read / search / escalate |
| Pepper | Vendors & operations | Outbound | send / reply / read / search / escalate |
| Happy | Inbound triage & security | Inbound | + block / allow / list filters |
reading inbox…, sending email…) as the assistant works through a multi-step plan.id is passed as AgentMail's clientId.block_sender, allow_sender, list_filters as the gatekeeper.On … wrote: is hidden behind a ··· pill).lib/assistants-seed.ts as code. AgentMail is the persistence layer for everything email-shaped.┌────────────────────────────────────────────────────────────┐│ Browser (Vercel) ││ ││ ┌─────────┐ ┌─────────┐ ┌─────────┐ ││ │ Friday │ │ Pepper │ │ Happy │ ← all 3 mounted, ││ │ panel │ │ panel │ │ panel │ CSS toggles ││ └─────────┘ └─────────┘ └─────────┘ which is shown ││ │ ▲ │ ▲ │ ▲ ││ │ │ EventSource(SSE) per inbox (auto-reconnect) ││ │ │ + useChat (Vercel AI SDK) │└───────┼──┼──────────┼─┼──────────┼─┼──────────────────────┘▼ │ ▼ │ ▼ │┌────────────────────────────────────────────────────────────┐│ Next.js route handlers (Vercel functions) ││ ││ /api/chat ← streamText + tools, UI Message Stream ││ /api/threads ← client.inboxes.threads.list ││ /api/thread/X ← client.threads.get ││ /api/events ← SSE relay around ││ client.websockets.connect │└────────────────────────────────────────────────────────────┘│▼ AgentMail WS + REST┌────────────────────────────────────────────────────────────┐│ AgentMail ││ inboxes, threads, messages, lists, real-time events │└────────────────────────────────────────────────────────────┘
pnpm lockfile, but npm / yarn / bun work fine - just regenerate the lock).agentmail.to subdomain, where common usernames like friday are already globally claimed (the template's suffix-retry logic handles that, but you'll get friday-3oac@agentmail.to instead of friday@yourdomain.com).AI_PROVIDER env.git clone https://github.com/agentmail-to/agentmail-template-starkhqcd agentmail-template-starkhqpnpm installcp .env.example .env.local# fill in AGENTMAIL_API_KEY, AGENTMAIL_DOMAIN, your AI provider key# (ANTHROPIC_API_KEY or OPENAI_API_KEY), EXA_API_KEY, and PRINCIPAL_EMAILpnpm dev
Open http://localhost:3000.
No database to provision. Just paste the five environment variables when prompted.
| Variable | Required | What it's for |
|---|---|---|
AGENTMAIL_API_KEY | yes | Sending/receiving email, opening the WS, managing lists |
AGENTMAIL_DOMAIN | recommended | A domain you've verified in your AgentMail dashboard |
AI_PROVIDER | optional | anthropic (default) or openai |
ANTHROPIC_API_KEY | one of | Required when AI_PROVIDER=anthropic (the default) |
OPENAI_API_KEY | one of | Required when AI_PROVIDER=openai |
AI_MODEL_ID | optional | Override the model id. Defaults: claude-sonnet-4-6 / gpt-4o |
EXA_API_KEY | yes | Web search tool for venue / vendor research |
PRINCIPAL_EMAIL | yes | Your email; assistants use it for forward_to_principal escalations |
AGENTMAIL_USERNAME_<ID> | optional | Override an assistant's local-part (e.g. AGENTMAIL_USERNAME_FRIDAY=tony-friday) |
NEXT_PUBLIC_APP_URL | optional | Informational; your deployed URL |
Missing keys surface as clear errors in the UI (Missing required env var: …) rather than silently no-op'ing. There is no "demo mode" - the template assumes you've configured what it needs.
| Layer | What |
|---|---|
| Framework | Next.js 16 (App Router, Turbopack) |
| Language | TypeScript 5 |
| UI | React 19 + Tailwind CSS v4 |
| Markdown | react-markdown + remark-gfm (for tables) |
| Email primitive | AgentMail SDK (agentmail 0.5.x) |
| AI primitive | Vercel AI SDK v6 + @ai-sdk/anthropic, @ai-sdk/openai, @ai-sdk/react |
| Model | Provider-agnostic. Default: Anthropic Claude (claude-sonnet-4-6). Set AI_PROVIDER=openai for GPT (gpt-4o). See lib/model.ts. |
| Web search | Exa via exa-js |
| Schemas | Zod 4 for tool input validation |
| Persistence | None. Inbox state lives in AgentMail; assistants live in code. |
Realtime on Vercel serverless has two big constraints:
The standard answer is a server-side WS → browser-side SSE relay, with the realtime service (AgentMail) holding the durable connection. That's what this template does:
AgentMail WS ◄── server-side ──► SSE stream ◄── EventSource (auto-reconnect)(durable) Next.js route in browser(one per assistant)
| Practice | Where | What we do |
|---|---|---|
| API key never reaches the browser | app/api/events/route.ts | The browser opens an EventSource to /api/events?assistantId=…. The server opens the AgentMail WS using AGENTMAIL_API_KEY and pipes events down. |
| Connection scoped to one assistant | lib/assistants.ts → ensureProvisioned | Each SSE call resolves the assistant, gets its inboxId, and calls socket.sendSubscribe({ inboxIds: [inboxId], eventTypes: [...] }) so only relevant events flow. |
| Heartbeat to defeat proxy timeouts | app/api/events/route.ts | A : keepalive\n\n comment frame is sent every 15s. Vercel and most edge proxies will close idle TCP connections; the heartbeat keeps it alive. |
| Clean shutdown on disconnect | request.signal.addEventListener("abort", …) | When the browser closes the tab or navigates away, request.signal aborts. We close the AgentMail WS and the SSE controller to avoid orphan sockets. |
| Auto-reconnect | Browser native (EventSource spec) | EventSource reconnects automatically when the server closes the stream. The Vercel function timeout (typically 5-15 min on Pro / Fluid) just causes a brief blip; the browser reopens transparently. |
| No state lost across reconnects | client.inboxes.threads.list on first mount | The initial state comes from a REST call to /api/threads so the inbox is fully populated before the live stream starts. Reconnects don't replay history - they don't need to. |
| All assistant streams kept alive | app/page.tsx | All three Inbox components are mounted simultaneously (CSS hidden for the inactive ones). Streams keep flowing in the background - when you switch tabs, the new view is already live. |
| Heads-up status on the UI | components/inbox.tsx | A small status dot shows Live / Reconnecting / Connecting / Disconnected so the user always knows whether they're seeing fresh state. |
The AgentMail WS speaks JSON. After connect, the server sends:
{ "type": "subscribe","inbox_ids": ["friday-3oac@agentmail.to"],"event_types": ["message.received", "message.sent", "message.delivered"] }
Each event arrives as:
{ "type": "event","eventType": "message.received","eventId": "...","message": { ... },"thread": { ... } }
We forward each event as a single SSE frame (data: <json>\n\n) to the browser, which deserializes and upserts the thread into the inbox list.
Two reasons. (1) Vercel route handlers can't accept WS upgrades - they're request/response only. (2) The user-facing UX is identical: SSE gives you server-push, EventSource auto-reconnects, no manual handling needed. The transport difference is invisible. If you ever need full duplex from the browser (rare for an inbox UI), you'd run the WS layer outside Vercel (Cloudflare Workers, Fly, Railway, etc.).
MIT - go build cool things.
Spin up named AI assistants each with their own email inbox. Live inbox UI, and streaming chat with tool calls.