El muro con el que se topa todo agente
Registrarse para una prueba, crear un espacio de trabajo, reclamar una clave de API, unirse a una beta: todos estos casos terminan en un formulario, y todo formulario termina en un buzón. Un humano le echa un vistazo al teléfono. Un agente no tiene nada a lo que echarle un vistazo — no tiene ninguna dirección que pueda leer, y los sensatos se detienen y te piden el código, lo cual anula el sentido mismo de haber mandado un agente.
La solución no es un prompt más ingenioso. Es un buzón que el agente pueda leer mediante programación, sin ninguna cuenta que crear antes (un agente que crea una cuenta de correo se topa con el mismo muro un nivel más abajo) y sin ninguna clave que gestionar. Un buzón desechable es exactamente eso: un buzón existe en el momento en que le llega correo, y leerlo es una sola solicitud HTTP.
Dos formas de darle un buzón
Se puede llegar al mismo buzón de dos formas, y la elección depende de cómo esté construido el agente, no del buzón:
| Criterio | Servidor MCP | Función de herramienta REST |
|---|---|---|
| Encaja cuando | El agente se ejecuta en un cliente MCP — Claude Code, Cursor, Claude Desktop, o un framework con un adaptador MCP. | Escribes el agente en código: LangChain, el OpenAI Agents SDK, el Vercel AI SDK, o tu propio bucle. |
| Espera | wait_for_message bloquea del lado del servidor hasta 25 segundos y devuelve el mensaje entero. Cero tokens gastados mientras espera. | La función de herramienta hace un bucle de una vez por segundo hasta su plazo. Cero tokens también — el bucle está en tu código, no en el modelo. |
| Configuración | Una URL en la configuración del cliente. Sin código. | Dos funciones, cuarenta líneas, una biblioteca HTTP. |
| Qué ve el modelo | Seis herramientas con descripciones, más un párrafo de instrucciones que envía el servidor en el momento de conectar. | Lo que digan tus propias descripciones de herramienta. Los docstrings de más abajo están escritos para serlo. |
La vía MCP es una línea, y la configuración por cliente tiene la línea exacta para siete clientes; un buzón que un agente de IA puede leer explica las herramientas en profundidad. El resto de esta guía es la vía REST, para agentes que construyes tú mismo.
{"mcpServers":{"grabmail":{"url":"https://grabmail.io/mcp"}}}Una función de herramienta: Python, para LangChain y el OpenAI Agents SDK
Dos funciones sencillas con docstrings cuidados. Los docstrings importan más que el código: en los dos frameworks se convierten en la descripción que lee el modelo para decidir cuándo llamar a la herramienta y qué hacer con la respuesta, así que dicen las dos cosas que un agente hace mal — alias frente a dirección, y qué significa timed_out.
"""inbox_tools.py — two plain functions any agent framework can wrap. No key, no account."""
import secrets
import time
import requests
API = "https://grabmail.io/api/v1"
def create_inbox() -> dict:
"""Create a fresh disposable email inbox for this task.
Returns the ADDRESS to poll and the ALIAS to give to websites. Put the alias
into forms; never hand out the address. Nothing is created server-side.
"""
address = f"agent-{secrets.token_hex(4)}@grabmail.io"
r = requests.get(f"{API}/mailbox", params={"address": address}, timeout=15)
r.raise_for_status()
return {
"address": address,
"alias": r.json().get("alias"),
"next_step": "Put the alias into the form. Then call wait_for_message with the address.",
}
def wait_for_message(address: str, subject_contains: str = "", timeout_seconds: int = 60) -> dict:
"""Wait for an email to arrive at the address, up to timeout_seconds.
Returns the message (from, subject, text, html) or {"status": "timed_out"}.
On timed_out, call again — up to three times — before concluding no mail was sent.
"""
deadline = time.monotonic() + timeout_seconds
while time.monotonic() < deadline:
r = requests.get(f"{API}/mailbox", params={"address": address}, timeout=15)
if r.status_code == 429: # slow down, do not fail
time.sleep(float(r.headers.get("Retry-After", 1)))
continue
r.raise_for_status()
for m in r.json()["messages"]:
if subject_contains.lower() in m["subject"].lower():
full = requests.get(f"{API}/message/{m['id']}", params={"mailbox": address}, timeout=15)
full.raise_for_status()
return full.json()
time.sleep(1) # one read a second, never throttled
return {"status": "timed_out", "hint": "Call again, up to three times, before giving up."}Envolverlas es una llamada por framework. El tool de LangChain lee el docstring y las anotaciones de tipos; el function_tool del Agents SDK hace lo mismo y añade las herramientas a un agente cuyas instrucciones repiten el bucle:
# LangChain: the docstring becomes the tool description the model reads.
from langchain_core.tools import tool
create_inbox_tool = tool(create_inbox)
wait_for_message_tool = tool(wait_for_message)
# agent = create_react_agent(model, tools=[create_inbox_tool, wait_for_message_tool, ...])# OpenAI Agents SDK: same two functions, same docstrings.
from agents import Agent, Runner, function_tool
signup_agent = Agent(
name="Signup agent",
instructions=(
"When a site needs an email address, call create_inbox once. Put the ALIAS in the form. "
"Right after submitting, call wait_for_message with the ADDRESS and a word from the expected "
"subject. If it returns timed_out, call it again, up to three times."
),
tools=[function_tool(create_inbox), function_tool(wait_for_message)],
)
result = Runner.run_sync(signup_agent, "Sign up for a trial at https://app.example.com/signup and report the login.")
print(result.final_output)La misma herramienta en TypeScript, para el Vercel AI SDK
El tool() del AI SDK toma una descripción, un esquema y un execute; la descripción lleva las mismas dos frases. Pasa las dos herramientas a generateText o streamText con un maxSteps por encima de cuatro, porque el bucle dura cuatro llamadas a herramienta:
// inbox-tools.ts — the same two tools for the Vercel AI SDK (v5 shape: inputSchema + execute).
import { tool } from 'ai';
import { z } from 'zod';
const API = 'https://grabmail.io/api/v1';
const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
export const createInbox = tool({
description: 'Create a fresh disposable email inbox for this task. Returns the ADDRESS to poll and the ALIAS to give to websites. Put the alias into forms; never hand out the address.',
inputSchema: z.object({}),
execute: async () => {
const address = `agent-${crypto.randomUUID().slice(0, 8)}@grabmail.io`;
const res = await fetch(`${API}/mailbox?address=${encodeURIComponent(address)}`);
const { alias } = (await res.json()) as { alias: string | null };
return { address, alias, next_step: 'Put the alias into the form. Then call waitForMessage with the address.' };
},
});
export const waitForMessage = tool({
description: 'Wait for an email to arrive at the address, up to timeoutSeconds. Returns the message (from, subject, text, html) or { status: "timed_out" }. On timed_out, call again — up to three times — before concluding no mail was sent.',
inputSchema: z.object({
address: z.string(),
subjectContains: z.string().optional(),
timeoutSeconds: z.number().int().min(5).max(120).default(60),
}),
execute: async ({ address, subjectContains = '', timeoutSeconds }) => {
const deadline = Date.now() + timeoutSeconds * 1000;
while (Date.now() < deadline) {
const res = await fetch(`${API}/mailbox?address=${encodeURIComponent(address)}`);
if (res.status === 429) { await sleep(Number(res.headers.get('retry-after') ?? 1) * 1000); continue; }
if (!res.ok) throw new Error(`GET /mailbox answered ${res.status}`);
const { messages } = (await res.json()) as { messages: { id: string; subject: string }[] };
const hit = messages.find(m => m.subject.toLowerCase().includes(subjectContains.toLowerCase()));
if (hit) return (await fetch(`${API}/message/${hit.id}?mailbox=${encodeURIComponent(address)}`)).json();
await sleep(1000);
}
return { status: 'timed_out', hint: 'Call again, up to three times, before giving up.' };
},
});La forma es idéntica en cualquier framework que tenga una noción de herramienta: una descripción que lee el modelo, un esquema para los argumentos, y una función que se ejecuta de tu lado. Las dos cosas que hay que trasladar son la frase del alias y la frase de timed_out; todo lo demás es el cliente de la guía de Node o la guía de Python.
El bucle, en cuatro pasos
Sea cual sea el framework, un registro son las mismas cuatro llamadas a herramienta, y hay que decírselo al agente en sus instrucciones en lugar de dejar que lo descubra por su cuenta:
create_inbox, una vez por tarea. Vuelven una dirección, un alias y una frase que dice cuál es cuál.- El alias va en el formulario. Se envía.
wait_for_messagesobre la dirección, de inmediato, consubject_containspuesto a una palabra que va a llevar el correo de confirmación — «code», «verify», «confirm». Ni con un temporizador, ni después de otro trabajo: el correo ya está de camino.- El código sale del mensaje que devolvió la espera; el agente lo escribe o sigue el enlace. Seis dígitos después de las palabras que usa la plantilla — códigos OTP en tests automatizados tiene las reglas de extracción, que se aplican a un agente exactamente igual que a un test.
Una espera de sesenta segundos que devuelve timed_out no es un fallo; es «todavía no». Las instrucciones deberían decir: vuelve a llamar, hasta tres veces. Tres llamadas son más de tres minutos, que cubre cualquier correo transaccional que realmente se haya enviado — y le da al agente tres oportunidades de notar que el formulario mostró un error, o que escribió mal el alias.
La regla del alias
Todo buzón tiene dos direcciones. La dirección es con la que lee el agente; cualquiera que la tenga puede abrir el buzón, porque no hay cuenta y la dirección es la única llave. El alias es una segunda dirección en un dominio distinto que entrega en el mismo buzón y que no se puede usar para leerlo.
Así que el sitio recibe el alias y el agente se queda con la dirección. Un agente que pega la dirección en un formulario le ha entregado al sitio — y a cualquiera a quien el sitio se la filtre — la capacidad de leer todos los mensajes que el agente vaya a recibir ahí. Las funciones de herramienta de arriba devuelven las dos con un next_step que dice cuál va dónde, y las instrucciones lo repiten, porque una regla dicha dos veces es una regla que se sigue.
Barreras de seguridad para un agente sin supervisión
Una batería de pruebas falla y se detiene. Un agente que interpreta mal una situación sigue adelante, y sigue gastando. Cinco límites evitan que un paso de correo se convierta en la parte cara de una tarea:
- Un buzón por tarea
- Nunca reutilices una dirección entre tareas o ejecuciones. Un mensaje antiguo con un código que parece válido es la forma más rápida de que un agente haga con total confianza lo que no debe.
create_inboxno cuesta nada; llámala siempre. - Un presupuesto de esperas
- Tres llamadas a
wait_for_message, y después parar e informar. Un agente que espera indefinidamente un correo que nunca se envió quema una ranura de worker y una factura. - Un plazo para todo el paso
- Cinco minutos de principio a fin, desde el envío hasta el código. Superado eso, lo correcto es contarle a un humano lo que pasó, no volver a intentar el formulario.
- Filtrado por asunto
- Pasa siempre
subject_contains. Un correo de bienvenida que llega antes que el correo con el código es, si no, «el mensaje», y el agente extrae seis dígitos de un pie de página publicitario. - Registrar la dirección en el log
- Escribe la dirección en el log de la tarea. Los mensajes se conservan 5 días, así que un humano puede abrir el buzón después y ver exactamente lo que vio el agente — lo más útil, con diferencia, cuando una ejecución sale mal.
Agentes de navegador
Un agente que conduce un navegador real — Browser Use, un servidor MCP de Playwright, un modelo computer-use — es el caso en el que el paso de correo muerde con más fuerza, porque se va a encontrar el formulario antes de que nadie lo haya planeado. Tres cosas hacen que funcione:
- Dale los dos servidores. Las herramientas de navegador y las herramientas de buzón en la misma sesión, para que «revisa tu correo» sea una llamada a herramienta y no un callejón sin salida.
- Pon el bucle en el system prompt. Cuatro líneas: crea un buzón en el primer campo de correo; el alias en el formulario; espera sobre la dirección justo después de enviar; tres reintentos ante un
timed_out. - Espera rechazos. Un formulario que rechace el dominio del alias lo va a decir en la página; el agente debería leer el error y parar, no probar nombres. Las formas honestas de evitar un rechazo — un dominio propio, o un dominio del fondo que se mantiene fuera de las listas de bloqueo — son decisiones de configuración tuyas, no del agente.
Para que lo lea el propio agente, el sitio publica llms.txt, un mapa en texto plano que dice las mismas cosas que esta página en la forma que prefiere un modelo, y un documento OpenAPI a partir del cual un agente que escribe código puede construir el cliente.
Antes de dejarlo correr sin supervisión
- Descripciones de herramienta que indiquen la regla del alias y qué significa
timed_out. - Instrucciones con el bucle de cuatro pasos y un presupuesto de tres esperas.
create_inboxllamada una vez por tarea, nunca reutilizada.subject_containsen cada espera.- La dirección escrita en el log de la tarea.
- Una línea que prohíba enviar nada confidencial al buzón.
Eso es todo lo que necesita un agente. El mismo buzón sirve a una batería de pruebas de día y a un agente de noche, porque por dentro son las mismas tres llamadas HTTP — y si el sitio en el que se registra el agente rechaza los dominios públicos, un dominio propio o el fondo que se mantiene fuera de las listas encajan sin cambiar nada en las herramientas.
Preguntas
¿Necesito una clave de API para el agente?
No. Los dominios públicos no piden clave, ni cuenta, ni cabecera, tanto por REST como por MCP. Solo el fondo de pago de dominios que se mantienen fuera de las listas de bloqueo de correo desechable usa un token Bearer, y las funciones de herramienta son idénticas salvo por eso.
¿MCP o una herramienta REST — cuál debería elegir?
Si el agente ya vive en un cliente MCP, MCP: es una línea y la espera es del lado del servidor. Si estás escribiendo el agente en un framework, una función de herramienta: son cuarenta líneas, funciona con cualquier modelo, y controlas la descripción que lee el modelo. Las dos formas llegan al mismo buzón.
¿Cuánto cuesta la espera en tokens?
Nada, de las dos formas. La espera de MCP bloquea en el servidor; la herramienta REST hace el bucle en tu código. El modelo gasta tokens en la llamada a la herramienta y en leer el resultado, no en los sesenta segundos intermedios — que es exactamente la razón para no dejar que un modelo consulte un buzón por sí mismo.
¿Pueden ejecutarse varios agentes a la vez?
Sí. Cada tarea recibe su propio buzón y no hay ningún estado de sesión. Los límites son una lectura por segundo por dirección y 1200 solicitudes por minuto por cliente por REST, y 8 llamadas simultáneas a wait_for_message por MCP — superado eso, la herramienta responde timed_out de inmediato y el agente vuelve a llamar.
¿Puede el agente enviar correo desde el buzón?
No. El servicio solo recibe, por diseño — un buzón gratuito sin cuenta que pudiera enviar sería un relé de spam en menos de una hora. Un agente que tenga que enviar correo necesita un proveedor de envío y sus propias credenciales.
¿Qué pasa si el sitio rechaza el dominio del alias?
Entonces está en una lista de bloqueo de dominios desechables, y ningún nombre delante de la @ va a cambiar eso. Apunta un dominio que poseas al servicio (un registro MX, gratis), o usa un dominio del fondo de pago que se mantiene fuera de las listas — las dos opciones encajan en las mismas herramientas cambiando la constante del dominio. Por qué los formularios de registro bloquean el correo desechable explica qué comprobación te rechazó.
¿Es privado el buzón para mi agente?
No. Cualquiera que conozca la dirección puede leerlo, que es la razón por la que existe el alias y por la que nunca debería enviarse nada confidencial ahí. Para un código que vive diez minutos eso no supone ningún problema; es la única regla que las instrucciones del agente tienen que dejar clara.


