A parede em que todo agente esbarra
Cadastrar-se para um teste gratuito, criar um workspace, resgatar uma chave de API, entrar em um beta: cada uma dessas coisas termina em um formulário, e todo formulário termina em uma caixa de entrada. Um humano dá uma olhada no celular. Um agente não tem nada para olhar — ele não tem um endereço que consiga ler, e os mais sensatos param e pedem o código a você, o que anula todo o sentido de ter enviado um agente.
A solução não é um prompt mais esperto. É uma caixa de entrada que o agente consegue ler de forma programática, sem conta para criar antes (um agente que cria uma conta de e-mail esbarra na mesma parede um nível abaixo) e sem chave para gerenciar. Uma caixa de entrada descartável é exatamente isso: uma caixa de entrada existe no momento em que o e-mail chega a ela, e lê-la é uma única requisição HTTP.
Duas formas de dar a ele uma caixa de entrada
A mesma caixa de entrada é acessível de duas formas, e a escolha depende de como o agente é construído, não da caixa de entrada:
| Critério | Servidor MCP | Função de ferramenta REST |
|---|---|---|
| Serve quando | O agente roda em um cliente MCP — Claude Code, Cursor, Claude Desktop, ou um framework com um adaptador MCP. | Você escreve o agente em código: LangChain, o OpenAI Agents SDK, o Vercel AI SDK, ou o seu próprio loop. |
| Espera | wait_for_message bloqueia do lado do servidor por até 25 segundos e retorna a mensagem inteira. Zero tokens gastos durante a espera. | A função de ferramenta faz um loop de uma vez por segundo até o prazo. Zero tokens também — o loop está no seu código, não no modelo. |
| Configuração | Uma URL na configuração do cliente. Nenhum código. | Duas funções, quarenta linhas, uma biblioteca HTTP. |
| O que o modelo vê | Seis ferramentas com descrições, mais um parágrafo de instruções que o servidor envia no momento da conexão. | O que quer que as descrições das suas ferramentas digam. As docstrings abaixo são escritas justamente para isso. |
O caminho via MCP é uma linha, e a configuração por cliente tem a linha exata para sete clientes; uma caixa de entrada que um agente de IA consegue ler explica as ferramentas em profundidade. O restante deste guia é o caminho via REST, para agentes que você mesmo constrói.
{"mcpServers":{"grabmail":{"url":"https://grabmail.io/mcp"}}}Uma função de ferramenta: Python, para LangChain e o OpenAI Agents SDK
Duas funções simples com docstrings cuidadosas. As docstrings importam mais do que o código: nos dois frameworks elas se tornam a descrição que o modelo lê para decidir quando chamar a ferramenta e o que fazer com a resposta, então elas dizem as duas coisas que um agente erra — alias versus endereço, e o que timed_out significa.
"""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."}Envolvê-las é uma chamada por framework. O tool do LangChain lê a docstring e os type hints; o function_tool do Agents SDK faz o mesmo e adiciona as ferramentas a um agente cujas instruções repetem o loop:
# 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)A mesma ferramenta em TypeScript, para o Vercel AI SDK
O tool() do AI SDK recebe uma description, um schema e um execute; a description carrega as mesmas duas frases. Passe as duas ferramentas para generateText ou streamText com um maxSteps acima de quatro, porque o loop tem quatro chamadas de ferramenta de comprimento:
// 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.' };
},
});O formato é idêntico em qualquer framework que tenha uma noção de ferramenta: uma descrição que o modelo lê, um schema para os argumentos, e uma função que roda do seu lado. As duas coisas para levar adiante são a frase sobre o alias e a frase sobre timed_out; tudo o mais é o cliente do guia de Node ou do guia de Python.
O loop, em quatro passos
Seja qual for o framework, um cadastro é sempre as mesmas quatro chamadas de ferramenta, e o agente deveria ser avisado disso nas próprias instruções, em vez de descobrir sozinho:
create_inbox, uma vez por tarefa. Voltam um endereço, um alias e uma frase dizendo qual é qual.- O alias vai para o formulário. Envie.
wait_for_messageno endereço, imediatamente, comsubject_containsdefinido para uma palavra que o e-mail de confirmação vai carregar — “code”, “verify”, “confirm”. Não em um timer, não depois de outro trabalho: o e-mail já está a caminho.- O código sai da mensagem que a espera retornou; o agente o digita ou segue o link. Seis dígitos depois das palavras que o template usa — códigos OTP em testes automatizados tem as regras de extração, que se aplicam a um agente exatamente como a um teste.
Uma espera de sessenta segundos que retorna timed_out não é uma falha; é “ainda não”. As instruções deveriam dizer: chame de novo, até três vezes. Três chamadas são mais de três minutos, o que cobre qualquer e-mail transacional que tenha realmente sido enviado — e dá ao agente três chances de perceber que o formulário mostrou um erro, ou que ele digitou o alias errado.
A regra do alias
Toda caixa de entrada tem dois endereços. O endereço é aquele com o qual o agente lê; qualquer um que o tenha pode abrir a caixa de entrada, porque não há conta e o endereço é a única chave. O alias é um segundo endereço em um domínio separado que entrega na mesma caixa de entrada e não pode ser usado para lê-la.
Então o site recebe o alias e o agente guarda o endereço. Um agente que cola o endereço em um formulário entregou ao site — e a qualquer um para quem o site o vazar — a capacidade de ler toda mensagem que o agente algum dia receber ali. As funções de ferramenta acima retornam os dois com um next_step dizendo o que vai para onde, e as instruções repetem isso, porque uma regra dita duas vezes é uma regra seguida.
Proteções para um agente sem supervisão
Uma suíte de testes falha e para. Um agente que interpreta mal uma situação continua indo, e continua gastando. Cinco limites impedem que uma etapa de e-mail se torne a parte cara de uma tarefa:
- Uma caixa de entrada por tarefa
- Nunca reutilize um endereço entre tarefas ou execuções. Uma mensagem antiga com um código de aparência plausível dentro é a forma mais rápida de um agente fazer a coisa errada com total confiança.
create_inboxnão custa nada; chame-a toda vez. - Um orçamento de esperas
- Três chamadas a
wait_for_message, depois pare e reporte. Um agente que espera indefinidamente por um e-mail que nunca foi enviado consome um slot de worker e gera custo. - Um prazo para a etapa inteira
- Cinco minutos do envio ao código, de ponta a ponta. Além disso, a ação certa é contar a um humano o que aconteceu, não tentar o formulário de novo.
- Filtragem por assunto
- Sempre passe
subject_contains. Um e-mail de boas-vindas que chega antes do e-mail com código é, do contrário, “a mensagem”, e o agente extrai seis dígitos de um rodapé de marketing. - Registrando o endereço no log
- Escreva o endereço no log da tarefa. As mensagens ficam 5 dias, então um humano pode abrir a caixa de entrada depois e ver exatamente o que o agente viu — a coisa mais útil quando uma execução dá errado.
Agentes de navegador
Um agente pilotando um navegador de verdade — Browser Use, um servidor MCP do Playwright, um modelo de computer-use — é o caso em que a etapa de e-mail morde mais forte, porque ele vai encontrar o formulário antes de qualquer um ter planejado para isso. Três coisas fazem isso funcionar:
- Dê a ele os dois servidores. As ferramentas de navegador e as ferramentas de caixa de entrada na mesma sessão, para que “confira seu e-mail” seja uma chamada de ferramenta, e não um beco sem saída.
- Coloque o loop no system prompt. Quatro linhas: crie uma caixa de entrada no primeiro campo de e-mail; alias no formulário; espere no endereço logo após o envio; três novas tentativas em caso de
timed_out. - Espere recusas. Um formulário que rejeita o domínio do alias vai dizer isso na página; o agente deveria ler o erro e parar, não tentar outros nomes. As formas honestas de contornar uma recusa — um domínio seu, ou um domínio do conjunto mantido fora das listas de bloqueio — são decisões de configuração para você, não para o agente.
Para a própria leitura do agente, o site publica llms.txt, um mapa em texto puro que diz as mesmas coisas que esta página diz, na forma que um modelo prefere, e um documento OpenAPI a partir do qual um agente que escreve código consegue construir o cliente.
Antes de deixá-lo rodar sem supervisão
- Descrições de ferramentas que declaram a regra do alias e o que
timed_outsignifica. - Instruções com o loop de quatro passos e um orçamento de três esperas.
create_inboxchamada uma vez por tarefa, nunca reutilizada.subject_containsem toda espera.- O endereço escrito no log da tarefa.
- Uma linha proibindo que qualquer coisa confidencial seja enviada à caixa de entrada.
Isso é tudo o que um agente precisa. A mesma caixa de entrada serve uma suíte de testes de dia e um agente de noite, porque por baixo são as mesmas três chamadas HTTP — e se o site em que o agente se cadastra recusa os domínios públicos, um domínio seu ou o conjunto mantido fora das listas se encaixa sem nenhuma mudança nas ferramentas.
Perguntas
Preciso de uma chave de API para o agente?
Não. Os domínios públicos não exigem chave, conta nem cabeçalho, tanto via REST quanto via MCP. Só o conjunto pago de domínios mantidos fora das listas de bloqueio de e-mail descartável usa um token bearer, e fora isso as funções de ferramenta são idênticas para ele.
MCP ou uma ferramenta REST — qual devo escolher?
Se o agente já vive em um cliente MCP, MCP: é uma linha e a espera é do lado do servidor. Se você está escrevendo o agente em um framework, uma função de ferramenta: são quarenta linhas, funciona com qualquer modelo, e você controla a descrição que o modelo lê. Os dois alcançam a mesma caixa de entrada.
Quanto a espera custa em tokens?
Nada, de qualquer forma. A espera via MCP bloqueia no servidor; a ferramenta REST faz o loop no seu código. O modelo gasta tokens na chamada de ferramenta e na leitura do resultado, não nos sessenta segundos entre elas — e essa é a razão inteira para não deixar um modelo fazer polling em uma caixa de entrada sozinho.
Vários agentes podem rodar ao mesmo tempo?
Sim. Cada tarefa recebe sua própria caixa de entrada e não há estado de sessão. Os limites são uma leitura por segundo por endereço e 1200 requisições por minuto por cliente via REST, e 8 chamadas simultâneas de wait_for_message via MCP — além disso a ferramenta responde timed_out na hora e o agente chama de novo.
O agente pode enviar e-mail a partir da caixa de entrada?
Não. O serviço só recebe, por design — uma caixa de entrada gratuita sem conta que pudesse enviar viraria um relay de spam em menos de uma hora. Um agente que precisa enviar e-mail precisa de um provedor de envio e das próprias credenciais.
E se o site recusar o domínio do alias?
Então ele está em uma lista de bloqueio de domínios descartáveis, e nenhum nome na frente do @ vai mudar isso. Aponte um domínio seu para o serviço (um registro MX, gratuito), ou use um domínio do conjunto pago mantido fora das listas — os dois se encaixam nas mesmas ferramentas mudando a constante do domínio. Por que formulários de cadastro bloqueiam e-mail descartável explica qual verificação te recusou.
A caixa de entrada é privada para o meu agente?
Não. Qualquer um que saiba o endereço pode lê-la, e é por isso que o alias existe e por isso que nada confidencial deveria jamais ser enviado para lá. Para um código que vive dez minutos, isso não é problema; é a única regra que as instruções do agente precisam declarar claramente.


