Créer un clone de Grok Bot : une équipe d'agents IA toujours actifs avec l'API Baguette Bot

Construis ta propre équipe d'agents IA en Node : un chercheur, un rédacteur et un relecteur, chacun avec son ordinateur, des fichiers qui circulent, un cron et la reprise en main.

Par l'équipe Baguette Bot 12 min de lecture
Clone Grok Bot
Sommaire
  1. L'idée de Grok Bot, et ce qu'on en reprend
  2. Ce qu'il te faut
  3. Étape 1 : créer l'équipe
  4. Étape 2 : parler à un agent, même quand c'est long
  5. Étape 3 : faire circuler les fichiers
  6. Étape 4 : le travail d'équipe
  7. Étape 5 : le travail récurrent avec un cron
  8. Étape 6 : l'écran en direct, et la reprise en main
  9. Ce que ça coûte
  10. Aller plus loin
  11. FAQ

On construit ici une petite équipe de trois agents IA qui travaille seule chaque lundi matin : un chercheur fait la veille sur le web et enregistre ses notes, un rédacteur en tire une newsletter, un relecteur la corrige, et le résultat t'arrive sur Telegram. Une page web te montre le journal de l'équipe et te laisse prendre la main sur l'ordinateur de n'importe quel agent, par exemple pour te connecter à un site à sa place. Le tout tient dans un server.js et un index.html, sur l'API Baguette Bot.

L'idée de Grok Bot, et ce qu'on en reprend

Grok Bot est le produit d'agents lancé en bêta par xAI (devenu SpaceXAI) en août 2026, conçu avec Cursor. Il promet une équipe d'agents toujours actifs : des bots qui se connectent aux sites comme une personne, se parlent entre eux, se passent le relais (un bot « Chief of Staff » peut coordonner des spécialistes), et continuent à travailler quand ton portable est fermé. Quand un site demande un mot de passe ou un captcha, tu reprends la main sur le navigateur.

Ce tutoriel ne copie pas Grok Bot : c'est ton app, avec la même idée. Et il y a une différence de conception à connaître dès le départ. Chez Grok Bot, les bots d'un utilisateur partagent un ordinateur. Avec l'API Baguette Bot, chaque agent a son propre ordinateur, isolé des autres. Les agents ne voient donc pas les fichiers de leurs collègues : c'est ton serveur qui fait circuler le travail, messages et fichiers. Il joue le rôle du coordinateur.

Ce qu'il te faut

  • Node 18 ou plus récent.
  • Une clé API créée dans la console (1 € offert une fois ton e-mail vérifié, de quoi faire quelques tours de l'équipe).
  • En option, un bot Telegram pour recevoir le résultat : crée-le avec @BotFather, qui te donne un jeton, et récupère l'identifiant de ta conversation avec lui.
bash
mkdir mon-equipe && cd mon-equipe
npm init -y
npm pkg set type=module
npm i express node-cron
mkdir public
export BAGUETTE_API_KEY=bb_live_…
export TOPIC="l'IA en Europe"
# optionnel
export TELEGRAM_TOKEN=123456:ABC… TELEGRAM_CHAT_ID=123456789

Étape 1 : créer l'équipe

Trois agents, trois rôles. Chacun reçoit des instructions permanentes et un champ metadata qui dit à quelle équipe il appartient. On garde leurs identifiants dans team.json pour ne pas les recréer à chaque démarrage (tu as droit à 200 agents par compte).

Le début de server.js :

javascript
// server.js
import express from "express";
import cron from "node-cron";
import { readFile, writeFile } from "node:fs/promises";

const KEY = process.env.BAGUETTE_API_KEY;
const BASE = "https://api.baguettebot.com";
const TOPIC = process.env.TOPIC ?? "l'IA en Europe";
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

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;
  });

const ROLES = {
  chercheur: { name: "Chercheur", idle_minutes: 2, instructions:
    "Tu fais de la veille sur le web avec Chrome. Tu vérifies chaque information à la source et tu notes toujours son URL. " +
    "Si un site te demande de te connecter, n'invente pas d'identifiants : dis-le dans ta réponse." },
  redacteur: { name: "Rédacteur", idle_minutes: 2, instructions:
    "Tu écris des newsletters claires et sobres en français. Tu n'utilises que les faits des notes qu'on te donne, avec leurs liens." },
  relecteur: { name: "Relecteur", idle_minutes: 1, instructions:
    "Tu relis des textes. S'il est prêt à être publié, réponds uniquement VALIDÉ. Sinon, liste les corrections nécessaires, numérotées, sans réécrire le texte." },
};

