x402の実装で二重払いを防ぐ|AIエージェント決済の上限とTypeScript例

コラム

/約20分で読めます

コラム

/約20分

x402の実装で二重払いを防ぐ|AIエージェント決済の上限とTypeScript例
目次(タップで折りたたみ)

    ある会社の開発チームが、社内の調査用AIエージェントに、外部の有料APIの代金を自動で払わせることになりました。使うのはx402です。HTTPの402 Payment Requiredで支払条件を受け取り、ステーブルコインで払って、同じ要求を送り直す。公式のサンプルを短くすれば、支払いまで通る構成はすぐに作れます。

    ところが、このまま本番に出すと困ることが2つあります。

    • エージェントが、APIに言われるまま署名してしまう。1回あたりの上限を決めても、0.01ドルを1万回払う暴走は止まらない。
    • タイムアウトのたびに新しい支払に署名し直すと、同じ依頼に2回払ってしまう。

    しかもexact方式の支払は、実行した後では原則として取り消せません。だから守りどころは2つです。署名する前に、支払先・資産・金額を自社のルールで絞ること。そして、結果が分からないときに払い直さない仕組みを持つことです。以下では、この開発チームが作る順に、x402 v2の流れ、最小の実装、署名前の確認、再送対策、状態の管理、監査の記録を組み立てていきます。

    AIエージェント決済の市場やプロトコル全体の見取り図は、AIエージェント決済とは?仕組み・主要プロトコル・企業の備えにまとめています。

    この記事でわかること

    • x402 v2の通信の流れと、TypeScriptの最小構成
    • 署名の前に支払先・金額を確かめる方法と、タイムアウト後に2回払わない方法
    • 処理と決済の状態の管理、監査の台帳、本番前のチェックリスト

    この記事で使う言葉

    • resource server:代金を受け取る有料API側のサーバー
    • Facilitator:支払の検証と決済確定を代わりに行うサービス
    • scheme(支払方式)/exact:支払のやり方の種類。exactは、提示された額をそのまま払う方式
    • プッシュ型支払:払う側が自分の資産を相手へ送り出す支払。実行後は払う側から取り消せない
    • CAIP-2:チェーンの種類と番号でネットワークを表す書き方(例:eip155:8453)

    x402 v2では、402からAPIの応答までがどう進むか

    x402は、HTTPの402 Payment Requiredを支払条件の提示に使うオープンなプロトコルです。公式のx402 payment flowに沿うと、通信は次の順で進みます。

    1. エージェントが、保護されたAPIへ普通のHTTP要求を送る。
    2. 有料API側のサーバー(resource server)が、402とPAYMENT-REQUIREDを返す。支払候補のacceptsには、scheme、network、asset(支払資産)、amount、payTo(受取アドレス)などが入る。
    3. エージェントが候補を選ぶ。署名の前に自社の支払ルールと突き合わせ、許可されたときだけ署名担当のウォレットへ渡す。
    4. エージェントが、署名済みの支払データをPAYMENT-SIGNATUREに入れて、同じAPI要求を送り直す。
    5. API側のサーバーが、自分で、またはFacilitator経由で、署名と支払条件を検証する。
    6. 検証に通ると、サーバーがAPIの処理と決済確定を進める。確定した応答には、結果の本体とPAYMENT-RESPONSEが入る。

    この流れのとおり、x402が決めているのは通信の手順です。価格の決め方や上限、重複の防止は、導入する側が足します。

    要素x402が扱うこと導入側で足すこと
    402応答支払候補と、呼び出し先APIの情報を伝える価格の決め方、価格の有効期限、利用条件
    署名済みの支払データ支払方式に沿った支払の承認(authorization)支払先の許可リスト、金額の上限、社内承認、鍵の失効
    検証・決済確定(verify / settle)署名・残高・条件の検証と、オンチェーンへの送信Facilitator障害時の扱い、突き合わせ、返金・利用クレジットの方針
    HTTPの再送支払データ付きでAPIを再要求する業務の要求の冪等性(何度送っても1回分だけ処理されること)、応答の保存、重複実行の防止
    決済の記録トランザクションハッシュ等の決済結果注文・原価・勘定との紐付け、監査の記録、保存期間

    v1の記事やサンプルを見るときは、次の点が違います。v1とv2を混ぜないでください(x402 Foundationのv1→v2移行ガイド)。

    • ヘッダー名:v2はX-PAYMENTではなくPAYMENT-SIGNATURE。応答はX-PAYMENT-RESPONSEではなくPAYMENT-RESPONSE
    • ネットワークIDの書き方:v2はCAIP-2形式(例:Base Mainnetはeip155:8453)
    • パッケージ名:v1のx402-expressから、v2の@x402/expressなどへ分割・変更

    支払データの形式も違うので、あわせて混ぜないようにします。なお、公式リポジトリは2026年9月24日時点でx402-foundation/x402へ移っています。coinbase/x402は開発用のフォークという位置づけです。

    最小の構成で、有料APIと支払クライアントを動かす

    次は、公式リポジトリのTypeScript examplesを短くした検証用の構成です。テスト用ネットワークのBase Sepolia(eip155:84532)を使います。API側は、1回0.001ドルの支払を求めます。

    ウォレットの秘密鍵、Facilitatorの認証情報、受取アドレスは、環境変数や秘密情報の管理サービスで扱います。コードやログには埋め込みません。

    // resource-server.ts
    import express from "express";
    import { paymentMiddleware, x402ResourceServer } from "@x402/express";
    import { ExactEvmScheme } from "@x402/evm/exact/server";
    import { HTTPFacilitatorClient } from "@x402/core/server";
    
    const facilitator = new HTTPFacilitatorClient({
      url: process.env.FACILITATOR_URL!,
    });
    const server = new x402ResourceServer(facilitator)
      .register("eip155:84532", new ExactEvmScheme());
    
    const app = express();
    app.use(paymentMiddleware({
      "POST /v1/research": {
        accepts: [{
          scheme: "exact",
          price: "$0.001",
          network: "eip155:84532",
          payTo: process.env.PAY_TO as `0x${string}`,
        }],
        description: "Research API response",
        mimeType: "application/json",
      },
    }, server));
    
    app.post("/v1/research", express.json(), async (req, res) => {
      res.json({ result: await runResearch(req.body.query) });
    });

    クライアント側では、SDKのラッパーがまとめて処理します。最初の要求、402の読み取り、支払データの作成、署名付きの再送です。

    // agent-client.ts
    import { x402Client, wrapFetchWithPayment } from "@x402/fetch";
    import { ExactEvmScheme } from "@x402/evm/exact/client";
    import { privateKeyToAccount } from "viem/accounts";
    
    const signer = privateKeyToAccount(
      process.env.AGENT_PAYMENT_KEY as `0x${string}`,
    );
    const client = new x402Client();
    client.register("eip155:84532", new ExactEvmScheme(signer));
    
    const paidFetch = wrapFetchWithPayment(fetch, client);
    const response = await paidFetch("https://api.example.com/v1/research", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ query: "x402" }),
    });
    
    if (!response.ok) throw new Error(`request failed: ${response.status}`);
    const result = await response.json();

    これは接続を確かめるためのものです。本番でこのまま使うと、APIが示した条件をSDKが処理できる限り、エージェントは署名してしまいます。冒頭の1つめの困りごとです。本番に出すには、次の確認を署名の手前に入れます。

    署名する前に、支払先と金額を自社のルールで確かめる

    支払った後に異常に気づいても、exact方式のプッシュ型支払は原則として取り消せません。いちばん確実に止められるのは、署名する前です。x402クライアントのライフサイクルフックにはonBeforePaymentCreationがあります。金額の超過などを、支払データを作る前に中止できます。

    const approved = {
      networks: new Set(["eip155:84532"]),
      payTo: new Set([process.env.VENDOR_WALLET!.toLowerCase()]),
      assets: new Set([process.env.APPROVED_ASSET!.toLowerCase()]),
      maxPerRequest: 1_000_000n, // assetの最小単位。例: 6 decimalsなら1.00
    };
    
    client.onBeforePaymentCreation(async ({ selectedRequirements: p }) => {
      const recipient = p.payTo.toLowerCase();
      if (!approved.networks.has(p.network))
        return { abort: true, reason: "network is not approved" };
      if (!approved.payTo.has(recipient))
        return { abort: true, reason: "recipient is not approved" };
      if (!approved.assets.has(p.asset.toLowerCase()))
        return { abort: true, reason: "asset is not approved" };
      if (BigInt(p.amount) <= 0n || BigInt(p.amount) > approved.maxPerRequest)
        return { abort: true, reason: "per-request limit exceeded" };
    
      // 実装ではresource、日次累計、依頼元agent、承認ticketも照合する
    });

    上の例は、Base Sepolia(eip155:84532)にそろえています。APPROVED_ASSETには、そのネットワークで確かめた支払資産のアドレスを入れます。金額は資産の最小単位で書くので、桁数(decimals)も確かめてください。このフックは、最初の支払要求より前に登録します。

    この断片では、日次の上限や、同時に来る要求への予算の確保を省いています。これだけを本番の支払制御として使わないでください。

    1回あたりの金額だけでは、0.01ドルを1万回払う暴走は止められません。支払ルールでは、少なくとも次の項目を組み合わせます。

    • 誰が何のために:エージェントID、業務目的
    • どこへ、何で:呼び出し先API、HTTPメソッド、ネットワーク、支払資産、受取アドレス
    • どれだけ:1回の上限、時間帯別の累計、実行回数、有効期限

    上限の残りの確認と確保は、同じトランザクションで行います。並列で動く複数のエージェントが、同時に上限をすり抜けるのを防ぐためです。

    長時間動くエージェントには、日次の枠を持つ支払専用の鍵を持たせます。全社の親ウォレットから直接署名させてはいけません。スマートアカウントのセッションキー(期間や範囲を限った一時的な鍵)で権限を絞る設計はSession Keyの設計で扱っています。ガス代の肩代わりを含む費用の負担は、Paymasterの費用と回収設計で詳しく書いています。

    タイムアウトしても2回払わないようにする

    EVMのexact方式では、EIP-3009の承認を使います。この承認にはnonce(一度しか使えない番号)と有効期間があり、同じ承認の使い回しはコントラクト側で拒否できます(ERC-3009: Transfer With Authorization)。

    しかし、タイムアウトのたびにクライアントが新しい承認に署名すれば、それぞれが有効な別の支払になりえます。冒頭の2つめの困りごとです。オンチェーンのnonceが防ぐのは、同じ承認の使い回し(リプレイ)です。「同じ業務の要求に2回払わない」ことは、別に作る必要があります。

    そのための仕組みが、x402のPayment-Identifier拡張です。業務上の1つの要求ごとに、支払ID(payment ID)を支払データに付けます。サーバーは同じIDへの応答を保存し、重複した処理を避けます。作るときの要点は4つです。

    • 支払IDは、再試行ごとではなく業務の要求ごとに1つ作る。エージェントが再起動しても同じIDを戻せるよう、保存しておく。
    • サーバーは、共有の永続ストレージで支払IDに一意制約をかける。1つのプロセスのメモリ上のキャッシュだけでは足りない。再起動、複数のインスタンス、保存期限が切れた後の再試行に耐えられないからだ。
    • 同じIDで要求の中身が違えば拒否する。メソッド、呼び出し先API、本文のハッシュ、価格条件を記録しておく。IDが一致しただけで、別の処理結果を返さない。
    • クライアントは、結果が分からないまま署名し直さない。まず支払ID、トランザクションハッシュ、サーバーの照会APIで、決済確定と処理結果を確かめる。

    サーバーが402応答でこの拡張への対応を示していなければ、クライアントだけが支払IDを送っても効きません。対応していないAPIにつなぐエージェントでは、同じ要求への自動の再支払を止めます。人の確認か、取引の照会へ回すのが安全です。

    「処理は済んだのに代金が入らない」をどう防ぐか

    今の@x402/expressの既定のミドルウェアは、次の順で動きます。

    1. 支払を検証してから、ルートの処理関数を実行する
    2. 処理関数の応答を、いったん手元に保留する
    3. 応答が2xx/3xxなら決済確定(settle)を実行し、成功したら保留していた応答とPAYMENT-RESPONSEをクライアントへ送る

    この動きは、公式のx402 payment flowと、Expressミドルウェアの実装で確かめられます。

    つまり、有料の処理は成功したのに決済確定に失敗し、できた成果をクライアントに返さない、という場面がありえます。たとえば処理関数が外部APIに発注を出した後で決済確定が失敗すると、発注は残ったまま代金は受け取れません。メール送信や在庫の確保も同じです。HTTPの応答を保留するだけでは、こうした外への動きは巻き戻せません。

    SDKの既定の流れを使うAPI側のサーバーには、次の状態と、埋め合わせのルールを持たせます。

    状態SDKの既定の流れで起きたこと失敗・タイムアウトの後にすること
    支払条件を提示した
    REQUIREMENT_ISSUED
    支払ID、条件、要求の中身のハッシュ、有効期限を示した同じ業務の要求には同じ条件を返す。別の要求には別のIDを使う
    署名した
    AUTHORIZED
    クライアントが、署名済みデータのハッシュ、エージェントID、支払ルールの判定結果を記録した結果が分からないときは新しく署名せず、同じ支払IDで照会する
    検証した
    VERIFIED
    ミドルウェアが、支払者、金額、ネットワーク、支払条件を検証した同じ支払IDの処理関数を、永続ストレージの一意制約で何度も実行させない
    処理済み・応答はまだ送っていない
    WORK_COMPLETED_RESPONSE_BUFFERED
    処理関数が処理を終えたが、2xx/3xxの応答はまだクライアントに送っていない決済確定に失敗したら応答を捨てる。実行済みの外への動きはやり直さず、取消・解放などの埋め合わせへ進む
    決済が確定した
    SETTLED
    Facilitatorの結果とトランザクションハッシュを記録した支払はやり直さない。保留した応答の再送か、結果の照会を認める
    応答を送った
    RESPONSE_SENT
    HTTPステータス、応答のハッシュ、成果物の場所を記録したクライアントのタイムアウトには同じ応答を返し、新しい承認を作らせない
    埋め合わせが必要
    COMPENSATION_REQUIRED
    処理が成功した後の決済確定の失敗、または決済確定の後の配信の失敗を記録した記録済みのルールに従って実行する。外への動きの取消・在庫の解放・再配信、支払済みなら利用クレジットか返金

    重い処理や外への動きを、決済確定の後に始めたいこともあります。その場合は、SDKの既定の流れとは別に、独自の非同期ジョブとして作ります。支払の対象になる処理関数では、次の2つだけを行います。

    • 永続キューへ、冪等にジョブを登録する
    • 受け付けたことを示す結果を作る

    実際の処理は、決済確定の成功を確かめてからワーカーが始めます。途中で失敗したときは次のように扱います。

    • ジョブは登録済みで、決済確定が失敗した → そのジョブを実行禁止にする
    • 決済確定は成功し、ワーカーが失敗した → 同じジョブIDで再開するか、埋め合わせる

    既定のミドルウェアだけで「決済確定の後に実際の処理」になるわけではありません。

    決済確定の後にサービスを出せなかったときは、同じ支払を巻き戻すのではありません。次のどれかを選びます。API処理の安全なやり直し、利用クレジットの付与、事業者から支払者への別の送金による返金です。x402公式FAQも、exactとuptoのプッシュ型支払は実行後に取り消せないと説明しています。返金は別の送金などの業務処理になります(x402 FAQ)。埋め合わせの条件、手数料の負担、承認者は、障害が起きる前に決めておきます。

    ウォレットの署名で、何が分かり何が分からないか(KYA・KYT)

    ウォレットの署名で確かめられるのは、基本的に1つだけです。「そのアドレスを動かせる鍵が、このメッセージに署名した」ことです。アドレスの裏にいる法人、部署、エージェント、利用目的、社内承認までは、自動では証明されません。

    KYA(Know Your Agent:エージェントの確認)という言葉を使う場合も、1つのプロトコルの機能とは考えません。次の4つに分けて作ります。

    • エージェントの身元:エージェントの実体、所有する組織、モデルやツールのバージョン、実行環境を登録し、支払用のウォレットに結びつける。
    • 権限:誰が、何の目的で、どのAPI・金額・期間を任せたかを残す。支払ルールのバージョンと承認チケットで記録する。
    • ウォレットの認証:鍵を動かせることを署名で確かめる。x402のSign-In-With-X拡張は、CAIP-122(ウォレットでログインするための共通規格)に基づく認証や、購入済みAPIへの再アクセスに使える。ただし、法人が実在することや権限が正当なことまでは証明しない。
    • KYT(Know Your Transaction:取引の確認)と取引の審査:FacilitatorのKYT、制裁・地域・取引の監視、取引先の審査を、自社のリスク基準につなぐ。Facilitatorの判定だけに頼らない。

    どの規制に当たるかは、事業の形・当事者・支払資産・地域で変わります。本人確認、資金移動、暗号資産・電子決済手段、AML/CFT(マネー・ローンダリングおよびテロ資金供与対策)、税務・会計です。ここで示した支払ルールは、技術的な安全のための制御です。法令上のKYCやKYTの義務を満たすことまでは意味しません。

    1件のAPI利用を、支払から会計まで追えるようにする

    ログは、秘密鍵や署名済みデータの全文を残す場所ではありません。一方で、障害対応と会計の突き合わせには、共通のキーが要ります。HTTPの要求、支払ルールの判定、支払、APIの成果を1件に結びつけるキーです。少なくとも次の項目を、改ざんを検知できる監査ログと支払台帳に保存します。

    分類最小の項目使いみち
    要求支払ID、要求の中身のハッシュ、呼び出し先API、メソッド、時刻再試行・重複処理の判定
    権限エージェントID、所有する組織、支払ルールのバージョン、承認チケット、判定結果任せた範囲の説明
    支払条件scheme、network、asset、amount、payTo、有効期限示された条件と署名の中身の比較
    決済確定支払者、トランザクションハッシュ、状態、Facilitator、確定時刻オンチェーンとの突き合わせと障害の復旧
    サービス応答のステータスとハッシュ、成果物ID、原価部門支払と、提供した結果の対応
    埋め合わせ返金・利用クレジットのID、理由、承認者、元のトランザクション返金・会計の二重計上の防止

    鍵やエージェントを止めるときは、署名を無効にするだけでは足りません。次のものもまとめて止めます。

    • まだ使われていない承認と、実行待ちのキュー
    • API側のウォレット許可リストと、セッション
    • 累計の枠

    支払用の鍵を目的別・環境別に分けておけば、1つのエージェントを止めたときに全社の支払が止まる事態を避けやすくなります。

    支払条件の提示(オファー)と提供の受領証を暗号で残したいなら、x402のOffer & Receipt拡張も使えます。ただし受領証は、自社の注文・原価・税務の記録そのものではありません。支払台帳との対応は保ち続けます。

    本番に出す前のチェックリスト

    • バージョンを固定したか:v2のパッケージだけを使い、ヘッダーとCAIP-2のネットワークIDを確かめる結合テストを持つ。
    • 支払候補を確かめたか:ネットワーク、支払資産、受取アドレス、金額、呼び出し先APIを許可リストと突き合わせる。知らない候補には署名しない。
    • 上限は並列の実行に耐えるか:1回の額、日次の累計、回数、エージェント別の枠を、分けられない1つの処理で確保する。
    • 鍵を分けたか:権限を絞ったエージェント専用の鍵を使い、親ウォレットや受取ウォレットと共用しない。失効の手順を訓練しておく。
    • 業務の要求を保存したか:支払IDと要求の中身のハッシュを保存し、タイムアウトの後は新しく署名する前に状態を照会する。
    • サーバーも冪等か:複数のインスタンスで一意制約を共有し、同じIDで中身が違う要求を拒否する。
    • 決済確定の後の障害を試したか:200応答の消失、Facilitatorのタイムアウト、API処理の失敗、エージェントの再起動を、わざと障害を起こす試験で再現する。
    • 埋め合わせ方を決めたか:再提供、利用クレジット、返金の条件と、承認の流れを決める。
    • 追えるか:支払IDから、支払ルール、トランザクション、応答、会計の記録までたどれる。
    • 技術で決めないことを分けたか:法務・AML/CFT・税務・会計の判断を、技術の実装で代わりにしない。

    最初のPoCは、小さく始めます。テストネット、1つのAPI、1つの支払資産、決まった少額で動かします。手動の承認を超える支払は止めます。

    正常なHTTP 200だけで合格にはしません。同じ要求の10回の再試行、決済確定の直後のプロセス停止、Facilitatorのタイムアウト、鍵の失効を試します。それから本番の枠を広げる方が、x402の手軽さを保ったまま事故の範囲を小さくできます。

    関連記事

    XTELAができること

    私たちは、AIやAPIのサービス要件を、x402に対応した有料API、支払用ウォレットと権限のルール、支払台帳、監視と障害復旧の仕組みへ落とし込む設計と、テストネットでのPoC開発を行います。二重払いや決済確定後の失敗を意図的に起こす試験まで含めて、本番の支払枠を広げられる状態を貴社と一緒に作ります。法的な判断は弁護士と連携して進めます。AIエージェント決済・x402の実装について相談する

    主要参考資料

    資料の確認日と注意

    ここで扱ったx402 v2の仕様、公式リポジトリの移転を含む公開一次情報は、2026年9月24日に確認しました。x402のパッケージ、拡張、Facilitatorの対応ネットワーク・支払資産・料金、各法域での扱いは変わりえます。本番に入れるときは、最新の公式仕様と、使うFacilitatorの条件を確かめてください。法務・AML/CFT・税務・会計は、対象の事業に応じて専門家に確認してください。

    お問い合わせ

    どんなフェーズからでも、お持ちのアイデアや企画をもとにご提案可能です。
    まずはお気軽にご相談下さい!