✖️

【ハンズオン】Solana × x402でAPI課金を実装 - ゼロから学ぶ次世代決済プロトコル

に公開

こんにちは!ブロックチェーンエンジニアの山口夏生です。
ブロックチェーン×AI Agentで自律経済圏を創るブロックチェーン開発組織Komlock labでCTOをしています。

皆さんは、x402をご存知ですか?
Coinbaseが主導する、暗号資産を利用したAIエージェント時代の決済プロトコルです。
今世界中で、急速に注目を集めています。

(0xJeff氏の投稿を一部翻訳)
エージェント間商取引額: 過去30日間で5,000万ドル(約75億円)に到達。
サービス提供者: フロー(取引の流れ)の95〜99%をサービスプロバイダー(AIエージェント)が占めている。
急成長: 主要な指標は、過去3ヶ月間で3倍以上に増加。

https://x.com/0xJeff/status/2003733328093151716

本記事では、Solanaを使ってx402プロトコルをゼロから実装し、その仕組みを基礎から理解します。ライブラリに頼らず仕組みを理解したい方に向けた入門記事です。必要最小限のコードで、ステップごとに解説します。

TL;DR

  • x402 = HTTP 402を活用したAPI課金プロトコル
  • Solana devnetでサーバー・クライアントをゼロから実装
  • 支払いフロー(402→トランザクション署名→検証→決済)を手を動かして理解

x402とは(30秒で理解)

HTTP 402の復活

HTTP 402 Payment Required。1999年にHTTP/1.1で定義されながら、20年以上「将来の使用のため予約」として放置されていたステータスコードです。

それがついに実用化されました。理由は2つ:

  1. ブロックチェーンの成熟: Solanaでは1トランザクション約$0.00025、確定まで400msと、マイクロペイメントに最適な環境が整った
  2. AIエージェントの台頭: 2025年にはAIエージェント市場が470億ドル規模に成長すると予測され、人間の介入なしに自動決済するニーズが高まっている

x402プロトコルの概要

x402は、オープンな決済プロトコルです。
HTTP 402ステータスコードを活用し、APIアクセスに対するマイクロペイメントを実現します。

https://www.x402.org/

5つのゼロ:

特徴 説明
ゼロ手数料 プロトコル手数料なし(ネットワーク手数料のみ)
ゼロ待機 インターネット速度での即座な送金
ゼロ摩擦 アカウント登録・個人情報不要
ゼロ中央集権 オープン標準、誰でも構築・拡張可能
ゼロ制限 特定のネットワークに非依存

x402のユースケース

x402が活用できる代表的なシナリオを紹介します。

ユースケース 説明
有料API AI推論、データ分析、翻訳などのAPIを従量課金で提供
プレミアムコンテンツ 記事、動画、音楽への単発アクセス課金
AIエージェント間決済 エージェントが自律的に他のサービスを購入
IoTマイクロペイメント センサーデータやコンピューティングリソースの売買
ゲーム内課金 アイテムや機能のリアルタイム購入

特にAIエージェント連携は注目分野で、MCP(Model Context Protocol)サーバーにx402を組み込むことで、Claude等のAIアシスタントが自動的に有料APIにアクセスできるようになります。

x402 on Solana

x402はネットワーク非依存のプロトコルで、Base、Arbitrum、Solanaなど複数のチェーンに対応しています。

本記事ではSolanaを選択します。
Solanaの特徴:

  • 低コスト: 1トランザクション約$0.00025
  • 高速: 確定まで約400ms
  • USDC対応: Circle公式のdevnet Faucetあり

先日、Solana上でのx402の取引ボリュームが、x402主要チェーンであるBASEを抜きました🔥
https://x.com/Yu_Ayato/status/2010903189210706402

周辺技術やサービス

x402エコシステムには、以下のツールやサービスがあります。

ツール/サービス 説明
x402-solana Solana用のx402SDK by PayAI。サーバー/クライアント両対応
CDP Facilitator Coinbase提供のファシリテーター
Corbits x402を活用したAIエージェント決済プラットフォーム

参考動画: Corbidsの仕組みについては、Yukiさんの解説動画が詳しいです。

https://www.youtube.com/watch?v=LJQIkd66MkY