async function loadTeam() {
  let team = {};
  try { team = JSON.parse(await readFile("team.json", "utf8")); } catch {}
  for (const [role, def] of Object.entries(ROLES)) {
    if (!team[role]) team[role] = (await api("/v1/agents", { ...def, metadata: { team: "newsletter", role } })).id;
  }
  await writeFile("team.json", JSON.stringify(team, null, 2));
  return team;
}
const team = await loadTeam();

const journal = [];
const note = (text) => { journal.push(`${new Date().toLocaleTimeString("fr-FR")}  ${text}`); console.log(text); };

Tous les agents utilisent ici le modèle par défaut, anthropic/claude-haiku-5.5. Rien ne t'empêche de donner un modèle différent à chaque rôle (un champ model de plus, choisi dans GET /v1/models) : un modèle plus solide pour le chercheur, un modèle rapide pour le relecteur.

Étape 2 : parler à un agent, même quand c'est long

Une recherche sur le web peut dépasser les 120 s que wait: true accepte d'attendre. Dans ce cas, la réponse revient avec status: "working". La fonction ask() gère ce cas : elle surveille le statut de l'agent, puis lit la suite de la conversation à partir du message envoyé, grâce au paramètre after.

javascript
// Une tâche, et la réponse finale de l'agent, même au-delà de 120 s
async function ask(role, content) {
  const id = team[role];
  note(`${ROLES[role].name} : au travail`);
  const run = await api(`/v1/agents/${id}/messages`, { content, wait: true });
  if (run.status === "done") return run.output;

  for (let i = 0; i < 360; i++) { // 30 minutes au plus
    await sleep(5000);
    const agent = await api(`/v1/agents/${id}`);
    if (agent.status === "working") continue;
    if (agent.status === "blocked") { note(`${ROLES[role].name} est bloqué : ${agent.status_text ?? "ouvre son écran"}`); continue; }
    const page = await api(`/v1/agents/${id}/messages?after=${run.message_id}&limit=200`);
    const messages = page.data ?? page;
    return messages.filter((m) => m.role === "assistant" && m.content).at(-1)?.content ?? "";
  }
  throw new Error(`${ROLES[role].name} : pas de réponse après 30 minutes`);
}

Si tu préfères le temps réel au sondage, le flux GET /v1/agents/{id}/events donne la même information sans attendre : l'événement status passe à idle quand l'agent a fini. Le tutoriel de base montre comment le lire et le relayer.

Étape 3 : faire circuler les fichiers

Le chercheur enregistre ses notes sur son ordinateur. Le rédacteur ne peut pas les lire directement : ton serveur les télécharge chez l'un et les dépose chez l'autre. Le téléchargement renvoie le fichier en binaire, d'où un fetch direct plutôt que le helper JSON.

javascript
async function download(role, path) {
  const r = await fetch(`${BASE}/v1/agents/${team[role]}/files?path=${encodeURIComponent(path)}`, {
    headers: { Authorization: `Bearer ${KEY}` },
  });
  if (!r.ok) throw new Error(`${path} introuvable chez ${ROLES[role].name}`);
  return Buffer.from(await r.arrayBuffer());
}
const upload = (role, path, buffer) =>
  api(`/v1/agents/${team[role]}/files`, { path, content_base64: buffer.toString("base64") });

// Éteindre un ordinateur dès qu'on n'en a plus besoin arrête la facturation
const stopComputer = (role) => api(`/v1/agents/${team[role]}/computer/stop`, {}).catch(() => {});

Ces deux routes allument l'ordinateur s'il est éteint, et un fichier peut faire jusqu'à environ 30 Mo : un PDF, un tableur ou une capture passent de la même façon.

Étape 4 : le travail d'équipe

C'est ici que ton serveur devient le coordinateur. Il passe les notes du chercheur au rédacteur, puis fait des allers-retours entre le rédacteur et le relecteur jusqu'à ce que le texte soit validé (deux tours au plus, pour ne pas boucler). À la fin, il enregistre la newsletter et te prévient.

