Le mur que chaque agent percute
S’inscrire à un essai, créer un espace de travail, réclamer une clé d’API, rejoindre une bêta : chacune de ces actions se termine par un formulaire, et chaque formulaire se termine par une boîte mail. Un humain jette un œil à son téléphone. Un agent n’a rien vers quoi jeter un œil — il n’a aucune adresse qu’il puisse lire, et les plus sensés s’arrêtent pour vous demander le code, ce qui annule tout l’intérêt d’avoir envoyé un agent.
La solution n’est pas un prompt plus habile. C’est une boîte que l’agent peut lire par programme, sans compte à créer au préalable (un agent qui crée un compte mail se heurte au même mur, un niveau plus bas) et sans clé à gérer. Une boîte jetable, c’est exactement cela : une boîte existe dès que le courrier l’atteint, et la lire tient en une seule requête HTTP.
Deux façons de lui donner une boîte
La même boîte est accessible de deux façons, et le choix dépend de la manière dont l’agent est construit, pas de la boîte elle-même :
| Critère | Serveur MCP | Fonction outil REST |
|---|---|---|
| Convient quand | L’agent tourne dans un client MCP — Claude Code, Cursor, Claude Desktop, ou un framework doté d’un adaptateur MCP. | Vous écrivez l’agent en code : LangChain, l’OpenAI Agents SDK, le Vercel AI SDK, ou votre propre boucle. |
| L’attente | wait_for_message bloque côté serveur jusqu’à 25 secondes et renvoie le message en entier. Zéro token dépensé pendant l’attente. | La fonction outil boucle une fois par seconde jusqu’à son échéance. Zéro token non plus — la boucle est dans votre code, pas dans le modèle. |
| Mise en place | Une seule URL dans la configuration du client. Aucun code. | Deux fonctions, quarante lignes, une bibliothèque HTTP. |
| Ce que voit le modèle | Six outils avec leurs descriptions, plus un paragraphe d’instructions que le serveur envoie au moment de la connexion. | Ce que disent vos descriptions d’outils, quelles qu’elles soient. Les docstrings ci-dessous sont écrites pour jouer ce rôle. |
Le chemin MCP tient en une ligne, et la configuration propre à chaque client donne la ligne exacte pour sept clients ; une boîte qu’un agent IA peut lire explique les outils en détail. Le reste de ce guide, c’est le chemin REST, pour les agents que vous construisez vous-même.
{"mcpServers":{"grabmail":{"url":"https://grabmail.io/mcp"}}}Une fonction outil : en Python, pour LangChain et l’OpenAI Agents SDK
Deux fonctions simples, avec des docstrings soignées. Les docstrings comptent plus que le code : dans les deux frameworks, elles deviennent la description que le modèle lit pour décider quand appeler l’outil et que faire de la réponse — elles énoncent donc les deux choses qu’un agent se trompe sans qu’on les lui dise : alias contre adresse, et ce que signifie 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."}Les envelopper tient en un appel par framework. Le tool de LangChain lit la docstring et les indications de type ; le function_tool de l’Agents SDK fait de même et ajoute les outils à un agent dont les instructions répètent la boucle :
# 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)Le même outil en TypeScript, pour le Vercel AI SDK
Le tool() de l’AI SDK prend une description, un schéma et un execute ; la description porte les deux mêmes phrases. Passez les deux outils à generateText ou streamText avec un maxSteps supérieur à quatre, parce que la boucle compte quatre appels d’outil :
// 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 forme est identique dans tout framework qui a une notion d’outil : une description que le modèle lit, un schéma pour les arguments, et une fonction qui s’exécute de votre côté. Les deux éléments à reprendre tels quels sont la phrase sur l’alias et la phrase sur timed_out ; tout le reste, c’est le client du guide Node ou du guide Python.
La boucle, en quatre étapes
Quel que soit le framework, une inscription se résume aux quatre mêmes appels d’outil, et il vaut mieux le dire à l’agent dans ses instructions que le laisser le découvrir :
create_inbox, une fois par tâche. En retour viennent une adresse, un alias et une phrase précisant lequel est lequel.- L’alias va dans le formulaire. Envoyez.
wait_for_messagesur l’adresse, immédiatement, avecsubject_containsréglé sur un mot que portera l’e-mail de confirmation — « code », « verify », « confirm ». Ni sur une minuterie, ni après d’autres tâches : le courrier est déjà en chemin.- Le code sort du message que l’attente a renvoyé ; l’agent le saisit ou suit le lien. Six chiffres après les mots qu’utilise le modèle d’e-mail — les codes OTP dans les tests automatisés donne les règles d’extraction, qui s’appliquent à un agent exactement comme à un test.
Une attente de soixante secondes qui renvoie timed_out n’est pas un échec ; c’est un « pas encore ». Les instructions devraient dire : rappelez, jusqu’à trois fois. Trois appels, c’est plus de trois minutes, ce qui couvre n’importe quel courrier transactionnel réellement envoyé — et cela donne à l’agent trois occasions de remarquer que le formulaire a affiché une erreur, ou qu’il a mal tapé l’alias.
La règle de l’alias
Chaque boîte a deux adresses. L’adresse est celle avec laquelle l’agent lit ; quiconque la possède peut ouvrir la boîte, car il n’y a pas de compte et l’adresse est la seule clé. L’alias est une seconde adresse, sur un domaine séparé, qui livre dans la même boîte mais ne peut pas servir à la lire.
Le site reçoit donc l’alias, et l’agent garde l’adresse. Un agent qui colle l’adresse dans un formulaire a donné au site — et à quiconque le site la divulguerait — la capacité de lire chaque message que l’agent y recevra jamais. Les fonctions outils ci-dessus renvoient les deux, avec un next_step précisant ce qui va où, et les instructions le répètent, parce qu’une règle énoncée deux fois est une règle suivie.
Garde-fous pour un agent sans supervision
Une suite de tests échoue et s’arrête. Un agent qui interprète mal une situation continue, et continue de dépenser. Cinq limites empêchent l’étape e-mail de devenir la partie coûteuse d’une tâche :
- Une boîte par tâche
- Ne réutilisez jamais une adresse d’une tâche ou d’une exécution à l’autre. Un ancien message contenant un code plausible est le moyen le plus rapide pour un agent de faire, avec assurance, la mauvaise chose.
create_inboxne coûte rien ; appelez-le à chaque fois. - Un budget d’attentes
- Trois appels à
wait_for_message, puis arrêt et rapport. Un agent qui attend indéfiniment un courrier qui n’a jamais été envoyé brûle un emplacement de worker et une facture. - Une échéance sur l’étape entière
- Cinq minutes de l’envoi du formulaire au code, de bout en bout. Au-delà, la bonne action est de dire à un humain ce qui s’est passé, pas de retenter le formulaire.
- Filtrage par sujet
- Passez toujours
subject_contains. Un e-mail de bienvenue qui arrive avant l’e-mail de code devient sinon « le message », et l’agent extrait six chiffres d’un pied de page marketing. - Journaliser l’adresse
- Écrivez l’adresse dans le journal de la tâche. Les messages restent 5 jours, donc un humain peut ouvrir la boîte après coup et voir exactement ce que l’agent a vu — la chose la plus utile qui soit quand une exécution tourne mal.
Les agents navigateurs
Un agent qui pilote un véritable navigateur — Browser Use, un serveur MCP Playwright, un modèle de computer use — c’est le cas où l’étape e-mail mord le plus fort, parce qu’il rencontrera le formulaire avant que quiconque ne l’ait prévu. Trois choses le font fonctionner :
- Donnez-lui les deux serveurs. Les outils de navigateur et les outils de boîte mail dans la même session, pour que « consultez votre e-mail » soit un appel d’outil, pas une impasse.
- Mettez la boucle dans le prompt système. Quatre lignes : créer une boîte au premier champ e-mail ; l’alias dans le formulaire ; attendre sur l’adresse juste après l’envoi ; trois nouvelles tentatives sur
timed_out. - Attendez-vous à des refus. Un formulaire qui rejette le domaine de l’alias le dira dans la page ; l’agent devrait lire l’erreur et s’arrêter, pas essayer d’autres noms. Les façons honnêtes de contourner un refus — un domaine qui vous appartient, ou un domaine venu du pool tenu à l’écart des listes noires — sont des décisions de configuration qui vous reviennent, pas à l’agent.
Pour la lecture de l’agent lui-même, le site publie llms.txt, une carte en texte brut qui dit les mêmes choses que cette page, sous la forme qu’un modèle préfère, ainsi qu’un document OpenAPI à partir duquel un agent qui écrit du code peut construire le client.
Avant de le laisser tourner sans supervision
- Des descriptions d’outils qui énoncent la règle de l’alias et ce que signifie
timed_out. - Des instructions avec la boucle en quatre étapes et un budget de trois attentes.
create_inboxappelé une fois par tâche, jamais réutilisé.subject_containssur chaque attente.- L’adresse écrite dans le journal de la tâche.
- Une ligne interdisant d’envoyer quoi que ce soit de confidentiel vers la boîte.
C’est tout ce dont un agent a besoin. La même boîte sert une suite de tests le jour et un agent la nuit, parce qu’en dessous, ce sont les trois mêmes appels HTTP — et si le site auquel l’agent s’inscrit refuse les domaines publics, un domaine qui vous appartient ou le pool tenu à l’écart des listes s’intègre sans le moindre changement aux outils.
Questions
Ai-je besoin d’une clé d’API pour l’agent ?
Non. Les domaines publics ne demandent ni clé, ni compte, ni en-tête, aussi bien via REST que via MCP. Seul le pool payant de domaines tenus à l’écart des listes noires de mail jetable utilise un token bearer, et les fonctions outils restent par ailleurs identiques pour lui.
MCP ou un outil REST — lequel choisir ?
Si l’agent vit déjà dans un client MCP, MCP : c’est une ligne, et l’attente est côté serveur. Si vous écrivez l’agent dans un framework, une fonction outil : c’est quarante lignes, cela fonctionne avec n’importe quel modèle, et vous contrôlez la description que lit le modèle. Les deux atteignent la même boîte.
Combien coûte l’attente en tokens ?
Rien, dans les deux cas. L’attente MCP bloque sur le serveur ; l’outil REST boucle dans votre code. Le modèle dépense des tokens sur l’appel d’outil et sur la lecture du résultat, pas sur les soixante secondes entre les deux — ce qui est précisément la raison de ne jamais laisser un modèle interroger une boîte lui-même.
Plusieurs agents peuvent-ils tourner en même temps ?
Oui. Chaque tâche reçoit sa propre boîte, et il n’y a aucun état de session. Les limites sont d’une lecture par seconde par adresse et de 1200 requêtes par minute par client via REST, et de 8 appels wait_for_message simultanés via MCP — au-delà, l’outil répond aussitôt timed_out et l’agent rappelle.
L’agent peut-il envoyer des e-mails depuis la boîte ?
Non. Le service ne fait que recevoir, par conception — une boîte gratuite sans compte qui pourrait envoyer deviendrait un relais de spam en moins d’une heure. Un agent qui doit envoyer du courrier a besoin d’un fournisseur d’envoi et de ses propres identifiants.
Que faire si le site refuse le domaine de l’alias ?
C’est alors qu’il figure sur une liste noire de domaines jetables, et aucun nom devant le @ n’y changera rien. Pointez un domaine que vous possédez vers le service (un enregistrement MX, gratuit), ou utilisez un domaine du pool payant tenu à l’écart des listes — les deux s’intègrent aux mêmes outils en changeant simplement la constante de domaine. Pourquoi les formulaires d’inscription bloquent le mail jetable explique quelle vérification vous a refusé.
La boîte est-elle privée pour mon agent ?
Non. Quiconque connaît l’adresse peut la lire, c’est pourquoi l’alias existe et pourquoi rien de confidentiel ne devrait jamais y être envoyé. Pour un code qui ne vit que dix minutes, ce n’est pas grave ; c’est la seule règle que les instructions de l’agent doivent énoncer clairement.


