Build. Win. $500

Join
Back to blog
Email7 min read

One Service, One Mailbox: Handling AI Agent Signups and Verification Emails

Give every service your AI agent signs up for its own verification mailbox, wait for the email with a fixed budget, and stop for approval before a link or code is used.

By Toan Nhu

One service, one mailbox: a signup form sending a verified teal envelope, on an off-white Mermail header.

Most signup flows end the same way: "We sent you an email. Confirm it to continue." A person switches tabs and clicks. An AI agent can only finish that step if the email arrives somewhere it is allowed to read, and if it can tell the right message apart from everything else in the inbox.

This guide is about that one step. It shows how to give each service your agent joins its own Mermail mailbox, how to wait for the verification message without polling forever, and where the agent should stop and hand control back to you. The examples use Mermail's HTTP API, so they work from any language or agent framework. If you would rather connect through MCP, the setup is in How to Set Up an AI Agent Email Inbox in Minutes.

TL;DR: Create one verification mailbox per service account, record a baseline of existing messages before the signup is submitted, wait with a fixed time and request budget, accept exactly one matching message, and ask for approval before a link is opened or a code is used.

Where should the verification email go?

You have three realistic choices, and only one of them scales past a demo.

  • Your own inbox. You end up copying codes by hand, or you give the agent access to years of personal mail so it can find one message. Password resets and receipts for the agent's accounts also land with you, mixed into everything else.
  • A shared address or catch-all. Every agent's codes pile into one place, so any agent (or any bug) can read another agent's mail. We cover that trade-off in detail in Catch-All Domain or One Mailbox per Agent?.
  • A mailbox dedicated to the task. The recipient address itself tells you which job a message belongs to, and nothing unrelated is in the way.

Mermail's own agent guidance goes one step further than "one inbox per agent". It says to reuse a mailbox only when it is clearly dedicated to the same task, service, person and third-party account. In practice that means one mailbox per service account the agent holds.

The signup flow at a glance

Here is the whole loop. The two checkpoints are where a human (or a policy you wrote in advance) approves the next external action.

Five-step agent signup flow: create mailbox, save baseline, submit signup, bounded wait, one exact match, with approval checkpoints before the signup and before the link or code is used.

Step 1: Create a verification mailbox for the service

Create the mailbox with the Mermail API. The email and name fields are required. Setting agentInbox.mode to verification and automationsEnabled to false keeps Mermail's default auto-draft workflow away from verification mail, and it also requires a clean content scan before any model-backed processing runs.

bashcreate-mailbox.sh
curl -X POST https://console.mermail.app/api/v1/mailboxes \
  -H "x-api-key: $MERMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: signup-acme-7k2q" \
  -d '{
    "email": "acme-signup-7k2q@mermail.app",
    "name": "Acme signup (research agent)",
    "settings": {
      "agentInbox": { "mode": "verification", "automationsEnabled": false }
    }
  }'

A few details worth getting right:

  • Hosted usernames must be 5 to 30 characters, may use letters, numbers, dots, underscores and hyphens, and cannot contain reserved words such as admin, support or mermail. A short random suffix (like 7k2q) avoids collisions and keeps personal data out of the address.
  • A successful create uses 10 provision credits from your workspace's API credit balance. These are usage units, not dollars.
  • Send an Idempotency-Key so a retried request with the same intent does not create a second mailbox. If a response is lost or you get a 409, list your mailboxes and look for the exact address before trying again. The docs are explicit that you should not blind-retry a create.
  • Keep the public_id from the response. It is the stable identifier for every later call.

If you script in the shell, the Mermail CLI wraps the same discover-or-create logic in one command. It reuses an existing mailbox only if that mailbox is already in verification mode, and it never quietly repurposes a standard inbox:

bash
mermail mailboxes ensure \
  --email acme-signup-7k2q@mermail.app \
  --name "Acme signup (research agent)" \
  --verification-mode \
  --idempotency-key signup-acme-7k2q