javascript
async function notify(text) {
  const { TELEGRAM_TOKEN, TELEGRAM_CHAT_ID } = process.env;
  if (!TELEGRAM_TOKEN) return;
  await fetch(`https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ chat_id: TELEGRAM_CHAT_ID, text: text.slice(0, 4000) }),
  });
}

let running = false;
async function runNewsletter() {
  if (running) return note("Déjà en cours, on saute ce tour");
  running = true;
  try {
    await ask("chercheur",
      `Trouve les 5 informations les plus importantes de la semaine sur ${TOPIC}. ` +
      "Pour chacune : un titre, trois lignes de résumé et l'URL de la source. Enregistre le tout dans ~/Documents/notes.md.");
    const notes = await download("chercheur", "~/Documents/notes.md");
    await stopComputer("chercheur");
    await upload("redacteur", "~/Uploads/notes.md", notes);

    let draft = await ask("redacteur",
      "Les notes du chercheur sont dans ~/Uploads/notes.md. Écris la newsletter de la semaine " +
      "(500 mots au plus, en Markdown) et réponds uniquement avec son texte.");
    for (let round = 1; round <= 2; round++) {
      const review = await ask("relecteur", `Relis cette newsletter :\n\n${draft}`);
      if (review.trim().startsWith("VALIDÉ")) { note("Relecteur : validé"); break; }
      draft = await ask("redacteur",
        `Le relecteur demande ces corrections :\n\n${review}\n\nApplique-les et réponds uniquement avec le texte corrigé.`);
    }
    await Promise.all(["redacteur", "relecteur"].map(stopComputer));

    await writeFile(`newsletter-${new Date().toISOString().slice(0, 10)}.md`, draft);
    await notify(`Newsletter prête :\n\n${draft}`);
    note("Newsletter prête");
  } catch (e) {
    note(`Erreur : ${e.message}`);
    await notify(`La newsletter a échoué : ${e.message}`).catch(() => {});
  } finally {
    running = false;
  }
}

Le message du relecteur n'est pas transmis tel quel par magie : c'est une simple chaîne que ton code relit (VALIDÉ ou une liste) puis renvoie au rédacteur. C'est toute la logique du routage entre agents. Tu peux la rendre aussi riche que tu veux : un quatrième agent qui décide qui doit intervenir, des équipes en parallèle avec Promise.all, ou un humain qui valide avant l'envoi.

Étape 5 : le travail récurrent avec un cron

L'API n'a pas de planificateur : c'est ton serveur qui décide quand l'équipe travaille. Avec node-cron, une ligne suffit, ici tous les lundis à 8 h, heure de Paris :

javascript
cron.schedule("0 8 * * 1", runNewsletter, { timezone: "Europe/Paris" });

Ton serveur doit donc tourner en continu (un petit VPS, un conteneur, un service systemd). Si tu ne veux pas d'un processus permanent, sors runNewsletter() dans un script et lance-le avec un timer systemd ou une GitHub Action planifiée : les agents et leurs fichiers, eux, restent sur leurs ordinateurs entre deux tours.

Étape 6 : l'écran en direct, et la reprise en main

Dernière brique : une page qui affiche le journal et qui te laisse prendre la main sur l'ordinateur d'un agent. Le cas typique : le chercheur doit lire un site qui demande une connexion. Tu ouvres son écran, tu te connectes toi-même dans son Chrome, et comme les sessions du navigateur sont conservées entre les tâches, il restera connecté aux tours suivants. Tes identifiants ne passent jamais par le modèle ni par ton code.

La fin de server.js :

javascript
const route = (fn) => (req, res) => fn(req, res).catch((e) => res.status(500).json({ error: e.message }));
const app = express();
app.use(express.static("public"));

app.get("/api/state", (req, res) => res.json({ running, journal: journal.slice(-30) }));
app.post("/api/run", (req, res) => { runNewsletter(); res.json({ ok: true }); });

// Un lien interactif vers l'écran d'un agent (on allume l'ordinateur si besoin)
app.post("/api/live/:role", route(async (req, res) => {
  const id = team[req.params.role];
  if (!id) return res.status(404).json({ error: "Rôle inconnu" });
  if ((await api(`/v1/agents/${id}/computer`)).status !== "running") {
    await api(`/v1/agents/${id}/computer/start`, {});
    for (let i = 0; i < 30 && (await api(`/v1/agents/${id}/computer`)).status !== "running"; i++) await sleep(2000);
  }
  const live = await api(`/v1/agents/${id}/computer/live`, { interactive: true, ttl_seconds: 900 });
  res.json({ url: live.url });
}));

// Uniquement en local : un lien interactif donne le contrôle de l'ordinateur
app.listen(3000, "127.0.0.1", () => console.log("http://localhost:3000"));

Et la page :

html
<!-- public/index.html -->
<!doctype html>
<html lang="fr">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Mon équipe</title>
<style>
  body { font: 15px/1.5 system-ui, sans-serif; max-width: 1000px; margin: 0 auto; padding: 16px; }
  button { padding: 6px 12px; margin: 2px; }
  iframe { width: 100%; aspect-ratio: 16 / 10; border: 0; border-radius: 12px; background: #111; margin: 12px 0; }
  pre { background: #f3f3f3; padding: 12px; border-radius: 10px; white-space: pre-wrap; }
</style>
<body>
  <h1>Mon équipe d'agents</h1>
  <p><button id="run">Lancer la newsletter maintenant</button></p>
  <p>Prendre la main sur l'ordinateur du
    <button data-role="chercheur">chercheur</button>
    <button data-role="redacteur">rédacteur</button>
    <button data-role="relecteur">relecteur</button>
  </p>
  <iframe id="screen" title="Écran de l'agent"></iframe>
  <pre id="journal">…</pre>
<script>
  document.getElementById("run").onclick = () => fetch("/api/run", { method: "POST" });
  for (const b of document.querySelectorAll("[data-role]")) {
    b.onclick = async () => {
      b.disabled = true;
      const r = await fetch("/api/live/" + b.dataset.role, { method: "POST" }).then((x) => x.json());
      b.disabled = false;
      if (r.url) document.getElementById("screen").src = r.url; else alert(r.error);
    };
  }
  setInterval(async () => {
    const s = await fetch("/api/state").then((x) => x.json());
    document.getElementById("journal").textContent = (s.running ? "En cours…\n" : "") + s.journal.join("\n");
  }, 3000);
</script>
</body>
</html>

Lance node server.js, ouvre http://localhost:3000, clique sur « chercheur » : son bureau apparaît, tu peux cliquer et taper dedans. Puis « Lancer la newsletter maintenant » et suis le journal. Si l'ordinateur s'éteint pendant que tu te connectes à un site, augmente son idle_minutes avec PATCH /v1/agents/{id}.

Le serveur n'écoute que sur ta machine, et c'est voulu. Pour le mettre en ligne, ajoute d'abord une vraie authentification devant /api/live : quiconque obtient un lien interactif contrôle la souris et le clavier de l'agent tant que le lien est valide.

Ce que ça coûte

L'ordinateur coûte 2 € de l'heure tant qu'il est allumé, facturé à la seconde ; le modèle, les tokens au prix affiché par GET /v1/models. Les durées dépendent du sujet et des sites ; prenons un exemple : 6 minutes pour le chercheur, 5 minutes à eux deux pour le rédacteur et le relecteur. Ça fait 11 minutes d'ordinateur, environ 0,37 €, plus quelques minutes avant extinction quand on n'a pas encore appelé computer/stop. Avec un tour par semaine, compte autour de 1,50 € d'ordinateur par mois, plus les tokens des trois modèles. GET /v1/account te donne le solde restant, et la console détaille ce que coûte chaque agent. Si le solde est vide, l'API répond 402 (insufficient_balance) et ton catch te prévient sur Telegram.

Aller plus loin

  • Une équipe par client. Crée un trio par client avec metadata: { customer_id } et une ligne par équipe dans ta base ; attention à la limite de 60 créations d'agents par minute et de 200 agents par compte.
  • Un coordinateur IA. Remplace la boucle fixe par un quatrième agent qui reçoit l'état du travail et répond avec le prochain rôle à solliciter, que ton code applique.
  • Plus de canaux. Un webhook Slack ou ton service de push à la place de Telegram : l'API ne livre pas de notifications elle-même, et ses agents n'envoient pas d'e-mails.
  • Suivre en direct. Relaie le flux d'événements de chaque agent vers la page, comme dans le tutoriel de base, pour voir les réponses s'écrire.

Et garde l'usage dans les clous des conditions : pas de spam, de fraude, de collecte abusive de données personnelles ni de scraping agressif.

FAQ

Pourquoi ne pas faire partager un ordinateur aux trois agents ?

Avec l'API, chaque agent a le sien, isolé. C'est plus de transferts à écrire, mais un agent qui se trompe ou lit une page piégée ne touche pas aux fichiers et sessions des autres. Les transferts passent par GET et POST /v1/agents/{id}/files.

Les agents peuvent-ils se parler directement ?

Non, pas par l'API : ils se parlent à travers ton serveur, qui lit la réponse de l'un et l'envoie à l'autre avec POST /v1/agents/{id}/messages. C'est aussi ce qui te permet de journaliser, filtrer ou valider chaque échange.

Est-ce que l'équipe travaille quand mon ordinateur est éteint ?

Les agents travaillent sur leurs ordinateurs cloud, à Paris. Ce qui doit rester allumé, c'est ton serveur qui lance le cron. Sur un petit VPS, c'est « toujours actif » au sens de Grok Bot.

Que se passe-t-il si une tâche dure plus de 120 s ?

wait: true rend la main avec status: "working". La fonction ask() de l'étape 2 attend alors que l'agent repasse à idle et lit la suite avec after.

Pour la suite : la documentation de l'API, la console, la présentation de l'API, le tutoriel de base Créer ta propre app d'agents IA et notre comparatif Baguette Bot ou Grok Bot.

PartagerXLinkedInReddit