DEV Community

Cover image for How to let AI agents verify email signups
zerodrop
zerodrop

Posted on • Originally published at zerodrop.dev

How to let AI agents verify email signups

An AI coding agent can scaffold a signup flow in about ninety seconds. Route, form, validation, a call to Resend, a token in the database. Then it stops.

It stops because the next step is open the inbox and click the link, and the agent has no inbox. So it does one of three things: mocks the email provider and declares victory, writes a test that asserts sendEmail was called, or asks you to check your Gmail and paste the code back. All three break the loop. The agent can no longer verify its own work.

This is a tooling gap, not a model limitation. The agent can drive a browser and read a database. It just can't receive mail.

The shape of the fix

Give the agent three capabilities and the loop closes:

  1. Create a throwaway inbox address
  2. Wait for a message to arrive at it
  3. Get the OTP or magic link out of that message as a value, not as HTML to parse

That last one matters more than it looks. If you hand an agent a raw email body, it will write a regex. It will write a different regex next time, and one of them will match the unsubscribe link instead of the verification link. Extraction belongs in the infrastructure, where it's deterministic.

Wiring it up

ZeroDrop ships an MCP server that exposes exactly those three tools. Install it into any MCP-capable client:

{
  "mcpServers": {
    "zerodrop": {
      "command": "npx",
      "args": ["-y", "zerodrop-mcp"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

No API key needed for the free tier. If you have one, add it:

{
  "mcpServers": {
    "zerodrop": {
      "command": "npx",
      "args": ["-y", "zerodrop-mcp"],
      "env": {
        "ZERODROP_API_KEY": "your-key"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Restart the client and the agent has three new tools.

For Claude Code, one line does the same thing:

claude mcp add zerodrop -- npx -y zerodrop-mcp
Enter fullscreen mode Exit fullscreen mode

The server is also listed in the official MCP registry as dev.zerodrop/zerodrop-mcp, verified against the domain over DNS.

The three tools

generate_inbox(prefix?) — returns an address immediately. No network round trip, no registration. Pass a prefix if you want the address to be recognisable in logs.

wait_for_email(inbox, timeout_seconds, from_contains, subject_contains, require_otp, require_magic_link) — blocks until a matching message lands, up to 120 seconds. The filters are the useful part: require_otp: true means the agent waits for a message that actually contains a code, not just the first thing that arrives. Marketing footers and double-sends stop being a source of flake.

check_inbox(inbox) — non-blocking read of what's there.

Every returned message carries otp and magic_link as top-level fields, extracted at the edge before the agent ever sees the body.

What the loop looks like

Ask an agent to verify its own signup flow and it now runs something like:

generate_inbox(prefix: "signup-test")
   "signup-test-a91f@zerodrop-sandbox.online"

[drives the browser, submits the form with that address]

wait_for_email(
  inbox: "signup-test-a91f",
  require_otp: true,
  timeout_seconds: 30
)
   { otp: "847291", subject: "Verify your email", ... }

[types 847291 into the verification field]
[asserts the dashboard loaded]
Enter fullscreen mode Exit fullscreen mode

No mock. No mail server in Docker. No human pasting codes. The agent sent a real email through your real provider and read it back.

Why not just mock it?

Because a mock tests that you called a function. It does not test that your Resend API key is valid in this environment, that the template renders, that the token in the link matches the token in the database, or that the link isn't 404ing because of a trailing-slash mismatch in production. Every one of those has shipped broken while a green mock-based suite watched.

The agent-specific version of this problem is worse. An agent that mocks its own verification step has no signal about whether the feature works, so it reports success and moves on. You find out later.

Same infrastructure, human tests too

The MCP server is a wrapper over the same API the SDKs use, so an agent and your CI suite can share the approach:

import { ZeroDrop } from 'zerodrop-client'

const mail = new ZeroDrop()
const inbox = mail.generateInbox()

await page.getByLabel('Email').fill(inbox)
await page.getByRole('button', { name: 'Sign up' }).click()

const email = await mail.waitForLatest(inbox, {
  timeout: 15000,
  filter: { hasOtp: true },
})

await page.getByLabel('Code').fill(email.otp)
Enter fullscreen mode Exit fullscreen mode

SDKs exist for TypeScript, Python, Go, Ruby, PHP, and Java. The agent path and the CI path hit the same edge, so a flow an agent verified locally behaves the same way in your pipeline.

Caveats worth stating

The free sandbox uses a shared domain, which is fine for testing and wrong for anything resembling production data. Messages live for 30 minutes and then the TTL removes them — that's a hard guarantee at the storage layer, not a cleanup job. Inbound mail passes a spam filter at the edge before it's stored, so genuinely spammy sends may not land.

Try it

claude mcp add zerodrop -- npx -y zerodrop-mcp
Enter fullscreen mode Exit fullscreen mode

Then ask the agent to generate an inbox and wait for a message, and send it something from your own mail client. The code comes back extracted.

Then ask your agent to sign up for something and verify itself. If it comes back with a code it read out of a real inbox, the loop is closed.


ZeroDrop is free to use — no signup, no API key, works in CI out of the box.

zerodrop.dev · zerodrop-mcp · SDK · docs

Top comments (2)

Collapse
 
marcusykim profile image
Marcus Kim

The useful boundary here is returning otp and magic_link as structured fields instead of making an agent regex raw email HTML; that removes a surprisingly fragile layer from every signup test. The require_otp filter and the shared 30-minute TTL also show this is testing infrastructure, not a pretend production mailbox, which is the right tradeoff for ephemeral accounts. I'd make inbox ownership and cleanup part of the test contract too, since parallel runs can otherwise consume the wrong message and turn a closed verification loop into a new source of flake.

Collapse
 
zerodrop profile image
zerodrop

Agreed that ownership belongs in the test contract — the way we've drawn it is one inbox per test. generateInbox() is a local call (no network request, no registration), so each parallel worker mints its own address and collisions are impossible by construction rather than by discipline. The filters (from_contains, require_otp) then scope within an inbox for flows that send more than one message. Cleanup we deliberately left to the TTL — inboxes expire in 30 minutes at the storage layer, so there's nothing to tear down and no cleanup step to forget.