x402の実行フロー

標準的なx402フロー(verify → settle)

x402の標準的な支払いフローでは、verify(検証) と settle(決済) の2段階で処理されます。

ポイント:

  • verify: 支払いが有効かどうかを検証(まだ決済しない)
  • settle: コンテンツ提供後に実際の決済を実行
  • Facilitator(CDP等)が検証・決済を仲介

X-Payment ヘッダーの構造(v1)

クライアントは署名済みトランザクションを以下の形式でサーバーに送ります:

{
  "x402Version": 1,
  "scheme": "exact",
  "network": "solana-devnet",
  "payload": {
    "serializedTransaction": "<base64エンコードされたトランザクション>"
  }
}

この JSON 全体をBase64エンコードして X-Payment ヘッダーに格納します。

x402のバージョンについて

本記事は x402 V1 仕様で解説しています。2025年後半にリリースされたV2では、ヘッダー名が X-Payment から PAYMENT-SIGNATURE 等に変更され、ネットワーク指定もCAIP-2形式(solana:5eykt4U...)に変わっています。

本記事でV1を採用する理由:

  • Solana公式のx402ガイドがV1ベースで解説されている
  • Facilitatorを使わないゼロから実装では、V1/V2の差異は影響しない
  • 「クライアントが署名→サーバーが検証・送信」という基本フローはV1/V2で共通
  • 学習目的ではシンプルなV1形式の方が理解しやすい

本番環境でFacilitatorを使う場合は、公式ドキュメントで最新仕様を確認してください。

ゼロから実装でx402を体験

ここからは実際にコードを書いて、x402の仕組みを体験します。

本ハンズオンは Solana公式のx402ガイド を参考に、ステップごとに丁寧に解説したものです。

実際に手元で動かしてみたい方は、こちらのリポジトリをCloneしてください。
https://github.com/natsukingly/x402-solana-demo

実装するフローの全体像

これから実装するx402の支払いフローを先に確認しておきましょう。

  1. クライアント → /premium にリクエスト
  2. サーバー → 402 Payment Required で支払い条件(宛先・金額)を返却
  3. クライアント → 支払いトランザクションを作成・署名
  4. クライアント → /premium に再リクエスト(トランザクションを添付)
  5. サーバー → トランザクションを検証し、ネットワークに送信
  6. サーバー → 確認後、200 OK でコンテンツを返却

前提条件

  • Node.js v20以上
  • Solana CLI

1. プロジェクトのセットアップ

# リポジトリをクローン
git clone https://github.com/natsukingly/x402-solana-demo.git
cd x402-solana-demo

# 依存関係をインストール
npm install
ファイル構成
x402-solana-demo/
├── server.ts         # サーバー(ハンズオン用テンプレート)
├── client.ts         # クライアント(ハンズオン用テンプレート)
├── server.sample.ts  # サーバー完成版
├── client.sample.ts  # クライアント完成版
├── server.json       # サーバーウォレット(要作成)
├── client.json       # クライアントウォレット(要作成)
├── .env              # 環境変数(要作成)
└── .env.example      # 環境変数テンプレート
  • server.ts / client.ts: TODOコメントを埋めていくハンズオン形式
  • *.sample.ts: 困ったときの解答

2. Solana CLI設定(devnet)

# Solana CLIがなければインストール
sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"

# PATHに追加
export PATH="$HOME/.local/share/solana/install/active_release/bin:$PATH"

# devnetに接続
solana config set --url devnet

# 新しいウォレット作成(サーバー用)
solana-keygen new --outfile server.json

# クライアント用ウォレットも作成
solana-keygen new --outfile client.json
環境変数の設定
# .env.exampleをコピー
cp .env.example .env

# サーバーウォレットの公開鍵を確認
solana-keygen pubkey server.json

# .envのRECIPIENT_WALLETを更新
RECIPIENT_WALLET=上記で表示された公開鍵
devnet SOLの取得
# サーバーにSOLをエアドロップ
solana airdrop 2 $(solana-keygen pubkey server.json)

# クライアントにSOLをエアドロップ
solana airdrop 2 $(solana-keygen pubkey client.json)

