支払い ID を受け取り、決済 API とステータスページをもとに「この支払いはなぜ失敗したのか」に答えるサポートツールを作ります。エージェントは読み取りしかできません。返金も再試行も、何かを変更することもできません。セットアップにかかる時間は約 15 分です。
このような実行は、たいてい 10 〜 15 秒で終わります。費用の大半はコンピュートで、1 秒あたり $0.0002 で課金されます。エージェントが取得するページは 1 件につき $0.001 かかります。モデルの利用料はそのモデル自体の価格で課金され、選んだモデルによって変わるため、最初の数回の実行で cost_micros を確認してください。予算の $0.50 は上限であり、実際の料金ではありません。
必要なものを用意する
- クレジットのある Ephemeral アカウントと、API キーで作成する 2 つの API キー。実行を開始するバックエンド用のメンバーキーと、接続を 1 回だけセットアップするための管理者キーです。ロールとキーを参照してください。
- 決済 API のベース URL (HTTPS で、インターネットから到達できるもの) と、その読み取り専用キー。たとえば、支払いの読み取りしかできない制限付きキーです。
- 1 つのドメインで公開されているステータスページ。ここでは
status.example.comとします。 - 結果を受け取る、サーバー上の HTTPS エンドポイント。
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プレイブックを書いてアップロードする
プレイブックには、チームの知識を書きます。どの呼び出しを行い、返ってきた結果をどう読むかです。短く具体的にまとめてください。エージェントは、タスクと一緒に読み取り専用のファイルとして受け取ります。
# 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]ファイルは
playbook.mdという名前で保存されます。もう一度アップロードすると新しいバージョンが追加され、このファイル名を指定しているワークフローは、実行の開始時に最新のバージョンを使います。ファイルを参照してください。payments-api 接続を作成する
接続は、API のアドレスとキーを Ephemeral の内部に保管します。エージェントは名前で呼び出すだけで、キーを見ることはありません。
allowed_methodsを["GET"]に設定すると、それ以外のメソッドはすべて拒否されます。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." }'ダッシュボードで設定する場合は、接続を開き、接続を追加から HTTP API を選びます。名前に
payments-api、ベース URL を入力し、Bearer トークン を選んでキーを貼り付け、許可する HTTP メソッドではGETだけを選択します。instructionsは、どのエンドポイントがあるかをエージェントに伝えるものです。エージェントに表示されるため、機密情報は含めないでください。接続を読み戻してもトークンは表示されません。
GET /v1/catalog/connectionsは、メンバーに対して接続の種類と許可するメソッドを一覧表示しますが、URL や認証情報は含まれません。ワークフローを保存する
ワークフローは、タスク、プレイブック、接続、制限を 1 つの名前にまとめて保存するため、バックエンドは支払い ID を送るだけで済みます。エージェントからは、接続は
paymentsとして見えます。Web アクセスはステータスページに限定し、このタスクにコマンドは不要なのでシェルはオフにします。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 }'modelは設定していないため、各実行は開始時点のデフォルトモデルを使います。別のモデルを使いたい場合は、モデルカタログから選んでください。支払い ID、予算、コールバックはワークフローではなく、各実行に属します。バックエンドから実行を開始する
ワークフロー名、支払い ID、予算、コールバックを送ります。
Idempotency-Keyにより、再試行しても安全です。同じキーを再送すると、2 つ目の実行は開始されず、既存の実行が200で返ります。支払い ID やチケット ID など、再現できるキーを使ってください。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", … }レスポンスは
202 Acceptedで、queued状態の実行オブジェクトが返ります。実行は、ワーカーが空き次第開始されます。コールバック URL は HTTPS で、公開アドレスに解決される必要があり、シークレットは 24 ~ 1,024 文字でなければなりません。条件を満たさないとリクエストは400で失敗し、何も開始されません。ホスト型サービスでは、402はクレジットが予算に足りないことを意味します。コールバックを検証する
実行が最終ステータスに達すると、Ephemeral は実行オブジェクトを含む署名付きの
POSTを 1 回送信します。内容を信用する前に、ボディの生のバイト列に対して署名を確認してください。署名の対象は、X-Ephemeral-Timestampヘッダー、ピリオド、ボディをこの順に連結した文字列で、シークレットを使って HMAC-SHA256 で署名されます。 詳しい方式はコールバックを参照してください。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/ephemeral2xx以外の応答は、間隔を延ばしながら再試行されるため、同じ実行が 2 回届くことがあります。ハンドラーが実行 ID を記憶しているのは、そのためです。結果を読む
コールバックのボディは実行オブジェクトです。例の支払いでは、次のようになります。
{ "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 }outputは、エージェントの回答をテキストにしたものです。cost_microsは費用全体を 100 万分の 1 ドル単位で表した値で、ここでは $0.002618 です。成功しなかった実行は、statusがfailed、timed_out、cancelledのいずれかで、出力の代わりにerrorを持ちます。何も表示しないのではなく、その内容をサポート担当者に見せてください。billing_pendingがtrueの場合、費用はまだ暫定です。少し待ってからGET /v1/runs/{id}でもう一度実行を取得してください。エージェントが行った各呼び出しを確認するには、ダッシュボードで実行を開くか、その イベントをストリーミングします。
安全である理由
| リスク | 防ぐ仕組み |
|---|---|
| エージェントが支払いを変更または返金する。 | 接続とワークフローの両方が GET のみを許可し、それ以外のメソッドは HTTP method not permitted で拒否されます。 |
| 決済 API のキーが漏えいする。 | キーは Ephemeral の内部にとどまります。microVM にはネットワークもキーもありません。ブローカーが各リクエストにキーを付与し、レスポンスから認証情報を取り除きます。 |
| データが未知のサイトに送られる。 | Web アクセスは status.example.com のみを許可し、リダイレクトも同じリストと照合されます。Web アクセスを参照してください。 |
| エージェントがコマンドを実行する。 | shell が false です。 |
| 実行の費用が想定を超える。 | 予算の 500,000 マイクロ ($0.50) は開始時に確保され、超えることはありません。timeout_seconds により、120 秒で実行が終了します。 |
| 誰かが結果を偽造する。 | エンドポイントは、署名が検証できたリクエストだけを受け付けます。 |