FABRKNT
Building with the Stack — 実アプリを作る
アプリケーションパターン
レッスン 11 / 11·CONTENT16 分40 XP
コース
Building with the Stack — 実アプリを作る
レッスンの役割
CONTENT
順序
11 / 11

レッスン10 — Machine Payments — HTTP 402 と Tempo MPP スタック

問い

2026 年の有料 API はサインアップ → メール認証 → API キー → 請求設定 → プラン契約を経て、ようやく 1 回呼べる。SaaS 利用者には許容できるが、単発呼び出しの自律エージェントには重すぎる。アカウントも API キーも持たずに pay-per-call できる protocol はどう作るか?

原理(最小モデル)

  • HTTP 402 + Payment 認証スキーム(30 年前から予約されたコードを本気で使う)。 フロー: GET /resource402 Payment Required + WWW-Authenticate: Payment <challenge> → クライアントが HTTP の外で 支払い履行 → Authorization: Payment <credential> 付き再試行 → 200 OK。アカウント登録不要、API キー不要、rail(Tempo / Stripe / Lightning 等)はサーバが受け付けるものなら何でも。
  • rail 非依存(Core)+ rail 別 Methods + 抽象 Intents の 3 層。 Core が HTTP メカニクスと Payment ヘッダ文法(rail に依存しない)/ Methods が rail 固有実装(tempo//stripe//evm//solana//lightning//card/)/ Intents が抽象パターン(charge / authorize / subscription)。1 つの rail を Core に埋め込むと別 rail 追加時に Core 再設計、分離すると Methods への追加だけで済む。
  • 支払い処理は HTTP ラウンドトリップ外。 プロトコル本体はハンドシェイク(402 challenge → credential → 200)のみを規定し、決済 rail は challenge で委譲する。この分離が拡張性を作る(artemis の Collector/Strategy/Executor 分割と同じ規律)。
  • agent スケール問題に Intents が答える。 1 分 1000 リクエストを都度オンチェーン決済すると遅延・手数料で破綻。Intents の authorize/subscription がオフチェーン voucher の交換(セッション、close 時に決済)でこれを解く。

ハンドシェイクを 1 枚で(mpp-specs/README.md 準拠):

sequenceDiagram
    participant Client
    participant Server

    Client->>Server: GET /resource
    Server-->>Client: 402 Payment Required<br/>WWW-Authenticate: Payment ...

    Note over Client: Client fulfills payment challenge

    Client->>Server: GET /resource<br/>Authorization: Payment credential
    Server-->>Client: 200 OK

具体例

サーバ側(challenge 発行 + credential 検証、rail 非依存):

use mpp::server::{Mpp, tempo, TempoConfig};

let mpp = Mpp::create(tempo(TempoConfig {
    recipient: "0x742d35Cc6634C0532925a3b844Bc9e7595f1B0F2",
}))?;

let challenge = mpp.charge("1")?;             // WWW-Authenticate の値を返す
let receipt = mpp.verify_credential(&credential).await?;

Mpp::create は payment provider(tempo(...)/stripe(...)/自前)を引数に取り、challenge 発行と credential 検証を rail 非依存で提供する。サーバの他コードは provider 差し替えで影響を受けない。

クライアント側(402 を透過処理):

use mpp::client::{PaymentMiddleware, TempoProvider};
use reqwest_middleware::ClientBuilder;

let provider = TempoProvider::new(signer, "https://rpc.moderato.tempo.xyz")?;
let client = ClientBuilder::new(reqwest::Client::new())
    .with(PaymentMiddleware::new(provider))
    .build();

// 以降のリクエストは 402 が自動でハンドリングされる
let resp = client.get("https://mpp.dev/api/ping/paid").send().await?;

PaymentMiddleware が reqwest クライアントをラップ、402 を受けたら challenge をパース → provider で支払い履行 → Authorization: Payment を付けて再試行。呼び出し側からは普通の .get().send() に見える。

tempo wallet CLI(3 コマンドで全フロー):

tempo wallet login                        # passkey ログイン、ブラウザが開く
tempo wallet fund                         # 残高をトップアップ
tempo request https://aviationstack.mpp.tempo.xyz/v1/flights?flight_iata=AA100

3 番目が本丸 — 402 challenge / 署名 / 支払い / 再試行を全部実行。passkey は スコープ付きセッションキー(時限・上限金額・チェーンバウンド)を認可し、CLI は制限付きクレデンシャルだけを保持(root key は外に出ない)。

支払い形態:

  • One-shot(charge) — リクエストごとに独立オンチェーン決済、セッション状態なし。単発・低頻度向け。
  • Session(channel) — オンチェーンチャネルを 1 度開き、オフチェーン voucher をリクエストごとに交換、close 時決済。ストリーミング・反復向け(agent スケールへの解)。

失敗例(誤解)

WWW-Authenticate: Payment に Tempo を埋め込めば simple」は誤り — 1 rail を Core に埋め込んだ瞬間、別 rail を使いたい人すべてからプロトコルを fork される。Stripe が MPP を共同 maintain できるのは Core が rail 中立だから(Stripe は methods/stripe/ を寄贈、Core は触らない)。Lightning / ACH / 将来の rail も同形で追加。メカニクス / ポリシー / 具体実装 の分離が rail ロックインを防ぐ。


ここまでで「HTTP 402 + Core/Intents/Methods 3 層 + rail 中立」は着地した。仕様は IETF ドラフト(draft-ryan-httpauth-payment-00)で、ワイヤ細部はまだ変わりうる — 全体像(402 + Payment スキーム)と Tempo/Stripe リファレンス実装を学習対象に。コードは抜粋(実行時は公式実装参照)。

🛑 予測。 agent が 1 分 1000 件の有料 API リクエストを送る。都度オンチェーン Tempo tx で処理すると何が壊れるか、Intents 層はこれをどう解決するか?(答え: 都度決済は遅延・手数料で破綻(オンチェーン finality 待ち + gas)。Intents が chargeauthorize/subscription を分離し、authorize はチャネルを 1 度開いてオフチェーン voucher を交換、close 時に決済。セッション単位のまとめ払いで agent スケールに対応。)

ステップで組み立てる

Step 1-4: 仕様 → SDK → ウォレット

  1. Core/Intents/Methods を読むtempoxyz/mpp-specs)— Core は HTTP メカニクスのみ、Methods が rail 固有、Intents が両者の接続。新 method 追加は Core 変更でなく specs/methods/ 追加だけ。
  2. mpp-rs SDKtempoxyz/mpp-rs)— サーバ側 Mpp::create(tempo(...)) / クライアント側 PaymentMiddleware。両者とも payment provider を差し替えるだけで rail 切り替え可能。
  3. tempo wallet CLItempoxyz/wallet)— passkey ログイン → fund → tempo request <url>。one-shot vs session の使い分け、passkey 由来セッションキーの保持層と PaymentProvider を MPP middleware に渡す層の 2 つを ARCHITECTURE.md で確認。
  4. 既存サービスを MPP でラップ — L3 カスタム RPC、L6 cheatcode harness、L7 aggregator、L8 router を Mpp::create(tempo(...)) で包めば、agent も人も 1 リクエストごとに支払える(請求インフラ・API キー・Stripe ポータル統合 すべて不要)。