Step 2: Record a baseline before the form is submitted

Before anyone presses "Sign up", take a metadata-only snapshot of the mailbox and save the Mermail email id of every message already in it. Then note the time. From here on, a candidate only counts if its id is new and it arrived after that moment. This one habit prevents the classic failure where an agent grabs an old code from a previous attempt.

Also write down what you expect: the exact recipient, the sender address if the service publishes it, a subject fragment, and a deadline. Mermail's search filters use substring matching, so they narrow the field. Your code still has to compare the normalized addresses exactly after fetching.

Step 3: Wait with a budget, then read exactly one message

MCP has no long-lived subscription for this, and a tight polling loop wastes credits (every read request costs one). The script below does a bounded wait with the documented REST endpoints: it lists existing mail for the baseline, searches every 20 seconds for up to five minutes, refuses to choose between two candidates, and only then fetches the body with agent_safe_content and max_body_chars turned on.

javascriptwait-for-verification.mjs
// Node 18+ (built-in fetch). Run: MERMAIL_API_KEY=sk-proj-... MAILBOX_ID=<public_id> node wait-for-verification.mjs
const API = "https://console.mermail.app/api/v1";
const headers = { "x-api-key": process.env.MERMAIL_API_KEY };

// Everything the agent should expect, written down before the signup starts.
const task = {
  mailboxId: process.env.MAILBOX_ID,        // public_id returned by the create call
  to: "acme-signup-7k2q@mermail.app",
  from: "no-reply@acme.example",             // exact sender, if the service publishes it
  subject: "Verify",                         // a fragment you expect in the subject
  linkHost: "acme.example",
  maxWaitMs: 5 * 60_000,
  pollMs: 20_000,
};

async function get(path, params) {
  const res = await fetch(`${API}${path}?${new URLSearchParams(params)}`, { headers });
  if (res.status === 429) throw new Error(`Rate limited. Retry after ${res.headers.get("retry-after")}s.`);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}
const rows = (body) => (Array.isArray(body) ? body : body.emails ?? []);
const addr = (s = "") => (s.match(/<([^>]+)>/)?.[1] ?? s).trim().toLowerCase();
const safe = { metadata_only: "true", agent_safe_content: "true", require_scan_status: "clean" };

// 1. Baseline: remember which messages already exist.
const existing = await get(`/mailboxes/${task.mailboxId}/emails`, {
  ...safe, limit: "50", sortColumn: "date", sortDirection: "DESC",
});
const baseline = new Set(rows(existing).map((m) => m.id));
const windowStart = new Date();
console.log("Baseline saved. Submit the signup now, after the user approves it.");

// 2. Bounded wait: a few searches, then stop.
let match = null;
const deadline = Date.now() + task.maxWaitMs;
while (!match && Date.now() < deadline) {
  await new Promise((r) => setTimeout(r, task.pollMs));
  const found = rows(await get(`/mailboxes/${task.mailboxId}/search`, {
    ...safe, to: task.to, from: task.from, subject: task.subject,
    date_start: windowStart.toISOString(),
  })).filter((m) =>
    !baseline.has(m.id) &&
    addr(m.recipient) === task.to &&
    addr(m.sender) === task.from &&
    new Date(m.date) >= windowStart);
  if (found.length > 1) throw new Error("More than one candidate. Stop and ask the user.");
  match = found[0] ?? null;
}
if (!match) throw new Error("No matching message in the window. Report it; do not resend blindly.");