レートリミットエラーが出る場合:

  • Solana Faucet からブラウザで取得
  • または数分待ってから再試行

3. devnet USDCの取得

devnetではテスト用のUSDCを使用します。Faucetから取得できます:

https://faucet.circle.com/

上記サイトでSolana Devnetを選択し、クライアントウォレットのアドレスを入力してUSDCを取得してください。


Circle FaucetでSolana Devnetを選択した状態

4. サーバー用Token Accountの作成

サーバーがUSDCを受け取るためのToken Accountを事前に作成しておきます。

spl-token create-account 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU \
  --owner $(solana-keygen pubkey server.json) \
  --fee-payer server.json

ハンズオン: サーバー実装

server.ts を開いて、各TODOセクションのコードを実装していきましょう。

編集前のserver.ts
https://github.com/natsukingly/x402-solana-demo/blob/main/server.ts

Step 1: 定数を設定

以下の4つの定数を定義してください。

変数名 内容 ヒント
connection Solana Devnetへの接続 new Connection("https://api.devnet.solana.com")
USDC_MINT Devnet USDCのMintアドレス 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU
RECIPIENT_WALLET サーバーウォレットの公開鍵 solana-keygen pubkey server.json で確認
PRICE_USDC 価格(6桁小数点) 100 = 0.0001 USDC
正解
const connection = new Connection("https://api.devnet.solana.com");
const USDC_MINT = new PublicKey("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU");
const RECIPIENT_WALLET = new PublicKey(process.env.RECIPIENT_WALLET!);
const PRICE_USDC = 100; // 0.0001 USDC

Step 2: X-Paymentヘッダーをデコード

クライアントから送られてきたX-Paymentヘッダーをデコードします。

やること:

  1. xPaymentHeader を Base64デコードして JSON.parse
  2. paymentData.payload.serializedTransaction を Base64デコード
  3. Transaction.from() でトランザクションオブジェクトに変換
  4. getAssociatedTokenAddress(USDC_MINT, RECIPIENT_WALLET) で受取先Token Account計算
正解
const paymentData = JSON.parse(
  Buffer.from(xPaymentHeader, "base64").toString("utf-8")
);
const txBuffer = Buffer.from(
  paymentData.payload.serializedTransaction,
  "base64"
);
const tx = Transaction.from(txBuffer);
const recipientTokenAccount = await getAssociatedTokenAddress(
  USDC_MINT,
  RECIPIENT_WALLET
);

Step 3: インストラクション検証

トランザクション内のSPL Token Transfer Txを検証します。これがx402の最も重要な部分です。

検証のポイント:

チェック項目 方法
SPL Token Txか ix.programId.equals(TOKEN_PROGRAM_ID)
Transfer Txか ix.data[0] === 3(opcode 3 = Transfer)
金額の読み取り ix.data.readBigUInt64LE(1)(u64 little-endian)
宛先の確認 ix.keys[1].pubkeyが自分のToken Accountか
正解
let validTransfer = false;
let transferAmount = 0;

for (const ix of tx.instructions) {
  if (ix.programId.equals(TOKEN_PROGRAM_ID)) {
    if (ix.data.length >= 9 && ix.data[0] === 3) {
      transferAmount = Number(ix.data.readBigUInt64LE(1));
      if (ix.keys.length >= 2) {
        const destAccount = ix.keys[1].pubkey;
        if (
          destAccount.equals(recipientTokenAccount) &&
          transferAmount >= PRICE_USDC
        ) {
          validTransfer = true;
          break;
        }
      }
    }
  }
}

if (!validTransfer) {
  return res.status(402).json({
    error: "Invalid transfer instruction",
    details: "Transfer destination or amount is incorrect",
  });
}

Step 4: トランザクション処理

検証に通過したトランザクションをネットワークに送信します。

やること:

  1. connection.simulateTransaction(tx) でシミュレート
  2. connection.sendRawTransaction(txBuffer, { skipPreflight: false }) で送信
  3. connection.confirmTransaction(signature, "confirmed") で確認
正解
const simulation = await connection.simulateTransaction(tx);
if (simulation.value.err) {
  return res.status(402).json({
    error: "Simulation failed",
    details: simulation.value.err,
  });
}

