One way to run QA is to hand an LLM agent a browser and a goal like "sign up as a new user and confirm the account", and it finds its own way through the form. That suits exploratory checks and flows whose markup changes too often for fixed selectors. Email verification can stall these runs for reasons a scripted test never runs into. The agent is slow between steps, it decides for itself when to check for mail, and it reports its own success. This post covers setting up disposable inboxes for agent-driven QA so those three traits do not decide the outcome.

The harness creates the inbox, not the agent

The Lumbox MCP server gives an agent a create_inbox tool, and for open-ended agent work that is the right default. For a QA run, create the inbox in the harness code that launches the agent and hand the address and inbox ID to the agent in its task. The harness then always knows which inbox belongs to which run, cleanup does not depend on the agent remembering to do it, and an agent that decides to make a second or third inbox cannot push the organization past its plan's live inbox limit (3 on Free, 10 on Starter, 50 on Pro, 250 on Scale, after which creation returns 402).

// qa-signup.mjs
const API = "https://api.lumbox.co/v1";
const headers = { "X-API-Key": process.env.LUMBOX_API_KEY, "Content-Type": "application/json" };

export async function qaSignupRun(runAgent) {
  const created = await fetch(`${API}/inboxes`, {
    method: "POST",
    headers,
    body: JSON.stringify({ name: `qa-agent-${Date.now().toString(36)}` }),
  });
  const inbox = await created.json();

  const task = [
    `Open https://staging.example.com/signup and create an account with ${inbox.address}.`,
    `For email tools, use inbox_id ${inbox.id}.`,
    `After submitting the form, call get_otp. If it times out, call list_emails for the same inbox and use the parsed fields of what arrived.`,
    `Finish by reporting the URL of the page you ended on.`,
  ].join("\n");

  const report = await runAgent(task);

  const res = await fetch(`${API}/inboxes/${inbox.id}/emails?limit=20`, { headers });
  const { data } = await res.json();
  const fromApp = data.filter((e) => e.from.address.endsWith("@example.com"));
  const passed = fromApp.length === 1 && fromApp[0].parsed.otp_codes.length === 1;

  if (passed) await fetch(`${API}/inboxes/${inbox.id}`, { method: "DELETE", headers });
  return { passed, report, inboxId: inbox.id, emailsFromApp: fromApp.length };
}

runAgent stands in for however you drive the agent: Claude Code with the MCP server added, a Browser Use script, or your own loop. The harness only needs it to return the agent's final message.

Slow steps and the since window

A scripted test calls the email API a few milliseconds after clicking submit. An agent clicks submit, then spends a model step deciding what to do next, and the email can arrive during that step. Whether the agent then finds it depends on which tool it calls.

  • wait_for_email blocks until a matching message arrives, but it has no since argument, so the server only considers mail that arrives after the call starts. A message that landed while the model was thinking is invisible to it, and the tool runs to its timeout.
  • get_otp looks back five minutes by default, so it catches a code that arrived during the gap. It only returns codes from emails the parser categorized as verification, which depends on keywords like "verify" or "verification code" appearing in the message.
  • list_emails returns what is already in the inbox immediately, with an optional since, and shows parsed fields rather than bodies.

That is why the task above tells the agent to try get_otp first and fall back to list_emails. Written into the prompt, the fallback turns a stalled run into one extra tool call.

Polling costs twice

An agent told only to "check the inbox" tends to call a list tool, find nothing, and call it again. Each call is a model step, paid in tokens and latency, and an API request against a budget of 120 requests per minute per key that every agent on the key shares. A long-poll tool call such as get_otp is a single step and a single request that can wait up to 120 seconds on the server, which is cheaper on both counts.

Email content is untrusted input

When the agent tests your own staging app, the emails come from your own templates. QA agents are also pointed at third-party sites, for example to check a signup flow you integrate with, and there the message body is text written by someone else that ends up in the model's context. The Lumbox MCP tools default to the safe side. wait_for_email and list_emails strip bodies and return parsed fields such as otp_codes, verification_links and category. read_email includes the body only with include_body: true, wrapped between marker lines that label it as untrusted content. check_email_safety returns a phishing score with SPF, DKIM and DMARC results, plus a separate score for text that looks like instructions aimed at an AI agent. For QA runs, parsed fields are nearly always enough, so the task never needs to ask for a body.

Grade the run from the inbox, not the agent's report

An agent's final message saying "account created and verified" is a claim, and the harness can check it. The code above lists the inbox itself and asserts that the app sent exactly one email and that one code was extracted from it. Other checks that fit here: the sender address is the one your app should use, parsed.code_expiry is set if your template says "expires in 10 minutes", and your app's own API shows the account as verified. A duplicate send, a missing code or a wrong sender shows up in these checks even when the agent's report says everything went fine.

When a run fails, the harness above keeps the inbox, so a person can open the same messages the agent saw. Passing runs delete it, which keeps the live inbox count low. A periodic sweep of old qa-agent- inboxes handles the failures once they have been reviewed.

Wiring the tools

For an MCP client such as Claude Code, add the server with your key:

claude mcp add lumbox --env LUMBOX_API_KEY=ak_your_key -- npx -y @lumbox/mcp-server

The server exposes more than 80 tools. A signup QA run needs four of them: get_otp, wait_for_email, list_emails and read_email. Lumbox's own browser tools require a separately configured Steel browser server, so if your agent already drives a browser through Playwright or Browser Use, give it only the email tools. Setup details are in the MCP docs. For frameworks without MCP, wrap the same REST calls as tools; the Browser Use post shows one written that way.

Scripted suites have a different set of problems (parallel workers, CI cleanup, timestamps), covered in per-test inbox isolation in CI. The list of phrasings the code parser recognizes is in how OTP extraction works, which is worth checking against your own templates before an agent depends on get_otp.