Grok Bot, Meta Muse, les dots d'OpenAI, Manus : derrière des noms et des interfaces très différents, ces produits reposent sur les mêmes briques. Dans ce tutoriel, on les décompose, puis on construit une petite app de chat qui les assemble avec l'API Baguette Bot : tu écris une tâche, la réponse de l'agent s'affiche mot à mot, et l'écran de son ordinateur apparaît à côté, en direct. Deux fichiers, une centaine de lignes, et une base sur laquelle les autres tutoriels de la série construisent leur propre « app comme X ».
Les briques communes à tous ces produits
Regarde ce que promettent ces produits. Grok Bot, de xAI et Cursor, c'est une équipe d'agents toujours actifs qui partagent un ordinateur cloud, se connectent aux sites comme une personne et se passent le relais. Meta Muse donne à chaque utilisateur un agent sur sa propre machine virtuelle dans le cloud, qui continue à travailler quand l'app est fermée. Une dot d'OpenAI est un agent personnel avec son ordinateur et son navigateur, qui travaille en arrière-plan. Manus a popularisé l'agent à qui tu donnes une tâche et qui ouvre son ordinateur pour naviguer, écrire du code et produire des fichiers.
Une fois les interfaces retirées, il reste six briques :
| Brique | Ce que ça apporte | Dans l'API Baguette Bot |
|---|---|---|
| Un agent | Un rôle, une mémoire, un modèle | POST /v1/agents |
| Son propre ordinateur | Chrome, terminal, fichiers, sessions conservées | Allumé à la première tâche, /computer/start et /stop |
| L'écran en direct | Voir l'agent travailler, reprendre la main | POST /computer/live (iframe signée) |
| Les fichiers | Donner un document, récupérer un résultat | POST et GET /files |
| Les événements | Réponse mot à mot, statut, état de l'ordinateur | GET /events (SSE) |
| Planification, notifications, équipes | Routines, alertes, plusieurs agents qui collaborent | De ton côté : ton cron, ton canal, ton serveur |
Les cinq premières lignes sont des routes de l'API. La dernière, c'est ton code : l'API ne fournit pas de planificateur, pas de webhooks ni d'envoi de notifications, et ses agents n'envoient pas d'e-mails et ne passent pas d'appels. Ce n'est pas un défaut caché, c'est la répartition des rôles. Ton serveur décide quand un agent travaille (un node-cron, un timer systemd, une GitHub Action), où arrive le résultat (Telegram, Slack, ton propre service de push) et qui parle à qui quand tu as plusieurs agents. Pour tes utilisateurs, tu associes chacun à son agent avec le champ metadata et ta propre base de données.
Côté infrastructure, chaque agent a un ordinateur Linux isolé (bureau 1280×800, Chrome, terminal, sudo), hébergé à Paris, qui s'allume tout seul et s'éteint après idle_minutes sans activité. Ses fichiers restent quand il est éteint.
Ce qu'il te faut
- Node 18 ou plus récent (
fetchest intégré). - Une clé API, créée dans la console ; elle commence par
bb_live_. Tu as 1 € offert une fois ton e-mail vérifié, largement assez pour ce tutoriel. - Cinq minutes.
mkdir mon-app-agents && cd mon-app-agents
npm init -y
npm pkg set type=module
npm i express
mkdir public
export BAGUETTE_API_KEY=bb_live_…La clé reste sur ton serveur. La page web ne la voit jamais : elle parle à ton serveur, qui parle à l'API.
Le serveur : relayer l'agent vers le navigateur
Le serveur fait quatre choses : créer l'agent (une fois), lui transmettre les messages, relayer son flux d'événements vers le navigateur, et créer le lien vers son écran. Le relais est nécessaire parce que EventSource, côté navigateur, ne peut pas envoyer l'en-tête Authorization.
// server.js
import express from "express";
import { Readable } from "node:stream";
const KEY = process.env.BAGUETTE_API_KEY;
const BASE = "https://api.baguettebot.com";
// Le petit helper de la documentation, qui accepte aussi une réponse vide
const api = (path, body, method = body ? "POST" : "GET") =>
fetch(BASE + path, {
method,
headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
body: body && JSON.stringify(body),
}).then(async (r) => {
const text = await r.text();
const data = text ? JSON.parse(text) : {};
if (!r.ok) throw new Error(data.error?.message ?? r.statusText);
return data;
});
// Un seul agent pour la démo. Mets son id dans AGENT_ID pour le réutiliser.
let agentId = process.env.AGENT_ID;
async function getAgent() {
if (!agentId) {
const agent = await api("/v1/agents", {
name: "Assistant",
instructions: "Tu travailles sur ton ordinateur. Réponds en français, de façon concise.",
idle_minutes: 2,
metadata: { app: "mon-app-agents" },
});
agentId = agent.id;
console.log("Agent créé :", agentId);
}
return agentId;
}
// Les erreurs d'une route async finissent en JSON, pas en crash
const route = (fn) => (req, res) =>
fn(req, res).catch((e) => (res.headersSent ? res.end() : res.status(500).json({ error: e.message })));
const app = express();
app.use(express.json());
app.use(express.static("public"));
// 1. Envoyer une tâche, sans attendre : la réponse arrive par les événements
app.post("/api/chat", route(async (req, res) => {
const id = await getAgent();
await api(`/v1/agents/${id}/messages`, { content: String(req.body.text ?? "") });
res.json({ ok: true });
}));
// 2. Relayer le flux SSE de l'agent tel quel
app.get("/api/events", route(async (req, res) => {
const id = await getAgent();
const ctrl = new AbortController();
req.on("close", () => ctrl.abort());
const upstream = await fetch(`${BASE}/v1/agents/${id}/events`, {
headers: { Authorization: `Bearer ${KEY}` },
signal: ctrl.signal,
});
if (!upstream.ok) return res.status(upstream.status).end();
res.set({ "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" });
res.flushHeaders();
Readable.fromWeb(upstream.body).on("error", () => res.end()).pipe(res);
}));
// 3. Un lien vers l'écran, seulement si l'ordinateur est allumé
app.post("/api/live", route(async (req, res) => {
const id = await getAgent();
const computer = await api(`/v1/agents/${id}/computer`);
if (computer.status !== "running") return res.json({ url: null });
const live = await api(`/v1/agents/${id}/computer/live`, { interactive: false, ttl_seconds: 3600 });
res.json({ url: live.url });
}));
app.listen(3000, () => console.log("http://localhost:3000"));Trois remarques. On envoie la tâche sans wait : l'API répond tout de suite (202) et l'agent travaille. Ça règle d'office le cas des tâches de plus de 120 s, qui avec wait: true reviendraient en status: "working" : ici, le navigateur suit simplement le flux jusqu'au bout. Ensuite, le flux est relayé octet par octet, sans le réinterpréter : il est déjà au format SSE, et le : ping envoyé toutes les 15 s garde la connexion ouverte. Enfin, le lien d'écran est créé côté serveur ; seule son URL signée et temporaire part vers la page.
La page : la réponse en direct et l'écran
<!-- public/index.html -->
<!doctype html>
<html lang="fr">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Mon agent</title>
<style>
body { font: 15px/1.5 system-ui, sans-serif; margin: 0; padding: 16px; display: grid; gap: 16px; }
@media (min-width: 900px) { body { grid-template-columns: 1fr 1.4fr; } }
#log div { white-space: pre-wrap; padding: 8px 12px; border-radius: 10px; margin: 6px 0; }
.user { background: #e8f0fe; } .assistant { background: #f3f3f3; }
#status { color: #777; min-height: 1.5em; }
form { display: flex; gap: 8px; } input { flex: 1; padding: 8px; }
iframe { width: 100%; aspect-ratio: 16 / 10; border: 0; border-radius: 12px; background: #111; }
</style>
<body>
<section>
<div id="log"></div>
<p id="status"></p>
<form id="form"><input id="q" autocomplete="off" placeholder="Une tâche pour ton agent…"><button>Envoyer</button></form>
</section>
<iframe id="screen" title="Écran de l'agent"></iframe>
<script>
const log = document.getElementById("log");
const statusEl = document.getElementById("status");
const screen = document.getElementById("screen");
const bubbles = new Map();
function bubble(id, role) {
let el = bubbles.get(id);
if (!el) { el = document.createElement("div"); el.className = role; log.append(el); bubbles.set(id, el); }
return el;
}
const events = new EventSource("/api/events");
events.addEventListener("status", (e) => {
const d = JSON.parse(e.data);
statusEl.textContent = d.status === "working" ? (d.text || "Au travail…") : "";
});
events.addEventListener("delta", (e) => {
const d = JSON.parse(e.data);
bubble(d.message_id, "assistant").textContent += d.text;
});
const onMessage = (e) => {
const m = JSON.parse(e.data);
if (m.role === "assistant" && m.content) bubble(m.id, "assistant").textContent = m.content;
};
events.addEventListener("message", onMessage);
events.addEventListener("message.updated", onMessage);
events.addEventListener("computer", (e) => {
if (JSON.parse(e.data).status === "running") showScreen();
});
async function showScreen() {
if (screen.getAttribute("src")) return;
const { url } = await fetch("/api/live", { method: "POST" }).then((r) => r.json());
if (url) screen.src = url;
}
showScreen();
document.getElementById("form").addEventListener("submit", async (e) => {
e.preventDefault();
const q = document.getElementById("q");
if (!q.value.trim()) return;
const el = document.createElement("div"); el.className = "user"; el.textContent = q.value; log.append(el);
await fetch("/api/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text: q.value }) });
q.value = "";
});
</script>
</body>
</html>Lance node server.js, ouvre http://localhost:3000 et demande par exemple « Va sur le site de la SNCF et trouve le premier train Paris-Lyon demain matin ». L'événement computer passe à running au bout de quelques secondes, l'iframe affiche le bureau de l'agent et tu le vois ouvrir Chrome. Les delta remplissent la bulle de réponse au fil de l'écriture, et le message final la remplace par le texte complet (en Markdown : branche une bibliothèque comme marked si tu veux le rendre).
Montrer l'écran en direct, et laisser la main
Le lien de /computer/live est en lecture seule par défaut. Avec interactive: true, ton utilisateur prend la souris et le clavier dans l'iframe : c'est ce qui permet de se connecter à un site ou de passer un captcha à la place de l'agent, puis de le laisser continuer. Les sessions du navigateur sont conservées entre les tâches, donc une connexion faite une fois sert aux tâches suivantes.
Deux précautions. Un lien interactif donne le contrôle de l'ordinateur à quiconque l'a : ne le sers qu'à un utilisateur authentifié dans ton app, avec un ttl_seconds court (60 à 86 400 s, 3 600 par défaut). Et quand le lien expire, redemande-en un.
Ce que ça coûte
Pas d'abonnement : un solde prépayé en euros, rechargé dans la console. Deux postes :
- L'ordinateur : 2 € de l'heure tant qu'il est allumé, facturé à la seconde. Une tâche de 3 minutes coûte donc 0,10 € d'ordinateur, plus le temps d'attente avant l'extinction : avec
idle_minutes: 2, compte 5 minutes, soit environ 0,17 €. AppellePOST /v1/agents/{id}/computer/stopdès que tu sais que l'agent a fini pour arrêter la facturation tout de suite. - Le modèle : les tokens consommés, au prix par million de tokens du modèle.
GET /v1/modelsdonneeur_per_million_input_tokenseteur_per_million_output_tokenspour plus de 300 modèles ; le modèle par défaut estanthropic/claude-haiku-5.5.
GET /v1/account renvoie ton balance_eur. Quand il est à zéro, les tâches s'arrêtent et l'API répond 402 avec le code insufficient_balance : prévois ce cas dans ton interface.
Aller plus loin : une app pour chaque produit
L'app ci-dessus est le socle. Chaque tutoriel de la série reprend les mêmes routes et ajoute ce qui fait l'idée du produit dont il s'inspire. Ce sont tes propres apps, pas des copies : aucune marque ni interface n'est reprise.
- Un clone de Grok Bot : une équipe d'agents toujours actifs (un chercheur, un rédacteur, un relecteur), ton serveur qui fait passer les messages et les fichiers de l'un à l'autre, un cron pour le travail récurrent et la reprise en main pour se connecter à un site.
- Un clone de Meta Muse : un agent personnel par utilisateur, chacun avec son ordinateur, qui continue à travailler quand l'app est fermée.
- Un clone des dots d'OpenAI : un agent principal que l'utilisateur nomme et personnalise, qui travaille en arrière-plan.
- Un clone de Manus : tu donnes une tâche, l'agent ouvre son ordinateur et revient avec un résultat et des fichiers à télécharger.
- Un clone d'OpenClaw : ton agent dans une messagerie comme Telegram, sans serveur d'agent à installer ni à sécuriser.
Pour passer de la démo à un produit : une table utilisateur → agent dans ta base (l'id de ton utilisateur aussi dans metadata), un flux d'événements par agent, GET /v1/agents/{id}/messages pour réafficher l'historique au rechargement (avec after pour ne lire que la suite), et POST /v1/agents/{id}/stop pour un bouton « Arrêter ». Garde en tête les limites : 600 requêtes par minute par IP (dont 60 créations d'agents et 120 envois de tâches) et 200 agents par compte. Et l'usage doit respecter les conditions : pas de spam, de fraude, de collecte abusive de données personnelles ni d'attaque de sites.
FAQ
Est-ce qu'il y a un SDK à installer ?
Non. C'est une API REST en JSON : fetch en Node, requests en Python ou curl suffisent. Le helper api() de ce tutoriel fait une dizaine de lignes.
Pourquoi passer par mon serveur pour les événements ?
Parce que le flux demande l'en-tête Authorization avec ta clé, que EventSource ne sait pas envoyer, et parce que la clé ne doit jamais se trouver dans du code envoyé au navigateur. Ton serveur sert de relais, et c'est aussi là que tu vérifies que l'utilisateur a le droit de voir cet agent.
Que se passe-t-il si une tâche dure plusieurs minutes ?
Rien de particulier dans cette app : on n'attend pas la réponse HTTP, on suit les événements. Si tu utilises wait: true (pratique pour un script), la réponse revient au plus tard après 120 s ; si status vaut working, continue avec GET /v1/agents/{id}/events ou relis la conversation avec after.
Où tournent les ordinateurs des agents ?
À Paris. Chaque agent a le sien, isolé de celui des autres, avec ses fichiers et ses sessions de navigateur conservés jusqu'à ce que tu le supprimes. Les messages, eux, partent chez l'éditeur du modèle choisi.
Pour la suite : la documentation de l'API, la console pour créer ta clé, la présentation de l'API, et notre comparatif Baguette Bot ou Grok Bot.