const signature = await connection.sendRawTransaction(txBuffer, {
  skipPreflight: false,
});

const confirmation = await connection.confirmTransaction(
  signature,
  "confirmed"
);

if (confirmation.value.err) {
  return res.status(402).json({ error: "Transaction failed on-chain" });
}

return res.json({
  data: "これはプレミアムコンテンツです!支払いありがとうございます。",
  paymentDetails: {
    signature,
    amount: transferAmount,
    amountUSDC: transferAmount / 1_000_000,
    explorerUrl: `https://explorer.solana.com/tx/${signature}?cluster=devnet`,
  },
});

Step 5: 402レスポンス

支払いがない場合、status(402) で支払い情報を返します。

{
  message: "Payment Required",
  payment: { recipientWallet, tokenAccount, mint, amount, amountUSDC }
}
正解
const recipientTokenAccount = await getAssociatedTokenAddress(
  USDC_MINT,
  RECIPIENT_WALLET
);

return res.status(402).json({
  message: "Payment Required",
  payment: {
    recipientWallet: RECIPIENT_WALLET.toBase58(),
    tokenAccount: recipientTokenAccount.toBase58(),
    mint: USDC_MINT.toBase58(),
    amount: PRICE_USDC,
    amountUSDC: PRICE_USDC / 1_000_000,
  },
});

Step 6: サーバー起動

app.listen(3001, ...) でポート3001で起動します。

正解
app.listen(3001, () => {
  console.log("x402 Server running on http://localhost:3001");
});

ハンズオン: クライアント実装

client.ts を開いて、各TODOセクションのコードを実装していきましょう。

編集前のclient.ts
https://github.com/natsukingly/x402-solana-demo/blob/main/client.ts

Step 1: 接続とウォレット設定

変数名 内容 ヒント
connection Solana Devnetへの接続 new Connection("https://api.devnet.solana.com")
payer クライアントウォレット client.json から Keypair.fromSecretKey() で作成
正解
const connection = new Connection("https://api.devnet.solana.com");
const keypairData = JSON.parse(readFileSync("./client.json", "utf-8"));
const payer = Keypair.fromSecretKey(Uint8Array.from(keypairData));

Step 2: サーバーから支払い情報を取得

やること:

  1. fetch("http://localhost:3001/premium") でリクエスト
  2. response.json() でJSONパース(as PaymentQuote でキャスト)
  3. status !== 402 なら早期リターン
正解
const quoteResponse = await fetch("http://localhost:3001/premium");
const quote = (await quoteResponse.json()) as PaymentQuote;

if (quoteResponse.status !== 402) {
  console.log("Unexpected response:", quote);
  return;
}

console.log(`   Required: ${quote.payment.amountUSDC} USDC`);
console.log(`   Recipient: ${quote.payment.tokenAccount}`);

Step 3: 支払い情報を変数に格納

402レスポンスの quote.payment から必要な情報を取り出します。

変数名 取得元
recipientTokenAccount quote.payment.tokenAccount
mint quote.payment.mint
amount quote.payment.amount
正解
const recipientTokenAccount = new PublicKey(quote.payment.tokenAccount);
const mint = new PublicKey(quote.payment.mint);
const amount = quote.payment.amount;

Step 4: 支払い元Token Accountを取得

残高チェックも実装(payerTokenAccount.amount を使用)

正解
const payerTokenAccount = await getOrCreateAssociatedTokenAccount(
  connection,
  payer,
  mint,
  payer.publicKey
);

console.log(`   Payer Token Account: ${payerTokenAccount.address.toBase58()}`);
console.log(`   Balance: ${Number(payerTokenAccount.amount) / 1_000_000} USDC`);

if (Number(payerTokenAccount.amount) < amount) {
  console.error("Insufficient USDC balance!");
  console.log("Get devnet USDC from: https://faucet.circle.com/");
  return;
}

Step 5: USDC転送Txを作成

createTransferInstruction でSPL Token転送Txを作成します。

引数 説明
source 送信元Token Account(payerTokenAccount.address)
destination 送信先Token Account(recipientTokenAccount)
owner 署名者(payer.publicKey)
amount 転送金額
正解
const transferIx = createTransferInstruction(
  payerTokenAccount.address,  // source
  recipientTokenAccount,       // destination
  payer.publicKey,            // owner
  amount                       // amount
);