agent 向けプロダクトの伸びしろ: 単発のフライト状況 / プライシング oracle / token 単位課金の LLM 補完 — 人間向けには小さすぎて値付けできない粒度を、agent 向け API で開く(MPP がリクエスト単位決済を安価にしているから)。

答え合わせ(Test gate)

ユーザを締め出すか攻撃者にダブルスペンドさせる失敗モード 2 つを潰す:

// tests/mpp_gate.rs
use reqwest::StatusCode;

#[tokio::test]
async fn returns_402_without_payment() {
    let svc = test_endpoint().await;
    let resp = reqwest::get(svc.url("/resource")).await.unwrap();
    assert_eq!(resp.status(), StatusCode::PAYMENT_REQUIRED);

    let body: PaymentRequired = resp.json().await.unwrap();
    assert!(body.amount > U256::ZERO);
    assert_eq!(body.recipient, svc.recipient_address());
}

#[tokio::test]
async fn returns_resource_with_valid_payment() {
    let svc = test_endpoint().await;
    let receipt = make_micropayment(&svc).await;

    let resp = reqwest::Client::new()
        .get(svc.url("/resource"))
        .header("X-PAYMENT", receipt.encode())
        .send().await.unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    assert_eq!(resp.text().await.unwrap(), EXPECTED_RESOURCE_BODY);
}

