レッスン10 — Machine Payments — HTTP 402 と Tempo MPP スタック
問い
2026 年の有料 API はサインアップ → メール認証 → API キー → 請求設定 → プラン契約を経て、ようやく 1 回呼べる。SaaS 利用者には許容できるが、単発呼び出しの自律エージェントには重すぎる。アカウントも API キーも持たずに pay-per-call できる protocol はどう作るか?
原理(最小モデル)
- HTTP 402 +
Payment認証スキーム(30 年前から予約されたコードを本気で使う)。 フロー:GET /resource→402 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 が
chargeとauthorize/subscriptionを分離し、authorize はチャネルを 1 度開いてオフチェーン voucher を交換、close 時に決済。セッション単位のまとめ払いで agent スケールに対応。)
ステップで組み立てる
Step 1-4: 仕様 → SDK → ウォレット
- Core/Intents/Methods を読む(
tempoxyz/mpp-specs)— Core は HTTP メカニクスのみ、Methods が rail 固有、Intents が両者の接続。新 method 追加は Core 変更でなくspecs/methods/追加だけ。 - mpp-rs SDK(
tempoxyz/mpp-rs)— サーバ側Mpp::create(tempo(...))/ クライアント側PaymentMiddleware。両者とも payment provider を差し替えるだけで rail 切り替え可能。 - tempo wallet CLI(
tempoxyz/wallet)— passkey ログイン → fund →tempo request <url>。one-shot vs session の使い分け、passkey 由来セッションキーの保持層と PaymentProvider を MPP middleware に渡す層の 2 つをARCHITECTURE.mdで確認。 - 既存サービスを 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
specs/core/draft-httpauth-payment-00.mdのPaymentスキーム 3 ヘッダフィールドを言える(45 分)。mpp-rsの examples を参考に 0.01 charge する axum サーバを動かす(curl で 402、tempo requestで 200)(2 時間)。- レッスン3 のカスタム RPC を MPP でラップ、コールごとに charge(3 時間)。
- 有料 SSE エンドポイントに session モードで
tempo request、チャネル open / voucher 交換 / 決済をトレース(1.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: Paymentchallenge +Authorization: Paymentcredential。アカウント不要・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 リポとして公開する — それが会話に持っていく成果物。