Step 6: トランザクション構築と署名

やること:

  1. connection.getLatestBlockhash() でブロックハッシュ取得
  2. new Transaction({ feePayer, blockhash, lastValidBlockHeight })
  3. tx.add(transferIx) でTxを追加
  4. tx.sign(payer) で署名(送信はしない!)
正解
const { blockhash, lastValidBlockHeight } =
  await connection.getLatestBlockhash();

const tx = new Transaction({
  feePayer: payer.publicKey,
  blockhash,
  lastValidBlockHeight,
});

tx.add(transferIx);
tx.sign(payer);

Step 7: x402ペイロード作成

署名済みトランザクションをx402形式のペイロードに変換します。

やること:

  1. tx.serialize().toString("base64") でトランザクションをBase64化
  2. x402形式のオブジェクトを作成(x402Version, scheme, network, payload)
  3. オブジェクト全体をJSON → Base64に変換して X-Payment ヘッダー用の文字列を作成
正解
const serializedTx = tx.serialize().toString("base64");

const paymentProof = {
  x402Version: 1,
  scheme: "exact",
  network: "solana-devnet",
  payload: {
    serializedTransaction: serializedTx,
  },
};

const xPaymentHeader = Buffer.from(
  JSON.stringify(paymentProof)
).toString("base64");

Step 8: X-Paymentヘッダー付きでリクエスト

  • status === 200 なら成功、それ以外は失敗
正解
console.log("\n3. Sending payment...");

const paidResponse = await fetch("http://localhost:3001/premium", {
  headers: {
    "X-Payment": xPaymentHeader,
  },
});

const result = (await paidResponse.json()) as PaymentResult;

if (paidResponse.status === 200) {
  console.log("\n=== Payment Successful! ===");
  console.log(`Content: ${result.data}`);
  console.log(`Transaction: ${result.paymentDetails?.explorerUrl}`);
} else {
  console.log("\n=== Payment Failed ===");
  console.log(result);
}

完成後のコード

server.ts
https://github.com/natsukingly/x402-solana-demo/blob/main/server.sample.ts

client.ts
https://github.com/natsukingly/x402-solana-demo/blob/main/client.sample.ts

動作確認

サーバーの起動

npx ts-node server.ts

出力:

x402 Server running on http://localhost:3001

クライアントの実行

別のターミナルで:

npx ts-node client.ts

成功時の出力例

=== x402 Client Demo ===
Payer: Fz7qLnp9qVNTofMBnft3ZKKmVzMwuiAMg8Sf49rjNkqt

1. Requesting payment quote...
   Required: 0.0001 USDC
   Recipient: 3czoXAXJ9biX2VVLP3oigVcg7stuRxWhJz8be9BkWP5W

2. Creating payment transaction...
   Payer Token Account: 5gtyiVdUhUND9Y9Dt7UbT9GBhmeiMBUh66FSBgVA9kXj
   Balance: 1 USDC

3. Sending payment...

=== Payment Successful! ===
Content: これはプレミアムコンテンツです!支払いありがとうございます。
Transaction: https://explorer.solana.com/tx/xxxxx?cluster=devnet

Solana Explorerでトランザクションを確認すると、USDCの転送が記録されているのがわかります。
https://solscan.io/tx/5gcjxdQ9fdv46YCxuiwHTLcpwMRDUKWsKBRxLaURhqPYkiKNwig2C1SP1MqEG2LyxyLrdgTBBLQD4PbRtF2Xh6og?cluster=devnet

Solana Explorer(Devnet)でトランザクションを確認できます

トラブルシューティング

"Non-base58 character" エラー:
→ .env の RECIPIENT_WALLET が正しいSolana公開鍵になっているか確認

"InvalidAccountData" エラー:
→ サーバー用Token Accountが作成されているか確認(手順4参照)

spl-token create-account 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU \
  --owner $(solana-keygen pubkey server.json) \
  --fee-payer server.json

"Insufficient USDC balance" エラー:
→ Circle Faucet からクライアントウォレットにUSDCを追加取得

