0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

x402 でAIエージェント決済を本番化したら、金融庁・国税庁・インボイスと正面衝突した話【1年実装の記録】

0
Posted at

はじめに

x402 の「動かしてみた」記事は、もう日本語でもたくさんある。Facilitator を自作した話、Cursor / Claude Desktop の MCP から叩いた話、いい記事が揃っている。

なので、この記事はそこでは勝負しない。

僕が一年かけて踏んだのは、x402 / USDC / JPYC でエージェント決済を本番稼働まで持っていったときに、日本の資金決済法・国税庁・適格請求書制度と正面衝突した部分だ。コードを書ける人はここを知らないことが多いし、規制に詳しい人はコードを書かない。両方を一人で踏んだ記録は、たぶんまだ世に出ていない。だから書く。

何を作ったか(30秒)

AIエージェントに「上限と期限つきの使い捨て支払い権限」を渡して、人間の承認なしに USDC / JPYC で有料 API を呼ばせる決済インフラ。暴走したら1クリックで止まる。決済が走ったら日本の会計ソフト(freee / マネーフォワード)に自動で仕訳まで飛ぶ。

コアは4つ。

  • Pay TokenserviceId × 上限 × 有効期限 にスコープを切った短期 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 はトークン/エージェントの statusrevoked にするだけ。課金パスは課金処理に入る前に 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)まで気軽にどうぞ。

免責

本記事は個人の実装体験の共有であり、法務・税務・規制に関する助言ではありません。資金決済法・国税庁の取扱いは個別事情により判断が変わり、制度も更新されます。記載は執筆時点の筆者の理解にすぎず、正確性を保証しません。実際の判断は、必ず弁護士・税理士・行政書士等の専門家、および金融庁・国税庁の一次情報に基づいて行ってください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?