#[tokio::test]
async fn rejects_replayed_payment() {
    let svc = test_endpoint().await;
    let receipt = make_micropayment(&svc).await;
    let header_value = receipt.encode();

    // 1 回目は通る
    let r1 = reqwest::Client::new()
        .get(svc.url("/resource"))
        .header("X-PAYMENT", &header_value)
        .send().await.unwrap();
    assert_eq!(r1.status(), StatusCode::OK);

    // 2 回目は失敗するはず
    let r2 = reqwest::Client::new()
        .get(svc.url("/resource"))
        .header("X-PAYMENT", &header_value)
        .send().await.unwrap();
    assert!(matches!(r2.status(), StatusCode::PAYMENT_REQUIRED | StatusCode::CONFLICT));
}

3 つすべてエンドポイント local 起動で pass まで未完了(payment leg は forked Tempo testnet か anvil で)。replay テストで fail する 402 エンドポイントは URL を見つけた誰かが来るのを待っている wallet drainer。

合格基準

  • 上記 3 テスト(402 なし / 有効支払い 200 / replay 拒否)が green。
  • MPP の 3 層(Core / Intents / Methods)と各層の責務を即答できる。
  • 「Stripe Checkout + API キー」では agent に提供できないもの(リクエスト単位決済 + アカウント不要 + rail ロックイン不要)を 1 文で言える。

Drill

  1. specs/core/draft-httpauth-payment-00.mdPayment スキーム 3 ヘッダフィールドを言える(45 分)。
  2. mpp-rs の examples を参考に 0.01 charge する axum サーバを動かす(curl で 402、tempo request で 200)(2 時間)。
  3. レッスン3 のカスタム RPC を MPP でラップ、コールごとに charge(3 時間)。
  4. 有料 SSE エンドポイントに session モードで tempo request、チャネル open / voucher 交換 / 決済をトレース(1.5 時間)。
  5. SDK にない rail(好きな L1 のネイティブ資産)の PaymentProvider + ChargeMethod を実装してテスト(4 時間)。

Expert への接続

ここまでで application 層の endpoint を作った。Expert tier の counterpart は ペイメントレール工学(決済が製品になる L1 カテゴリ) — MPP がなぜ protocol として存在するかを駆動する 4 つの category shift(決済保証 / Fee 抽象化 / Identity hook / chain 非依存 surface)を抽出し、Tempo に限らず任意のペイメントレール L1 を読めるようにする。

まとめ(3行)

  • MPP = HTTP 402 + WWW-Authenticate: Payment challenge + Authorization: Payment credential。アカウント不要・API キー不要・rail 非依存で、agent が pay-per-call できる protocol。
  • Core(HTTP メカニクス、rail 中立)/ Intents(抽象 charge/authorize/subscription)/ Methods(rail 別実装)の 3 層分離が rail ロックインを防ぎ、新 rail は Methods 追加で済む。Stripe が共同 maintain できるのはこの構造ゆえ。
  • Test gate: 402 なし / 有効支払い 200 / replay 拒否。replay 拒否のない 402 は wallet drainer。L3/L6/L7/L8 を MPP で有料化できる。

ティア完走

「arb のアイデアがある」から「Revm が production と一致することを保証できる」+「pay-per-call で API を ship する」まで、systems engineering スタックの各層(ネットワーク / DB / VM / 認証 / 並行性)に対してエンドツーエンドで構築されテストゲートで動作証明されたアプリが少なくとも 1 つずつ揃った。本ティアの約束が、ここで履行された。

雇用主・プロジェクトに最も近い build を選び、production gap を埋め、小さな public リポとして公開する — それが会話に持っていく成果物。