devnet SOL airdropがレートリミット:
→ Solana Faucet からブラウザで取得、または数分待って再試行

blockhash期限切れエラー:
→ blockhashの有効期限は約60秒。再試行する場合は取り直す

おまけ: Solana固有の代替フロー

本記事では x402.org 標準フロー(サーバーがトランザクションを送信)を実装しましたが、Solanaでは別のアプローチも可能です。

代替フロー: クライアントが直接送信

  1. クライアントがトランザクションを直接ネットワークに送信(memo Tx付き)
  2. クライアントはトランザクション署名だけをサーバーに送信
  3. サーバーは署名を検証してコンテンツを返却

メリット:

  • 接続断の問題を解決: 支払い後にクライアントが切断しても、同じ署名で再リクエスト可能(支払いは既にオンチェーンで確定済み)

デメリット:

  • x402.org標準フローから逸脱する(標準ではサーバーがトランザクションを送信)

本番環境で接続断が問題になる場合は、この代替フローも検討してみてください。

応用: AIエージェント連携

なぜx402はAIエージェントと相性が良いのか

  1. 自動決済: 人間の介入なしにAPIアクセス料金を支払える
  2. マイクロペイメント: 1回のAPI呼び出しに$0.001のような少額課金が可能
  3. オープン標準: どのAIエージェントフレームワークからでも利用可能

MCP(Model Context Protocol)サーバーへの応用

Claude DesktopなどのMCPクライアントから利用するサーバーにx402を組み込むことで、AIアシスタントが自動的に有料APIにアクセスできます。

User → Claude Desktop → MCP Server (x402) → Paid API
                              ↓
                         支払いを自動実行

関連プロジェクト

プロジェクト 説明
Coinbase AgentKit AIエージェント向けブロックチェーンツールキット
MCPay.tech MCPサーバーのマネタイズ支援
Google A2A エージェント間通信・決済プロトコル(開発中)
Agents Payment Protocol (AP2) Google主導のエージェント決済標準化

まとめ

本記事では、x402プロトコルをライブラリなしで実装し、その仕組みを解説しました。

学んだこと:

  • HTTP 402ステータスコードを活用した課金フロー
  • SPL Token Transfer Txの検証方法
  • X-Paymentヘッダーの構造

本番で使うなら

本記事のゼロから実装は学習目的です。本番環境ではFacilitatorの利用を推奨します。

Facilitatorを使う理由:

  • サーバー側でブロックチェーンインフラを維持する必要がない
  • verify(検証)とsettle(決済)を分離し、コンテンツ提供後に決済を実行できる
  • Facilitatorは署名されたトランザクションを実行するだけで、支払い内容の改ざんや資金の窃取はできない設計

Coinbaseが提供しているsolana向けのパッケージの例です。
TypeScript以外にも様々な言語で提供されています。
https://github.com/coinbase/x402/tree/main/typescript/packages/mechanisms/svm

参考リンク

x402公式:

Solana関連:

日本語解説:


最後まで読んでいただきありがとうございました!
この記事が参考になったら、ぜひいいねやコメントをお願いします。

Komlock labはAI x ブロックチェーン開発に注力しています!!

Komlock labでは、こうした最新規格の調査だけでなく、実際のプロダクト開発や技術検証に日々励んでいます。
こちらは、以前開発したAIエージェント間の業務委託契約の紹介記事です。

https://zenn.dev/komlock_lab/articles/8f9702d9862dc0

Komlock lab エンジニア募集中

「AI x ブロックチェーン」という未開拓な領域で、未来のインフラを一緒に作りませんか?
エンジニア絶賛募集中ですので、DMでもDiscordでもお気軽にご連絡ください!

DM宛先:
https://x.com/0x_natto

PR記事とCEOの創業ブログ
https://prtimes.jp/main/html/rd/p/000000332.000041264.html
https://note.com/komlock_lab/n/n2e9437a91023

Komlock lab もくもく会&LT会

ブロックチェーン開発関連のイベントを定期開催しています!

https://connpass.com/user/Komlock_lab/open/

Discordコミュニティ

有益な記事の共有や開発の相談など行っています。どなたでもウェルカムです

https://discord.gg/5cEkN284sn

Komlock lab

Discussion