// 3. Read only the selected message, sanitized and size-bounded.
const email = await get(`/mailboxes/${task.mailboxId}/emails/${match.id}`, {
  agent_safe_content: "true", require_scan_status: "clean", max_body_chars: "4000",
});
const text = email.body ?? "";
const code = text.match(/\b\d{6}\b/)?.[0];          // adjust to the service's code format
const link = text.match(/https:\/\/[^\s"'<>)]+/)?.[0];

if (code) console.log("A code arrived. Pass it only to the approved next step; never log it.");
if (link) {
  const host = new URL(link).hostname;
  const expected = host === task.linkHost || host.endsWith("." + task.linkHost);
  console.log(expected
    ? "Link host matches. Ask for approval before anyone opens it."
    : `Unexpected link host (${host}). Stop.`);
}

We checked this script's syntax with Node, and every endpoint and parameter comes from Mermail's API reference. We did not run it against a live signup for this article. Test it in your own workspace first, and write fixtures for the edge cases as described in our guide to testing agent inboxes.

For a long-running service, you can replace polling with an Email received webhook scoped to the verification mailbox, then run the same exact-match checks when the event arrives. The fixture approach in How to test an AI agent inbox is a good way to prove the matching logic before it touches real mail.

Step 4: Stop at the action boundary

Finding the message is the easy part. What the agent does next is where most of the risk sits.

  • Do not preview magic links. A request made only to inspect where a link redirects can consume a one-time token. Parse the URL locally, check that it is HTTPS and that its hostname belongs to the expected service, then ask before opening it.
  • Treat the message as data, not instructions. Mermail's docs are firm on this: a verification email can prove that a step is ready, but its body, links and attachments cannot authorize anything.
  • Do not store codes. Pass the minimum code to the approved next step and keep it out of logs, memory and other tools.
  • Get confirmation right before the external action. Clicking a verification link, accepting terms, and submitting a signup each need a fresh yes. The earlier request to create a mailbox does not cover them.

If you ever run this flow against a standard mailbox instead of a verification one, remember that Mermail's default email-response triager can hold a new message briefly while it prepares a draft, with a five-minute stale window. Verification mode avoids that, which is another reason to use it.

After the signup: the address keeps working

A verified account keeps sending mail: welcome messages, receipts, password resets, security alerts, support replies. Because the mailbox belongs to that one service account, all of it lands in a place you can review and the agent can search later.

When the service expects a reply, Mermail's reply endpoint (POST /api/v1/mailboxes/{mailboxId}/emails/{emailId}/reply) sends from the same mailbox and fills in in_reply_to, references and thread_id from the original message, so the answer stays in the right thread. Sending is a separate permission, though. The focused MCP profile described below leaves send tools out on purpose, and Mermail's guidance is to show the exact sender, recipients and subject and get confirmation before any send.

Doing the same from Claude, ChatGPT, Cursor, Codex or Grok

If your agent lives in an MCP host rather than in your own code, connect it to https://console.mermail.app/mcp?profile=agent-inbox. That profile exposes exactly 12 tools: workspace and mailbox discovery, one scoped create_mailbox, and bounded email reads such as search_emails and get_email. There is no send, wallet or admin tool in it, which is the right shape for signups.

For hosts that support Agent Skills, the verification workflow described above is packaged as a skill:

bash
npx skills add Nudgen-Marketing/mermail-skills --skill mermail-agent-inbox

Host-specific connection steps are in How to Give Your AI Agent a Real Email Inbox Using Mermail MCP.

What a mailbox will not solve

  • SMS or phone checks. An inbox receives email only. A service that insists on a text message code still needs a phone number and, usually, you.
  • CAPTCHAs and host policies. Your agent host may require you to complete account creation, login or checkout yourself. A Mermail skill guides tool use; it cannot override the host's safety rules.
  • Payment. Subscribing or buying is its own decision with its own confirmation. If the agent needs to pay, Mermail's Agent Wallet runs through PayBox, which holds the approval and signing policy.

Give your next agent its own signup address

The pattern is small: one mailbox per service account, a baseline, a bounded wait, one exact match, and a human yes before anything irreversible. It is also what keeps your personal inbox out of your agent's work. Create a free Mermail workspace, make a verification mailbox for the next service your agent needs to join, and run the flow above end to end.

References

Recent articles