はじめに
x402 の「動かしてみた」記事は、もう日本語でもたくさんある。Facilitator を自作した話、Cursor / Claude Desktop の MCP から叩いた話、いい記事が揃っている。
なので、この記事はそこでは勝負しない。
僕が一年かけて踏んだのは、x402 / USDC / JPYC でエージェント決済を本番稼働まで持っていったときに、日本の資金決済法・国税庁・適格請求書制度と正面衝突した部分だ。コードを書ける人はここを知らないことが多いし、規制に詳しい人はコードを書かない。両方を一人で踏んだ記録は、たぶんまだ世に出ていない。だから書く。
何を作ったか(30秒)
AIエージェントに「上限と期限つきの使い捨て支払い権限」を渡して、人間の承認なしに USDC / JPYC で有料 API を呼ばせる決済インフラ。暴走したら1クリックで止まる。決済が走ったら日本の会計ソフト(freee / マネーフォワード)に自動で仕訳まで飛ぶ。
コアは4つ。
-
Pay Token:
serviceId × 上限 × 有効期限にスコープを切った短期 JWT - Kill Switch:トークンを失効させる緊急停止
- 自動仕訳:課金が確定したら会計ソフトへ起票
- 監査ログ:どのエージェントが・何に・いくら使ったか
スタックは Hono + TypeScript + PostgreSQL、チェーンは Polygon、送金は viem。フロントは Next.js(Vercel)、API は Railway。
技術で詰まった壁
1. チェーンと通貨の選定:Arbitrum じゃなく Polygon にした理由
最初 Arbitrum も候補だった。最終的に Polygon にしたのは、JPYC の流動性が決め手。USDC のガス代も安く($0.01 未満)、JPYC が Polygon ネイティブで、finality も数秒。「USDC だけ」なら他の選択肢もあったが、日本円ステーブルコインを最初から組み込む前提だと、ここで選択肢が絞られる。
2. ネットワーク retry による二重課金を「物理で」潰す
エージェント決済はマイクロトランザクションが大量に走る。ネットワーク retry が二重課金を生むのは時間の問題だった。
アプリ層のチェックだけだと競合で漏れる。課金テーブルに charge_ref の unique 制約を張って、DB レベルで二重課金を物理的に弾く。
create table if not exists lc_agent_charges (
id uuid primary key default gen_random_uuid(),
charge_ref text not null unique, -- ← ここが二重課金の最終防衛線
buyer_key_id text,
wallet_id text
-- ...
);
課金前に必ず charge_ref の存在を見て、既にあれば課金せずに現在の状態をそのまま返す(=冪等)。
const chargeRef = `${key.id}_${idempotencyKey}`;
// 既にこの charge_ref が適用済みなら、二重に課金せず今のトークンを返す
const seen = await sql`select id from lc_agent_charges where charge_ref = ${chargeRef} limit 1`;
if (seen.length > 0) return { ok: true, row: tok, chargeRef };
冪等キーは 1ツール呼び出し=1キーで SDK 側が発行する。だから transient なネットワークエラーでリトライしても、preflight は同じ予約を返すだけで予算を二重に取らない。
// 1 invocation = 1 idempotency key
const idempotencyKey = newIdempotencyKey();
const preflight = await client.preflight({ token, toolName, idempotencyKey });
同じキーを Stripe 側(トップアップ)にも lc_agent_<buyerKeyId>_<idempotencyKey> の形で渡して、決済代行レイヤーでも二重化を防いでいる。「アプリ層・DB 層・決済代行層」の三段で同じキーを使い回すのがポイント。
3. Kill Switch の race condition:revoke と課金が同時に来たら?
「止める」と「課金する」が同時に飛んできたとき、どっちが勝つか。ここを曖昧にすると「止めたはずなのに課金された」が起きる。
正直に書くと、最初の Kill Switch はバグっていた。ボタンは旧サービス(Railway 上の古い決済プロセス)を叩いていて、現在の課金経路はそこじゃなかった。つまり押しても実際の課金は止まらない。**一番たちが悪い「止まったつもり」**だった。
直した形はシンプルで、gateway_halted という実行時フラグを DB に置き、ゲートウェイとトークン発行エンドポイントが毎リクエストそれを見る。立てた瞬間に新規課金も発行も即停止する。
// 毎リクエストで参照される実行時フラグ。立った瞬間に全課金が止まる
const halted = (await getFlag("gateway_halted")) === "1"
|| process.env.LC_KILL_SWITCH === "1";
個別の revoke も同じ思想で「止まる側に倒す」。revoke はトークン/エージェントの status を revoked にするだけ。課金パスは課金処理に入る前に status を読み、revoked なら即座に弾く。revoke と課金が競合しても、課金側が status を見るので revoke が勝つ。
// 課金パスの冒頭で revoked を最優先に弾く(revoke が課金に勝つ)
if (tok.status === "revoked") {
return { ok: false, error: "token_revoked", status: 403 };
}
「暴走を止める機能」なんだから、迷ったら止まる側に倒す。Kill Switch が間違ったサービスを指していた件は、監視・緊急停止は実装した瞬間に必ず一度『本当に止まるか』を試すべきという教訓として、いまも自分の中に残っている。
4. Pay Token のスコープ設計:Buyer の API キーを直接エージェントに渡してはいけない
長期の Buyer JWT をそのままエージェントに持たせると、全権限が漏れる。暴走したときに「鍵そのもの」を revoke する羽目になる。
なので Buyer JWT からは 短期・スコープ限定の Pay Token を都度発行する設計にした。権限は serviceId × 上限 × 時間 に限定。暴走時は Pay Token だけ捨てれば済む。KYA(Know Your Agent)の tier で日次上限も enforce している。
5. 大量のマイクロ決済が会計ソフトの Journal 上限を殺す
これは作ってから気づいた壁。M2M 決済は件数が爆発する。数万件の課金を1件ずつ会計ソフトに起票したら、freee や マネーフォワード の Deal / Journal 数の制限に普通にぶつかる。
日次・月次で1本の仕訳にまとめる ChargeRollup を挟んで逃がした。「マイクロ決済」と「会計」を素朴につなぐと死ぬ、というのは実際に大量トランザクションを流すまで見えなかった。
6. ガスレス承認:ERC-2612 (Permit) + Embedded Wallet(JPYC×USDC でハマる)
エージェントにいちいち ETH/POL を持たせてガス代を払わせるのは非現実的。Embedded Wallet(Privy)と ERC-2612 の Permit を使い、ユーザーは端末で1回署名するだけ。以降はその署名が各 MCP リクエストに同行する。サーバーには署名も鍵も残らない(これは後述の「電子決済手段の管理」を避ける設計とも直結している)。
USDC だけなら簡単。JPYC を混ぜた瞬間にハマる。 ハマりどころは2つ。
(1) 桁が違う。 USDC は decimals = 6、JPYC は decimals = 18。base unit の計算ロジックを使い回すと、金額が一発でズレる。
(2) これが本命。 EIP-712 ドメインの name / version を、デプロイ済みコントラクトが返す DOMAIN_SEPARATOR() と完全一致させないと、署名が別の signer に復元されて permit() が revert する。しかもトークンごと・チェーンごとに値が違う。
// EIP-712 domain は各コントラクトの DOMAIN_SEPARATOR() と完全一致が必須。
// ズレると署名が別アドレスに復元されて permit() が revert する。
const TOKENS = [
{ currency: "USDC", chainId: 8453, decimals: 6, domainName: "USD Coin", domainVersion: "2" }, // Base
{ currency: "USDC", chainId: 137, decimals: 6, domainName: "USD Coin (PoS)", domainVersion: "2" }, // Polygon
{ currency: "JPYC", chainId: 137, decimals: 18, domainName: "JPY Coin", domainVersion: "1" }, // Polygon
];
同じ USDC でも Base は "USD Coin"、Polygon は "USD Coin (PoS)"。JPYC は "JPY Coin" で version は "1"。ここを定数で1箇所に閉じ込めて、currency × chainId で引くようにした。name / version がドリフトした瞬間に全署名が無効化されるので、トークン定義はコード中でここ一箇所だけに集約するのが事故防止になる。
JPYC(2025-10-27 発行の資金移動業ライセンス版)は ERC-2612(Permit)と EIP-3009 の両対応。ドメイン値はテストで確認しているが、コントラクトのアップグレードで変わりうるので、本番投入前にオンチェーンで再検証する前提にしている。
規制で詰まった壁(ここが本丸)
ここからが、コードの話じゃない。
7. 金融庁に法令解釈相談を出した(そして設計が変わった)
「非カストディだから大丈夫だろう」で進めるのが怖かったので、金融庁の FinTech サポートデスクに法令解釈相談を出した。前提を詰めながら、何度かやりとりした。
以下はあくまで僕の個別ケースについての一般的なやりとりから得た理解であって、正式な法令解釈通知でも、他のスキームに一般化できる結論でもない。そこは強調しておく。
当初は別の構成(トークン化株式まわり)も検討していて、そこで返ってきた整理が設計を変えるきっかけになった。要点だけ自分の言葉でまとめると:
- 暗号資産交換業:USDC は資金決済法上「電子決済手段」であって暗号資産ではない → 暗号資産交換業の枠ではない。
- 資金移動業:ユーザー資金の預託を受けない構成なら、為替取引の受託にあたらない方向。
- 有価証券売買の媒介:ユーザーの指示で販売者に注文を実行して売買を成立させる構成は「媒介」に寄りうる。技術的に非カストディでも、株式の売買が絡むと話が変わる。
そこから「株式系はやめて、純粋な pay-per-call の決済プロキシだけにしたら?」という方向に振り直した。その過程で、一番大事な線引きが見えた。
核心は「電子決済手段の管理」に当たるかどうか。 そしてそれは「利用者のために、電子決済手段(USDC)の移転を行い得る状態にあるか」で判断される(事務ガイドライン 電子決済手段等取引業者関係 Ⅰ-1-2-2④ あたり)。具体的には:
- セッションキー等で、一時的にでも自分がユーザーの USDC を動かせる構成 → 「管理」に寄る。
- スマートコントラクトや決済ソフトの展開・使用に一切関与せず、ソフトを配布するだけ → 「管理」に寄りにくい。
これが、この相談から得た一番大きい学びだった。「ノンカストディ」はラベルじゃない。「自分は技術的にユーザーの金を動かせるのか?」という一点で実質が決まる。 セッションキーを一瞬でも握った瞬間に線に近づく。だから「ユーザーの USDC に触れられる経路を、コードから物理的に消す」方向で設計し直した。コードを書く前に確認したから、作ってから作り直す事故を避けられた ——これが一番言いたいことだ。
8. 収益モデルを規制で諦めた話
実装として一番きれいだったのは、API 提供者の売上を自動で分割して、手数料をその場で天引きする案だった。スマートコントラクトに一行挟めば、収益が「気づかれずに」立つ。
でもこれは 7 で書いた線をまっすぐ踏み抜く。売上の流れに自分のコードを挟んで分配する時点で「媒介」に寄るし、分配のために一瞬でも USDC を動かせる権限を持てば「管理」に寄る。ラベルが何であれ、実態(技術的に動かせるか)で見られる。
なので自動天引きは捨てて、「お金の流れに割り込む」のをやめ、純粋なインフラ利用料として後から請求する形に倒した。コードの綺麗さより、どの行為類型に立つかで設計が決まる、という当たり前を、収益の旨味と引き換えに学んだ。
税務で詰まった壁
実装より地味だけど、ここで詰まる人が一番多いと思う。
9. USDC 売上は「期末時価評価なし、でも為替差損益あり」
第三者が発行した電子決済手段(USDC 等)を売上で受け取った場合、暗号資産の期末時価評価課税(含み益課税)の対象外になる。ここで「じゃあ楽だ」と思うと罠にハマる。
国税庁が 2025 年末に FAQ を更新して、電子決済手段を「金銭債権に該当」と整理した。取得時は券面額(1 USDC = 1 USD)で計上、譲渡時は対価と帳簿価額の差額を益金/損金、期末の時価評価は不要。ただし外貨建てなので、取得時レートで円換算して計上し、期末に決算日レートで再換算した差額を「為替差損益」として認識する必要がある。
「ステーブルだから無税」は誤解。決済頻度が高い SaaS だと、ここの自動化(日々のレートとトランザクション履歴を会計ソフトに自動連携)が無いと経理が死ぬ。
10. 「USDC で受け取ったから売上じゃない」は通らない
収入金額には金銭以外の物・権利・経済的利益の価額も含まれる。消費税の課税標準も同じ。USDC で受け取っても普通に売上計上義務がある。受領価額・円換算根拠・請求書との突合を残しておく。
11. 適格請求書(インボイス)と源泉徴収を実装に落とす
ここは「日本特化」で作り込んだ部分。
- 請求書発行元の
T + 13桁を国税庁の適格請求書発行事業者公表サイト API で検証して、適格なら課税仕入・非適格なら対象外、と税区分を自動判定。 - 個人事業主への支払いで源泉徴収義務が出るケースを自動判定して、10.21%(100万円超の部分は 20.42%) を控除、
外注費 / 預り金 / 普通預金の3行で起票。
エージェントが自動で API に金を払う世界でも、日本だとこの会計・税務処理は消えない。むしろ「自動で払うからこそ、自動で正しく記帳する」必要がある。
まとめ:この畑、実装と規制を両方踏んだ人が少ない
x402 / JPYC の技術記事は増えてきた。でも「本番まで動かして、日本の資金決済法・国税庁・インボイスと正面衝突した」記録はまだほとんど無いと思う。
僕はそこを一年踏んできた。同じ畑で手を動かしている人、これから JPYC × x402 を本番に載せようとしている人と、普通に技術の話がしたい。詰まりどころは共有できると思う。
実装や日本の規制まわりで話したい方は、GitHub(evidai)まで気軽にどうぞ。
免責
本記事は個人の実装体験の共有であり、法務・税務・規制に関する助言ではありません。資金決済法・国税庁の取扱いは個別事情により判断が変わり、制度も更新されます。記載は執筆時点の筆者の理解にすぎず、正確性を保証しません。実際の判断は、必ず弁護士・税理士・行政書士等の専門家、および金融庁・国税庁の一次情報に基づいて行ってください。