Créez un outil de support qui prend un identifiant de paiement et répond à la question "pourquoi ce paiement a-t-il échoué ?" à partir de votre API de paiements et de votre page de statut. L'agent ne peut que lire : il ne peut rien rembourser, réessayer ni modifier. La mise en place prend environ quinze minutes.
Une exécution de ce type dure généralement de 10 à 15 secondes. L'essentiel du coût est le calcul, facturé 0,0002 $ par seconde, et chaque page que l'agent récupère coûte 0,001 $. La part du modèle dépend du modèle que vous choisissez : lisez donc cost_micros sur vos premières exécutions. Le budget de 0,50 $ est un plafond, pas un prix.
Rassembler ce qu'il vous faut
- Un compte Ephemeral avec des crédits, et deux clés API depuis Clés API : une clé membre pour votre backend, qui lance les exécutions, et une clé administrateur pour la configuration unique de la connexion. Voir rôles et clés.
- L'URL de base de votre API de paiements (HTTPS, accessible depuis Internet) et une clé en lecture seule pour celle-ci, par exemple une clé restreinte qui ne peut que lire les paiements.
- Une page de statut publique sur un seul domaine, ici
status.example.com. - Un point de terminaison HTTPS sur votre serveur, qui recevra le résultat.
export EPHEMERAL_URL=https://app.ephemer.al export EPHEMERAL_API_KEY=eak_… # member key, for your backend export EPHEMERAL_ADMIN_KEY=eak_… # admin key, for setup only export EPHEMERAL_CALLBACK_SECRET=$(openssl rand -hex 16) # 32 characters; keep it on your serverÉcrire et téléverser le playbook
Le playbook contient le savoir-faire de votre équipe : quels appels effectuer et comment lire la réponse. Gardez-le court et concret. L'agent le reçoit sous forme de fichier en lecture seule, à côté de la tâche.
# Failed payment playbook You can only read. The payments connection allows GET, and web access is limited to status.example.com. 1. GET /payments/{payment_id}. Note status, amount, currency, failure_code and failure_message. 2. GET /payments/{payment_id}/attempts. Check whether earlier attempts failed for the same reason. 3. If failure_code is processor_error or timeout, read https://status.example.com for incidents around that time. 4. Decide the cause: - card_declined, insufficient_funds, expired_card: the customer's bank or card. Suggest a retry or another card. - processor_error, timeout: ours or the processor's. Say whether the status page shows an incident. - fraud_review: a risk rule held it. Do not suggest bypassing it. 5. Never guess. If a call fails or a field is missing, say so. Answer in at most six lines: the cause, the evidence (field and value) and the next step for the support agent. Do not include card numbers or customer email addresses.curl $EPHEMERAL_URL/v1/files \ -H "Authorization: Bearer $EPHEMERAL_API_KEY" \ -F [email protected]Le fichier est enregistré sous le nom
playbook.md. Le téléverser à nouveau ajoute une nouvelle version, et les workflows qui désignent ce fichier utilisent la plus récente au démarrage d'une exécution. Voir fichiers.Créer la connexion payments-api
Une connexion conserve l'adresse et la clé de l'API à l'intérieur d'Ephemeral. L'agent l'appelle par son nom et ne voit jamais la clé. Définissez
allowed_methodssur["GET"]pour que toute autre méthode soit refusée.curl -X PUT $EPHEMERAL_URL/v1/connections/payments-api \ -H "Authorization: Bearer $EPHEMERAL_ADMIN_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "http", "url": "https://api.example.com/v1", "auth": { "type": "bearer", "token": "rk_live_…" }, "allowed_methods": ["GET"], "instructions": "Look up a payment with GET /payments/{id} and its attempts with GET /payments/{id}/attempts. Amounts are in cents." }'Vous préférez le tableau de bord ? Ouvrez Connexions, choisissez Ajouter une connexion puis API HTTP, saisissez le nom
payments-apiet l'URL de base, choisissez Jeton Bearer et collez la clé, puis sélectionnez uniquementGETsous Méthodes HTTP autorisées. Lesinstructionsindiquent à l'agent quels points de terminaison existent ; elles lui sont montrées, donc n'y mettez aucun secret.Relire la connexion n'affiche aucun jeton, et
GET /v1/catalog/connectionsla liste aux membres avec son type et ses méthodes autorisées, sans l'URL ni aucun identifiant.Enregistrer le workflow
Un workflow regroupe la tâche, le playbook, la connexion et les limites sous un seul nom, si bien que votre backend n'envoie que l'identifiant du paiement. L'agent voit la connexion sous le nom
payments. L'accès web est limité à la page de statut, et le shell est désactivé car la tâche n'a besoin d'aucune commande.curl -X PUT $EPHEMERAL_URL/v1/workflows/investigate-payment \ -H "Authorization: Bearer $EPHEMERAL_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "prompt": "Find out why this payment failed.", "files": [{ "file": "playbook.md" }], "connections": { "payments": { "ref": "payments-api", "allowed_methods": ["GET"] } }, "web": { "allow": ["status.example.com"] }, "shell": false, "timeout_seconds": 120 }'Aucun
modeln'est défini : chaque exécution utilise donc le modèle par défaut au moment où elle démarre ; choisissez-en un autre dans le catalogue de modèles si vous le souhaitez. L'identifiant du paiement, le budget et le callback appartiennent à chaque exécution, pas au workflow.Lancer une exécution depuis votre backend
Envoyez le nom du workflow, l'identifiant du paiement, le budget et un callback. L'
Idempotency-Keyrend une nouvelle tentative sans risque : une clé répétée renvoie l'exécution déjà existante avec200au lieu d'en lancer une seconde. Utilisez une clé que vous pouvez reconstruire, comme l'identifiant du paiement ou celui de votre ticket.const paymentId = "pay_123"; const response = await fetch(`${process.env.EPHEMERAL_URL}/v1/runs`, { method: "POST", headers: { Authorization: `Bearer ${process.env.EPHEMERAL_API_KEY}`, "Content-Type": "application/json", "Idempotency-Key": `investigate-${paymentId}`, }, body: JSON.stringify({ workflow: "investigate-payment", input: { payment_id: paymentId }, max_cost_micros: 500000, callback: { url: "https://example.com/hooks/ephemeral", secret: process.env.EPHEMERAL_CALLBACK_SECRET, }, }), }); const run = await response.json(); // { id: "run_…", status: "queued", … }La réponse est
202 Acceptedavec l'objet d'exécution, au statutqueued. L'exécution démarre dès qu'un worker est libre. L'URL du callback doit utiliser HTTPS et pointer vers une adresse publique, et un secret doit comporter de 24 à 1 024 caractères ; sinon la requête échoue avec400et rien n'est lancé. Sur le service hébergé,402signifie que vos crédits ne couvrent pas le budget.Vérifier le callback
Lorsque l'exécution atteint un statut final, Ephemeral envoie un
POSTsigné avec l'objet d'exécution. Vérifiez la signature sur les octets bruts du corps avant de faire confiance à quoi que ce soit. Le texte signé est l'en-têteX-Ephemeral-Timestamp, un point et le corps, signés en HMAC-SHA256 avec votre secret. Voir callbacks pour le schéma complet.import crypto from "node:crypto"; import http from "node:http"; const handled = new Set(); // use your database in production http .createServer((req, res) => { const chunks = []; req.on("data", (chunk) => chunks.push(chunk)); req.on("end", () => { const body = Buffer.concat(chunks); const timestamp = req.headers["x-ephemeral-timestamp"] ?? ""; const signature = req.headers["x-ephemeral-signature"] ?? ""; const expected = "sha256=" + crypto .createHmac("sha256", process.env.EPHEMERAL_CALLBACK_SECRET) .update(timestamp + ".") .update(body) .digest("hex"); const valid = signature.length === expected.length && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)); const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300; if (!valid || !fresh) return res.writeHead(401).end(); const run = JSON.parse(body); if (!handled.has(run.id)) { handled.add(run.id); // Attach run.output to the support ticket for run.id here. } res.writeHead(204).end(); // any 2xx counts as delivered }); }) .listen(3000); // serve this behind HTTPS at /hooks/ephemeralToute réponse autre que
2xxest retentée avec des délais croissants : la même exécution peut donc arriver deux fois. C'est pourquoi le gestionnaire mémorise les ID d'exécution.Lire le résultat
Le corps du callback est l'objet d'exécution. Pour le paiement d'exemple, il ressemble à ceci :
{ "id": "run_118622d16f00105f7bba6d2281774705", "status": "succeeded", "output": "- **Cause:** The customer’s bank declined the payment for insufficient funds.\n- **Evidence:** `failure_code: insufficient_funds`; `failure_message: The card issuer declined the charge: insufficient funds.`\n- **Attempts:** Both attempts failed for the same reason.\n- **Next step:** Suggest the customer retry after resolving the balance issue or use another card.", "created_at": "2026-10-03T09:14:05.118Z", "started_at": "2026-10-03T09:14:05.402Z", "finished_at": "2026-10-03T09:14:16.447Z", "max_cost_micros": 500000, "cost_micros": 2618, "billing_pending": false, "zero_data_retention": false }outputest la réponse de l'agent sous forme de texte.cost_microsest le coût total en millionièmes de dollar, ici 0,002618 $. Une exécution qui n'a pas réussi a lestatusfailed,timed_outoucancelledet uneerrorau lieu d'un résultat ; montrez cela à votre agent plutôt que rien. Sibilling_pendingvauttrue, le coût est encore provisoire : relisez l'exécution avecGET /v1/runs/{id}un peu plus tard. Pour voir chaque appel effectué par l'agent, ouvrez l'exécution dans le tableau de bord ou diffusez ses événements.
Pourquoi c'est sûr
| Risque | Ce qui l'empêche |
|---|---|
| L'agent modifie ou rembourse un paiement. | La connexion et le workflow n'autorisent que GET ; toute autre méthode est refusée avec HTTP method not permitted. |
| La clé de paiements fuite. | La clé reste dans Ephemeral. La microVM n'a ni réseau ni clé ; le broker ajoute la clé à chaque requête et retire les identifiants des réponses. |
| Des données partent vers un site inconnu. | L'accès web n'autorise que status.example.com, et les redirections sont vérifiées par rapport à la même liste. Voir accès web. |
| L'agent exécute des commandes. | shell is false. |
| Une exécution coûte plus que prévu. | Le budget de 500 000 micros (0,50 $) est réservé au départ et ne peut pas être dépassé ; timeout_seconds met fin à l'exécution après 120 secondes. |
| Quelqu'un falsifie le résultat. | Votre point de terminaison n'accepte que les requêtes dont la signature est valide. |