AbaPay について
AbaPayは、BaseとCelo上でステーブルコインによる公共料金などの支払いを行うプラットフォームです。アプリ、チャット、AIエージェントから利用でき、上限付きのオンチェーン許可、複数宛先への一括払い、予約支払いに対応し、MCP経由でも支払い機能を提供します。
主な機能
- 01USDT、USDC、USATでの料金支払い
- 02BaseとCeloに対応
- 03加盟店情報の確認と失敗時の返金
- 04上限付きで取り消し可能なエージェント許可
- 05一括払いと予約自動支払い
- 06残高・プラン・請求向けMCPツール
活用シーン
- 01チャットやMCP経由での通話料・データ購入
- 02許可上限内での定期料金支払いの予約
- 03複数の受取先への料金をまとめて処理
- 04支払い前の残高と購入可能プランの確認
MCP 対応
AbaPayは`/api/mcp`のStreamable HTTP JSON-RPCを通じて、機能確認、残高確認、プラン一覧、料金支払い、一括払い、スケジュール管理をMCPクライアントに提供します。支払いにはチャットと同じ許可上限と安全制御を適用し、OAuthトークンだけでは支出できず、各`pay_bill`呼び出しにPINが必要です。
設定
リモートサーバーはインストール不要です。クライアントを選んで設定をコピーするか、ワンクリックで導入できます。
https://agents.abapays.com/api/mcpclaude mcp add --transport http investorphem-abapay https://agents.abapays.com/api/mcp{"mcpServers":{"investorphem-abapay":{"type":"http","url":"https://agents.abapays.com/api/mcp"}}}Cursor で開く{"servers":{"investorphem-abapay":{"type":"http","url":"https://agents.abapays.com/api/mcp"}}}VS Code で開く{"mcpServers":{"investorphem-abapay":{"type":"http","url":"https://agents.abapays.com/api/mcp"}}}{"mcpServers":{"investorphem-abapay":{"serverUrl":"https://agents.abapays.com/api/mcp"}}}コマンドはターミナルで実行し、JSON は各クライアントの MCP 設定に貼り付けて再起動します。認可が必要なサーバーは初回接続時にログインを求めます。
ツール
11このツールにパラメーターはありません。
詳細
⚡ AbaPay Protocol
AbaPayは、Base(デフォルトのチェーン)とCelo上に構築された、分散型のWeb3ネイティブな公共料金支払いプラットフォームです。ユーザーは、VTpass APIを介してサーバー側で即時に法定通貨決済を行いながら、オンチェーンのステーブルコイン(USDT、USDC、USAT)を使って、現実世界の請求(エアタイム、モバイルデータ、電気料金、ケーブルテレビ、銀行振込、教育用PIN、国際エアタイム/データ)を支払えます。支払いはWebアプリで直接行えるほか、Telegram、WhatsApp、X上の会話型自律AIエージェント(「DeAI」)を通じて、ハンズフリーで行うこともできます。DeAIはERC-8004に基づく実際のオンチェーンIDを持ち、8004scan.ioで検索可能です。制限付きの、ユーザーが取り消し可能なオンチェーンの支出許可を使って、ユーザーが操作しなくても請求を支払ったり、定期的/スケジュールされた自動支払いを実行したり、複数の受取人への一括支払いを決済したりできます。資産の保管も、サーバー側の鍵も必要ありません。
低手数料、国境を越えたユーティリティ販売(ナイジェリアと、VTpassのライブ国際カタログに掲載されるすべての国)、モバイルファーストのアクセシビリティを重視して設計されています。MiniPay、Valora、Farcaster Mini Apps、Coinbase Smart Wallet/Base Account、MetaMask、およびその他のWalletConnect対応ウォレットを利用できます(対応ウォレットと環境を参照)。
運営者: Masonode Technologies Limited(RC 9524980)、ナイジェリア。
🌟 主な機能
- マルチチェーン決済: Base(Mainnet/Sepolia)または Celo(Mainnet/Alfajores)で、USDT、USDC、USAT を使って直接請求書を支払えます。デフォルトのチェーンは Baseです。Celo も引き続き完全にサポートされ、切り替え可能です。アプリは接続中のチェーンを自動検出し、それに応じて利用可能なステーブルコインを絞り込み、並べ替えます。Base では USDC が先頭、Celo では USD₮ が先頭となり、USAT は Celo メインネット限定です。
- VTpass カタログをリアルタイム取得 — プロバイダー情報のハードコードは不要になりました: 通話時間、データ、電気、ケーブルテレビ、教育の各サービスについて、すべてのプロバイダー名、ロゴ、金額上限を、4つの別々のハードコードされたリストではなく、VTpass からリアルタイムで取得します(
src/lib/vtpassCatalog.ts。/api/providers経由でブラウザーに提供)。アプリ、チャット、MCP、管理者ダッシュボードはすべて同じプロセス内キャッシュを参照するため、信頼できる情報源は1つだけです。詳しくは、以下のプロバイダーのリアルタイムカタログ(VTpass 提供)をご覧ください。 - プロバイダーごとの金額上限をリアルタイムで適用: 上限はサービスごとの一律の数値ではなく、VTpass が公開している実際の
minimium_amount/maximum_amountをプロバイダーごとに適用します。通話時間だけでも、MTN は ₦200,000、Glo は ₦100,000、Airtel は ₦50,000、9mobile は ₦50,000 です。電気料金の最低額も、₦100(Ikeja、Aba)から ₦2,000(Ibadan)まで幅があります。一律の上限では、MTN の有効なチャージを誤って拒否したり、VTpass が拒否する Airtel のチャージを、ユーザーがオンチェーンで支払った後に誤って受け付けたりすることになります。 - 海外の請求書支払い: ユーザーは国を選択し、その国の通貨と料金で海外の通話時間/データを支払えます。取引履歴と領収書にはナイラだけでなく、現地通貨が反映されます。国のリストはすべてのチャネルで VTpass(
/get-international-airtime-countries)からリアルタイムで取得するため、アプリ、チャット、MCP の間で対象国の認識が食い違うことはありません。 - 即時販売: VTpass と API を連携し、トークンの即時生成、通話時間のチャージ、データバンドルの配信を自動化します。
- すべてのチャネルで教育用 PIN を購入可能: WAEC の試験結果照会 PIN と WAEC 登録 PIN は、アプリ、チャット(Telegram/WhatsApp/X/アプリ内)、MCP のいずれからも購入できます。アプリ限定ではありません。⚠️ JAMB については、正確さを期すための注意点があります: コード上の処理経路(インテント解析、プロフィール ID の検証、
variation_codeの処理)は一通り存在しますが、現在の VTpass マーチャントアカウントではjambが有効になっていません。VTpass は{"code":"011","content":{"errors":"Service is Not Valid"}}を返すため、リアルタイムカタログには表示されず、現在は販売できません。アカウントで有効化されれば、コードを変更せずに自動的に表示されます。 - スマートなマーチャント検証: 暗号資産の支払いを受け付ける前に、電気メーター、スマートカード/IUC 番号、アカウント情報を検証し、ユーザーの入力ミスや販売失敗をなくします。
- AbaPoints ロイヤルティシステム: ユーザーは、使用したステーブルコインの価値に1対1で連動するポイントを獲得できます。ポイントはアプリ内のポイントバッジと専用 API エンドポイントで確認できます。
- 自動返金セーフティネット: オンチェーンで支払いが確定した後に販売に失敗した場合、自動的に検出、検証され、ユーザーのウォレットへオンチェーンで返金されます。
- DND フォールバック SMS: 電気トークン/PIN の SMS 配信を自動化し、重要な取引通知についてはナイジェリアの Do-Not-Disturb(DND)登録を迂回します。
- マルチチャネル対応と通知: サポートチケット機能を内蔵し、Telegram、WhatsApp、X(Twitter)との Webhook 連携にも対応しています。ユーザーと管理者は、使い慣れたチャネルから取引や通知の受信ができます。
- 会話型 AI エージェント(「DeAI」): 自然言語アシスタント(
/api/deai)を使い、チャット形式のコマンドで残高の確認や請求書の支払いができます。Claude(Anthropic)を基盤としています。Telegram、WhatsApp、X、およびストアフロント上のアプリ内チャットウィジェット(src/components/AIChat.tsx)から利用できます。メニューの番号だけでなく、インテントを理解します。「Celo」や「usdt」と返信すれば、「1」や「2」と返信した場合と同じように機能します。選択を求める際には、各トークンのリアルタイム残高と承認済みエージェント上限を表示するため、情報がないまま選ぶ必要はありません。セッションが途切れた場合(ネットワーク切断、途中で放置など)は自動的に検出してクリーンアップされ、未処理のまま残りません。また、PIN を入力した直後にネットワーク障害が起きても、支払いが黙って失われたり二重に使われたりすることはありません。エージェントが結果を報告する前に、オンチェーンで確定するまで追跡します。 - エージェント起点の支払い(AbaPayV3): ユーザーは、チェーンごと、ステーブルコインごとに個別に設定できる、上限付きのオンチェーンで取り消し可能な利用許可(
setSpendingAllowance)を、Agent Hub タブから DeAI エージェントに付与できます。これにより、支払い時にウォレット署名を求めることなく、またユーザーの資金を預かることなく、Telegram/WhatsApp/X からユーザーに代わって請求書を支払えます。チャットで必要なチェーン/トークンの利用許可が承認されていない場合、エージェントは事前にそれを検出し、今すぐ承認するか、この1回の支払いを署名付きディープリンクで完了するかをユーザーに提示します。詳しくは、以下のAbaPayV3 — エージェントの利用許可をご覧ください。 - 自律スケジュール設定と自動支払いエージェント: 1回限りのチャット決済に加え、DeAI エージェントに定期請求(月次/週次/日次)、将来の1回限りの支払い(「10分後にこれを支払って」)、または複数の受取人/アカウントを一度に対象とするリクエストを依頼できます。エージェントはチェーン/トークンごとにまとめ、利用許可の上限が適用される同じリレイヤーを通じて、それぞれの支払いを予定どおりに自動実行します。ユーザーによる追加操作は不要です。
- オンチェーンのアトリビューション: AbaPay が送信する Celo 上のすべての取引(アプリでの支払い、承認、エージェントリレイヤー、管理者によるボルト操作)には、両方の Celo タグを含む ERC-8021 サフィックスが付与されます(
src/lib/attribution.ts)。タグは、Celo Open Rails ハッカソン用のcelo_2719403c6affと Celo Builders 用のcelo_9d71588659ecです。x402 の支払いは AbaPay ではなくファシリテーターがオンチェーンで送信するため、タグは付きません。Base の取引には、Base 独自のビルダーコードが付きます。 - オンチェーンのエージェント ID(ERC-8004): AbaPay の DeAI エージェントは、ERC-8004「Trustless Agents」レジストリを通じて、Celo と Base の両方で実際のオンチェーン ID として登録されているため、8004scan.io/AgentScan で検索できます。これは資金の移動方法とは別個のものであり、関連もありません。詳しくは、以下のERC-8004 エージェント IDをご覧ください。
- MCP サーバー(AI エージェントによる支払い): AbaPay は、MCP に対応する任意の AI クライアント(Claude や、Model Context Protocol をサポートするその他のエージェント)から、実際のツールサーバーとして利用できます。
describe_capabilities、check_balance、list_plans、pay_bill、複数の受取人向けのpay_bill_batch、および定期/1回限りのschedule_bill/list_schedules/cancel_scheduleを、/api/mcpの Streamable HTTP JSON-RPC 経由で提供します。Telegram/WhatsApp/X に加わる第4のチャネルであり、新たな信頼境界ではありません。チャットチャネルとまったく同じ、利用許可上限の適用、キルスイッチによる制御、割引対応を備えた実行パイプラインを通じて動作し、リンク済みウォレットが承認したチェーンに応じて、Celo または Baseのいずれかを利用します。詳しくは、以下のMCP サーバーをご覧ください。 - MCP OAuth 2.1(会話ごとではなく、最初に一度だけ認可): コネクターは、Dynamic Client Registration(
/api/oauth/register、/api/oauth/authorize、/api/oauth/token、/.well-known/配下のディスカバリー)を備えた、OAuth 2.1 認可コード+PKCE(S256)のフローに完全対応しています。ユーザーはブラウザーで一度認可します。AbaPay 独自の手書きの同意ページで API キーとPIN を証明すれば、以後の会話では API キーを再入力せず、Bearer トークンで接続できます。OAuth は支出を認可しません。pay_billを呼び出すたびに PIN が引き続き必要であり、Bearer トークンだけでできるのは残高の読み取りに限られます。OAuth に対応していないクライアント向けには、api_keyツール引数も引き続き利用できます。 list_plans— VTpass の実際のプランコードと価格を返し、推測しません: DATA/CABLE/EDUCATION について、list_plansは現在購入可能なプランと、その正確なvariation_codeおよび VTpass のリアルタイム価格を返します。ツールの説明とサーバーの指示の両方で、推測するのではなくpay_billの前に呼び出すようクライアントに伝えます。- x402 決済(メインアプリ、両チェーン対応): Web アプリで直接行った支払いは、x402 HTTP-payment プロトコルを介して決済します。Celo 上の USDC/USD₮/USAT には Celo 独自のファシリテーター、Base 上の USDC には Coinbase CDP ファシリテーターを使うため、コントラクト呼び出しを別名で表示するのではなく、実際に x402scan にインデックスされます。EIP-3009 に対応していない場合は、Base のガス代スポンサー付き経路も含め、オンチェーンの
payBillフローを使います。⚠️ x402 には EIP-3009 のtransferWithAuthorization署名が必要です。これはドレイナーが要求する署名と構造的に同じであるため、一部のウォレットスキャナーはリスクありと判定します。x402scan で可視化するために意図的に選択した既知のトレードオフです。無効にするにはNEXT_PUBLIC_X402_ENABLED=falseを設定します。署名不要のエージェント起点フローには、どちらの場合も影響しません。詳しくは、以下のx402 決済をご覧ください。 - 動的為替エンジン: ライブ市場レートによる換算、管理者が設定できる為替レート、自動的な利益幅の計算を行います。過少支払いを狙う不正を防ぐため、サーバー側で検証します。
- エグゼクティブ管理者ダッシュボード: VTpass の法定通貨残高、トークン/チェーンごとのオンチェーンボルト残高、取引分析、手動返金ツール、CSV エクスポートをリアルタイムで確認できます。管理者認証で保護されています。
- すべてのチャネルで確実に停止するキルスイッチ: ダッシュボードの「サービスを一時停止」トグルは、サービス単位のマスタースイッチ(
MASTER_AIRTIME、MASTER_INTERNET、MASTER_ELECTRICITY、MASTER_CABLE、MASTER_EDUCATION、MASTER_INTERNATIONAL)と、VTpass の serviceID をキーとするプロバイダー単位のスイッチ(AIRTIME_mtn、INTERNET_airtel-data、ELEC_ikeja-electric、CABLE_dstv、EDU_waec)からなる2段階のモデルです。どちらか一方がオフなら、支払いは拒否されます。src/lib/serviceRules.tsのkillSwitchKeysFor()は、エージェントのインテント(プロバイダーを含み、resolveServiceIdで正規化。これによりELEC_ikejaがELEC_ikeja-electricと一致しない問題を防ぎます)を、該当するキーに対応付けます。そのため、チャット、MCP、自律スケジューラーでも、Web アプリと同じスイッチが適用されます。詳しくは、以下のキルスイッチをご覧ください。 - Base でのガス代スポンサー: Coinbase Smart Wallet/Base Account のユーザーは、ガス代なしで支払えます。アプリは EIP-5792 経由でペイマスター対応を検出し、承認と支払いを1つのスポンサー付きトランザクションにまとめます。この機能に対応していないウォレット(MetaMask、WalletConnect、Valora など)では、通常の自己負担フローにシームレスに切り替わります。
- 共有・ダウンロード可能な領収書: すべての領収書は、デバイスのネイティブ共有シートから WhatsApp/Telegram などへ画像として直接共有するか、PNG または PDF として直接保存できます。
- Farcaster Mini App 対応: Farcaster フレームのメタデータを含んでいるため、Farcaster クライアント内から AbaPay を直接起動できます。
- NIM で支払う Nimiq Pay ミニアプリ: nim.abapays.com — Nimiq Pay の Mini Apps に掲載されています(nimiq/awesome#45、2026-10-20 にマージ、初の NIM 購入は 2026-10-24)。(
src/app/nim/。middleware.tsでマッピング。)メインアプリで販売するすべての商品を、Nimiq Pay(@nimiq/mini-app-sdk)内から NIM トークンのみで購入できます。POST /api/pay/nimは、NIM の販売価格に基づいて請求額を NIM で算出します。nimiq-2の対象 CoinGecko 取引所市場(米ドルステーブルコインのペア、新鮮なデータで異常値ではなく、取引高が $500 以上)のうち最も低い最良買い気配値を採用し、ブレンド済み CoinGecko 価格を上回らないようにします。このブレンド済み価格はフォールバックとしても使用されます。さらにNIMIQ_PRICE_BUFFER(デフォルトは2%)を加え、管理者のplatform_settings.exchange_rate(abapays.com と同じナイラレート)で換算し、NIM の整数に切り上げます。注文を記録する前に支払者の残高を確認することはありません。Mini App API にはウォレット全体の残高を取得するメソッドがなく、計算値では Nimiq Pay の HTLC の一部を見落とす可能性があるためです。正確な残高を把握している Nimiq Pay が、残高不足の送金を拒否します。支払いがオンチェーンで行われるまで、料金は一切発生しません。アプリが表示する残高(残高が見つかった場合のみ。見つからなければ「—」。Nimiq Pay を参照)は、Nimiq Pay のgetBalanceではなく、チェーンの読み取り(GET /api/pay/nim/balance?address=)から取得します。getBalanceは、資金のあるウォレットでも 0 を返したためです。ホスト側の残高はフォールバックとしてのみ使用します。Nimiq Pay は、ユーザーが自身のアドレスに資金を入れた HTLC に NIM を保持します(基本アカウントの残高は 0)。そのため、ウォレットの残高は基本残高に加え、未期限切れで送信者となっているすべての HTLC の残高を合算します。これには、古い HTLC の期限が切れる前に Nimiq Pay が残高を移す新しい HTLC も含まれます(HTLC → HTLC の移動であり、自身の出金取引だけでなく、ウォレットの HTLC をたどることで検出します)。また、そのような HTLC から届いた支払いは、チェーン上でそのウォレットが資金を提供したと確認できる場合(htlcFunder)、注文したウォレットによる支払いとして扱われます。abapays.com に適用されるすべての管理者設定は、同じcheckWebPaymentゲート(キルスイッチ、プロバイダーブレーカー、リアルタイムの最小/最大金額)を通じて適用されます。見積もり(GET /api/pay/nim)にはサービスごとの利用可否が含まれ、無効なサービスはアプリ上でグレー表示されます。POST { check: true }は、書き込みを行わずに1つのプロバイダーと金額を検証し、財務資金の送付先アドレス(NIMIQ_TREASURY_ADDRESS)と注文参照番号を返します。Nimiq Pay は、その参照番号を支払いのプレーンテキストメッセージとして付けて NIM を送信します(Nimiq Pay はdataを UTF-8 として保存し、通常のトランザクションは最大64バイト(MAX_NIM_MEMO_BYTES)です。47バイトの参照番号を16進数にエンコードすると2倍の長さになって上限を超え、支払いが Nimiq Pay で Pending のままになっていました)。Nimiq Pay のすべての支払いは、ユーザーの HTLC から支出され、Nimiq のコサイナーが共同署名します。サーバーは Albatross JSON-RPC(NIMIQ_RPC_URL。デフォルトはhttps://rpc.nimiqwatch.com)からトランザクションを読み取り、販売処理に進む前に、財務資金の送付先が受取人であること、見積もり以上の luna が送られていること、この注文の参照番号があること、注文したウォレットが送信者であること、およびNIMIQ_MIN_CONFIRMATIONS(デフォルトは10)を満たしていることを確認します。他のすべての決済手段と同じパイプラインを通じて販売します(src/lib/nimiq.ts、src/lib/nimSettle.ts)。/api/cleanupは、支払いの報告前にアプリが閉じられた注文(財務資金の取引履歴から参照番号で特定)を決済し、1時間支払いのない注文を期限切れにします。NIM の行はblockchain = 'NIMIQ'、token_used = 'NIM'となり、請求された NIM はamount_usdt、見積価格はnim_usd_priceに記録されます(マイグレーション 042)。サーバーは Nimiq の秘密鍵を保持しません。販売に失敗した場合は返金をキューに登録し、運用担当者が財務資金ウォレットから返金を送信し、Admin → Ops → Refunds で記録します。そこで返金ハッシュが Nimiq チェーン上で検証されます。システムが自らキューに登録していない NIM 注文(処理が止まっている、または VTpass に記録がない場合)は、Admin → Ledger → Refund からキューに登録できます(POST /api/admin/refunds { action: "QUEUE_NIM" }:オンチェーンで支払い済み、未配達、かつ未登録の注文)。アプリは要求があった場合にのみ接続します。Nimiq Pay の接続プロンプトを拒否すると「Connect wallet」ボタンが表示され、切断するとそのデバイス上のアカウント情報が削除されます。アプリの History タブ(POST /api/pay/nim/history)には、支払者自身の注文が表示されます。支払い待ち(Nimiq Pay が送金を保留中にすることがあります)、期限切れ・未払い(請求なし)、返金処理中の遅延支払いも表示され、それぞれに1行の説明が付きます。内部注文用のプレースホルダーが支払いハッシュとして返されることはありません。メーターのトークンと PIN については、ユーザーが Nimiq Pay で短いメッセージに署名し、src/lib/nimiqAuth.tsが Nimiq の署名済みメッセージダイジェストに対する Ed25519 署名と、公開鍵からメッセージに記載されたアドレスが導出されることを検証します。そのため、あるウォレットの署名で別のウォレットの注文を読むことはできません。プロバイダー選択画面は検索可能なボトムシートで、各プロバイダーのロゴ、国旗、または VTpass の事業者画像を表示します。
🛠️ Tech Stack
- フロントエンド: Next.js 16(App Router、React 19)、Tailwind CSS 4、Lucide Icons、next-themes(ダークモード)
- Web3/ウォレット: Wagmi、Viem(スポンサー付きトランザクション用の EIP-5792
sendCallsを含む)、WalletConnect Modal、Base Account SDK、Solidity スマートコントラクト(Hardhat) - バックエンド: Next.js Route Handlers(サーバーレス関数)
- 領収書: html2canvas(画像キャプチャ)、jsPDF(PDF エクスポート)
- AI: DeAI 会話型エージェントとアプリ内チャットウィジェット向けの Claude(Anthropic API)
- エージェント ID と決済: ERC-8004(オンチェーンのエージェント ID、Celo + Base)と x402(接続済みウォレットで署名。
src/lib/x402Pay.ts)を使用した、メインアプリでの HTTP ネイティブなファシリテーター決済 - MCP、A2A、x402 での Self エージェント ID(
src/lib/selfAgent.ts):エージェントはリクエストに署名できます(x-self-agent-address/-signature/-timestamp。keccak256(timestamp + METHOD + path?query + keccak256(body)を対象に署名)。AbaPay は SelfAgentRegistry で署名者を確認し(検証済み、新しい人間証明、Self のプロバイダー)、各レスポンス(X-AbaPay-Self-Agentヘッダー、JSON のself_agent、JSON-RPC のresult._meta["abapay/self_agent"])と決済に結果を表示します。Self は任意です。署名が確認できない場合は 401SELF_AGENT_INVALID` で拒否されます。期限切れ、取り消し済み、未登録のエージェントも、未検証として決済できます。ポリシーと例:docs/gitbook/self-agent-id.md。 - Grant レポート(管理者 → Grant):
/api/admin/grant-report?from=&to=[&format=csv](src/lib/grantReport.ts)は、指定期間に外部エージェントがエージェント用経路を通じて行った Celo 決済の成功件数を集計します。対象は x402 エージェント(source_channelAGENT)とリレーヤー経由の MCP/A2A エージェントです。ウェブアプリ、AbaPay 独自のチャットエージェント、返金/失敗、およびGRANT_EXCLUDED_WALLETS(管理者/リレーヤー/サーバーのウォレットを含む)は除外されます。Prezenti の目標に対するエージェントごとの帰属情報(ERC-8004、OAuth クライアント、Self Agent ID)と最終レポート用の決済ごとの CSV も含まれます。 - 支払者情報(管理者のみ): すべての x402、MCP、A2A 決済では、リクエストの User-Agent と、申告されたエージェント名/ERC-8004 ID(
X-Agent-Name、X-Agent-Id)をpayment_client_infoに記録します。MCP/A2A の場合は、プロトコル、サインイン方法、OAuth クライアントの登録名も記録します(ルートは各ツール呼び出しをrunWithAgentClientのもとで実行し、executeAgentPaymentが記録します。独自のチャットエージェントとスケジューラーは記録しません)。また、レスポンス後にオンチェーンで支払い元ウォレットを検索します(src/lib/payerIdentity.ts):所有する、または申告ウォレットである ERC-8004 エージェント ID をownerOf/getAgentWalletでそれぞれ確認します(ID は Blockscout から取得し、取得できない場合はCELO_RPC_URL上のレジストリログを使用)。SelfAgentRegistry から Self Agent ID の状態(検証済み、人間証明、証明の有効性)も取得します。結果はpayer_identitiesに 1 日間キャッシュされます。両テーブルはサービスロールのみがアクセスでき(マイグレーション 044)、管理者用台帳にのみ表示されます。領収書や公開 API には表示されません。検索によって決済に影響が及ぶことはありません。サポートでの確認方法:docs/runbooks/README.md、「エージェントが支払った開発者へのサポート」。 - エージェントのツールアクセス: MCP(Model Context Protocol)—
/api/mcpにある Streamable HTTP/JSON-RPC サーバーは、残高確認と請求書支払いのツールをすべての MCP クライアントに公開します。A2A(Agent2Agent)—/api/a2aにあり、/.well-known/agent-card.jsonにカードを公開し、同じツールをピアエージェントに提供します。x402 エンドポイントは/.well-known/x402にある x402 ディスカバリーマニフェスト({ version: 1, resources: [...] })にも掲載されています。agent402.tools などの x402 インデクサーは、ライブの 402 チャレンジを確認する前にこのファイルを読み込みます。AbaPay はここにインデックスされています(agent402.tools/api/index?seller=agents.abapays.com)。Cursor: cursor.directory/plugins/abapay に掲載されています。プラグインはintegrations/cursor-plugin/にあります(.cursor-plugin/plugin.json+https://agents.abapays.com/api/mcpを指すmcp.json。リポジトリルートの.cursor-plugin/marketplace.jsonによりマーケットプレイスに登録)。OAuth は Cursor のネイティブコールバック(cursor://anysphere.cursor-mcp/oauth/callback。完全一致のみ、PKCE 必須)と、新しいループバックコールバックに対応しています。Grok: grok.com/connectors → New Connector → Custom に、同じ URL を追加します。 - ユーティリティプロバイダー: VTpass API(請求書、通話時間、データ、教育、国際通話時間)
- 銀行振込プロバイダー: Monnify API(Moniepoint Inc.)— 口座の自動検出、Name Enquiry による確認、Moniepoint Microfinance Bank の事業用口座から引き落とされる実際の NUBAN 振込
- データベース/台帳: Supabase(PostgreSQL)— トランザクション、プラットフォーム設定、ポイント、返金
- メール: Resend(トランザクション用領収書メール)
- 通知とボット: Telegram Bot API、WhatsApp Cloud API、X(Twitter)API、VTpass Messaging API(SMS)
👛 対応ウォレットと環境
AbaPay は、3 種類の異なる実行環境で動作します。環境は src/app/page.tsx の読み込み時に検出されます
(environment = MINIPAY | FARCASTER | WEB。検出前の状態は LOADING で、2 秒のタイムアウト後に WEB にフォールバックします)。WEB の場合のウォレット接続は
src/config/wagmi.ts によって提供され、コネクターは injected()、baseAccount()、
walletConnect() のちょうど 3 つが登録されています。
| ウォレット/環境 | 接続方法 | 備考 |
|---|---|---|
| MiniPay(Opera Mini 組み込みの Celo ウォレット) | window.ethereum.isMiniPay で直接検出し、アプリが独自の viem ウォレットクライアントを構築して Celo に固定します | ガス代はステーブルコイン(txConfig.feeCurrency)で支払うため、ユーザーは CELO を用意する必要がありません。ネットワーク切り替えは意図的に無効化されています。 |
| Farcaster Mini App | @farcaster/miniapp-sdk の sdk.context で検出し、sdk.wallet.ethProvider を使用して Base に固定します | アプリを開いてもウォレットのポップアップが強制的に表示されないよう、アドレスは サイレントに getAddresses() で読み取られます。フレームのメタデータは public/.well-known/farcaster.json にあります。操作できないネットワークバッジの横に、独自の終了ボタンがあります。 |
| Valora | WalletConnect のみ — Valora のアプリ内ブラウザーでは、注入されたプロバイダー経由の接続を意図的にスキップします(isValoraBrowser()) | explorerRecommendedWalletIds によって、WalletConnect モーダルのおすすめリストの先頭に固定されます。Celo のみに制限され、アプリも自動的にそれに従います(walletApprovedChainIds())。注入経由の接続を無効にしている理由については、以下の「Valora は WalletConnect のみ」を参照してください。 |
| MetaMask およびその他の注入型ブラウザーウォレット | ウォレットが通知した EIP-6963 で検出されたコネクターを使用し、該当するものがない場合は汎用の injected() にフォールバックします | wagmi はインストールされているウォレットごとに 1 つのコネクターを検出します(multiInjectedProviderDiscovery はデフォルトで有効)。QR コードではなくこの方法を使うことで、web3 ブラウザーのユーザーが QR コードを表示されずに済む理由については、以下の「接続ボタンの選択方法」を参照してください。 |
| Coinbase Smart Wallet/Base Account | baseAccount() コネクター | ガス代のスポンサー対象となる唯一のウォレットです。アプリは EIP-5792 のペイマスター機能を確認し、承認と支払いを 1 つのスポンサー付き呼び出しにまとめます。それ以外のすべてのウォレットでは、通常の自己負担フローにフォールバックします。 |
Base App(isBaseAppBrowser() で検出される、Base App 独自のアプリ内ブラウザーでサイトを開いた場合) | 上記と同じ baseAccount() コネクターを使用しますが、MiniPay/Farcaster と同様に自動接続され(以下の許可リストを参照)、UI では Base に固定されます。ネットワーク切り替えメニュー、フッターのネットワーク表示、トークン選択には Base のみが表示され、切り替え可能な Celo は表示されません | 上記の「Coinbase Smart Wallet」とは異なります。通常のブラウザーでそのコネクターを選択した場合は、引き続き両方のチェーンを使用できます。この行は、ページ自体が Base App 内で実行されている場合に限られます。Farcaster と同様、操作できないネットワークバッジの横に独自の終了ボタンがあります。 |
| その他すべての WalletConnect v2 ウォレット(Trust、Rainbow、Ledger Live など) | QR モーダルを使用する walletConnect() コネクター | コードにウォレット固有の処理はありません。WalletConnect に対応し、Celo または Base をサポートしていれば動作します。 |
接続ボタンの選択方法
注入されたウォレットが常に優先されます。サードパーティのホストに接続しないため、WalletConnect のリレーをフィルタリングするネットワークでも引き続き利用できます。WalletConnect は、ウォレットがないブラウザー(一般的なデスクトップブラウザーや、ウォレットアプリとペアリングするスマートフォンのブラウザー)での代替手段です。
利用可能なウォレットは、window.ethereum を読み取るのではなく、必ず問い合わせて確認します。probeInjectedConnectors()(src/lib/walletEnv.ts)は、wagmi が検出したコネクターを取得し、それぞれのプロバイダーにタイムアウト付きの eth_accounts を送信します。これはプロンプトを表示しない呼び出しなので、ページの読み込み時にいつでも安全に実行できます。各ウォレットは、authorized(このサイトへの接続をすでに承認済み)、available(実在するが、まだ承認されていない)、none(存在しない、または応答しないスタブ)のいずれかとして返されます。
authorizedのウォレットがある場合 → 自動では何も起こりません。authorizedによって決まるのは、権限許可のポップアップなしで選択画面に表示できるウォレットであり、接続するかどうかではありません。詳しくは、以下の「自動接続は許可リスト方式」を参照してください。- 選択画面には、使用可能な注入済みウォレットすべてに加えて、Base Account と WalletConnect が表示され、それぞれに固有の EIP-6963 ロゴとステータスバッジが付きます(このサイトへの接続をすでに承認済みのウォレットには
Recent、それ以外にはInstalledが表示されます)。キャンセルすると試行が終了し、QR コードへの切り替えは行われません。EIP-6963 でアナウンスされ、かつwindow.ethereumに設定されている拡張機能は重複排除されるため、二重に表示されることはありません。 - Base Account は注入済みウォレットと並んで表示されます。 コネクターは
src/config/wagmi.tsにあり、probeInjectedConnectors()はinjectedタイプのコネクターを返します。Base Account は独自のコネクタータイプを持つため、選択肢に個別に追加されます。これは、デフォルトのチェーンであり、ガス代がスポンサーされるスマートアカウント体験を提供する Base で特に重要です。verifySignatureAcrossChainsは、Base Account が生成する ERC-1271 署名を検証します。 - この 2 つは常に選択肢に含まれるため、選択画面は必ず表示されます。拡張機能のないブラウザーでも、QR コードに直接進むのではなく、Base Account でのサインインと、スマートフォンのウォレットとのペアリングから実際に選択できます。
🔵 WalletConnect は常に選択肢であり、単なる代替手段ではありません。 まず選択肢のリストを作成し、そのリストの長さに基づいて選択画面を表示するかどうかを決めます。そのため、よくある「拡張機能が 1 つインストールされている」場合でも、WalletConnect は拡張機能のないブラウザーに限った代替手段ではなく、スマートフォンのウォレットとペアリングするための実際の選択肢として表示されます。
🔵 window.ethereum を使わない理由: EIP-6963 では、ウォレットはそのグローバル変数を占有するのではなく、イベントを通じて自身の存在をアナウンスします。これにより、複数の拡張機能が同じスロットを奪い合うことなく共存できます。グローバル変数そのものではなく、EIP-6963 でアナウンスされたコネクターを調べることで、window.ethereum が未設定である、または別のウォレットを指しているというだけで、ウォレットが正常に使えるブラウザーをウォレットなしと判定することはありません。
プロンプトには、どこで承認するかも明記されています。WalletConnect では、リクエストは別のアプリに届きますが、そのアプリを前面に表示するものはありません。そのため、文言ではアプリを開くよう案内しています(walletApprovalPrompt)。
Valora は WalletConnect 専用です
🔵 Valora 内で挿入型の経路をスキップする理由。 Valora のアプリ内ブラウザーでは、ページから eth_accounts に応答する何かが見えることがあります。ウォレットを報告するには十分に実体があり、自動接続を発火させるにも十分で、UI 上では接続済みに見えるほどです。しかし、支払いに使うには不十分です。Valora の挿入型プロバイダーは、支払い承認リクエストを接続ハンドシェイクとして受け取り、消費したうえでページに何も返しません。そのため、リクエストは解決されません。
Valora がサポートする経路は WalletConnect であり、WalletConnect 経由なら通常どおり動作します。実際の応答を伴う、実際のセッションリクエストです。そのため、Valora 内では挿入型の経路をスキップします。isValoraBrowser() が自動接続を抑制し、Connect ボタンの挿入型候補リストを空にすることで、クリックを WalletConnect に引き継ぎます。
🔵 Valora の検出には、ページ自身のグローバル変数ではなく、WalletConnect セッションが必要です。
isValoraBrowser() は isValora フラグまたはユーザーエージェント内の名前を探しますが、Valora のアプリ内ブラウザーではどちらも存在しません。プロバイダーは挿入されず、WebView は標準の Android Chrome ユーザーエージェントを報告します。ウォレットの名前を示すのはセッションだけです。WalletConnect は接続時にピアのメタデータを交換し、session.peer.metadata.name にはウォレット自身が名乗る名前が入っています。
そこで connectedWalletIsValora() は代わりにその値を読み取り、復元された Valora セッションをマウント時に切断して、ユーザーが新たにペアリングできるようにします。この動作の範囲を意図的に絞っているのは、この手間をかける意味があるのが次のケースに限られるためです。
- Valora のみ — それ以外のすべてのウォレットでは、復元されたセッションを維持します。
- 復元されたセッションのみ — ユーザーがたった今要求した接続が強制的に切断されることはありません(
userInitiatedConnect)。 - マウントごとに 1 回。接続処理中に競合することはありません。
この方法のトレードオフは、ピアのメタデータは接続後にしか存在しないため、接続を事前に阻止するのではなく、その後の動作を決める点です。どちらの検出処理も単語境界を使います。誤検知すると、正常に動作しているセッションが切断されたり、実際にブラウザー内にあるウォレットが、本来使うべき経路から外されたりします。これはより大きな損失につながる誤りです。tests/walletEnv.test.ts でテストされています。
ウォレットでのキャンセルが、常に応答を返すとは限りません
すべてのキャンセル経路は、ウォレットが拒否を報告することを前提としています。EIP-1193 ではそうすることが定められており、挿入型ウォレットもそのとおりに動作します。しかし、WalletConnect 経由の Valora はそうではありません。シートを閉じてもリレー経由で何も返されないため、捕捉できる拒否も、エラーも、イベントもありません。リクエストは開いたままになり、ページはすでに下された判断を待ち続けます。
withWalletTimeout は 90s で発火します。承認前にプロンプトを読む時間でもあるため、この制限時間は 90s のままにする必要があります。そのため、処理が 15s 続くと、ステータスバナーに 待機を終了 コントロールが表示されます。進行中のリクエストを中断することはできず(こちら側では何もできません)、支払いがキャンセルされたとも意図的に表示しません。ユーザーが少し後に承認すれば、支払いは確定するからです。そうでないと伝えることは、二重払いを招くことになります。
自動接続は許可リスト方式です:MiniPay、Base App、Farcaster のみ
ウェブでは、接続ボタンだけが接続手段です。 ユーザーが接続を求めるまで、ウォレットは接続されません。数か月前に拡張機能がこのサイトを承認していたウォレットも例外ではありません。
🔵 reconnectOnMount={false} は Providers.tsx で意図的に設定されています。 wagmi の reconnectOnMount のデフォルトでは、コネクターが保持され、ページを読み込むたびに暗黙的に再接続されます。これはプロバイダー内で行われるため、page.tsx のエフェクトが実行される前に発生します。これを false に設定すると、明示的な connect() 呼び出しだけがセッションを確立します。
自動接続は AUTO_CONNECT_SURFACES による許可リスト方式です。以前「authorized」だったウォレットをデフォルトで接続し、名前を指定して例外を設けるルールではありません。許可リスト方式なら、後から別のウォレット名で暗黙的な接続が再び現れるのを防げます。
この3つは程度の違いではなく、性質が異なります。アプリはウォレットの内部で動作するため、対象となるアカウントは1つだけです。ユーザーはそこで AbaPay を開くことでそのアカウントを選択しており、選択肢がないので、選択画面を省略しているわけでもありません。MiniPay と Farcaster はそれぞれ独自の SDK で接続され、wagmi には一切触れません。Base App は wagmi 経由で接続され、looksLikeBaseApp() によって判別されます。この判別は、通常のページ上では一般的なインジェクション型ウォレットでありながら、同じ isCoinbaseWallet フラグを設定する Coinbase デスクトップ拡張機能を意図的に除外します。
⚠️ トレードオフ: ウェブセッションは更新すると終了し、ユーザーはもう一度 接続を押す必要があります。確認を求めることが目的ですが、ページを頻繁に再読み込みするユーザーにとっては実際の負担です。
ページが確立していない接続は、接続ではありません
reconnectOnMount={false} は、wagmi がコネクターを再接続するのを防ぎます。しかし、再ハイドレートは防ぎません。設定は ssr: true で cookieStorage に保持されるため、読み込み時に wagmi は Cookie から connections/current を復元します。その結果、プロバイダーがセットアップされておらず、リレーソケットも存在しない状態でも、useAccount() はアドレスとともに isConnected を報告します。
🔵 再ハイドレートされた Cookie セッションとライブセッションは、useAccount() から見ると同じです。どちらもアドレスとともに isConnected を報告します。そのため、このページ自身が確立していない接続はマウント時に切断されます(両者を区別するのが userInitiatedConnect です)。Base App には影響しません。Base App の暗黙的な接続では、明示的に connect() が呼び出されるためです。
セッションごとに一度、ウォレットの所有者であることを証明する
🔵 クライアントが記述したフィルターは、権限ではありません。 アドレスパラメーターを使って匿名キーでブラウザーから履歴を直接読み取ることはありません。.ilike('wallet_address', address) のようなクライアント指定のフィルターは、クライアントと同じ程度にしか信頼できません。また、ウォレットアドレスは誰でも指定できる公開情報です。
接続後、ウォレットは明確な文言の所有権確認メッセージに署名します(src/lib/walletSession.ts はブラウザーとサーバーで共有されています。同じ文字列を別々に複製すると、たった1文字の違いで署名がすべて「無効な署名」になるためです)。GET /api/history はその署名からアドレスを導き出し、サービスロールクライアントでクエリを実行するため、他の人の記録を指定できるパラメーターは残りません。
verifySignatureAcrossChainsは EOA と ERC-1271/6492 スマートアカウントの両方にすでに対応しているため、確認できない形式の署名を理由に Base Account や Safe が締め出されることはありません。- 拒否した場合は切断されます。ユーザーが自分のアドレスであることの証明を拒んだからです。
- それ以外のエラーでは、接続状態は維持されますが、所有権は未証明のままです。それでも支払いはできます。支払いは支払い署名自体によって認証されるためです。制限されるのは履歴の閲覧だけであり、このエンドポイントが確認する必要のない署名形式を理由に、完全に利用を拒否することはありません。
- 読み取り専用で、5分間ではなくセッション中有効です。履歴を更新するたびにウォレットのポップアップを表示すると、ユーザーは表示されたものに何でも署名するようになってしまうためです。このセッション中はベアラー認証情報として機能します。変更操作では、操作ごとに新しい署名を引き続き使用します(
verifyWalletOwnership)。
復元された WalletConnect セッションは、ライブセッションではありません
🔵 wagmi は WalletConnect セッション(cookieStorage)を永続化し、読み込み時に復元するため、アドレスが表示されます。UI が接続済みと判断するのに必要なのはアドレスだけです。残高は表示され(公開 RPC から取得されるため、ウォレットにはアクセスしません)、支払いボタンが有効になり、すべて正常に見えます。
しかし、WalletConnect のリクエストがスマートフォンに届くのは、リレーソケットが開いている場合だけです。ソケットが切断されていると、eth_sendTransaction は閉じたパイプに書き込まれます。ウォレットにプロンプトは表示されず、何も返ってきません。そして、拒否されたものはないため、捕捉できるエラーもありません。リクエストはただ届かず、ユーザーがまだウォレットを確認していない状態と見分けがつきません。
walletConnectSessionLive()(src/lib/walletEnv.ts)は、ウォレットを操作する前にリレーを確認します。セッションが切断されている場合は、その旨を簡潔に報告して切断するため、Connect を押すと同じ古いセッションを復元するのではなく、新しい接続が確立されます。ソケット内部の情報を取得できない場合は、ライブとして扱います。誤検知で、支払いのたびに正常なウォレットを切断してしまうのを防ぐためです。インジェクション型ウォレットはプロセス内で動作し、切断されるソケットがないため、null を返します。
チェーン切り替えのハンドシェイクや、これまでタイムアウトがなかった Base の sendTransaction も含め、すべてのウォレット呼び出しにタイムアウトが設定されました。ウォレットアプリでは、タイムアウトが発生した場合、ほとんどの場合がそうであるため、再接続を促すとともに「ウォレットにリクエストが届きませんでした」と報告します。
関連するガードとして、walletApprovedChainIds() があります。WalletConnect ウォレットは、承認済みセッションに含まれないチェーンへのリクエストを通知せずに破棄します。そのため、接続中のウォレットが現在のチェーンを承認していない場合、アプリはウォレットが承認済みのチェーンに切り替えます。
デフォルトのチェーンは Base です
src/constants/index.ts の DEFAULT_CHAIN は BASE です。新しく接続したウォレットの接続先、ウォレット接続前にトークン選択欄へ設定される初期値、呼び出し側がチェーンを指定しない場合にエージェントリンクが承認するチェーンなど、今後の処理はすべてこの値を参照します。Celo も完全にサポートされており、切り替え可能です。サポートがなくなったわけではなく、最初に選択されないだけです。
wagmi.ts に登録されているチェーンの順序は、Base、Base Sepolia、Celo、Celo Alfajores です。wagmi は chains[0] をデフォルトとして扱い、残りは WalletConnect の オプション ネームスペースとして提供するため、Celo だけに対応したウォレットでも問題なく接続できます(上記の Valora の行を参照)。なお、アプリ独自の wagmi を使わないパス(src/lib/chain.ts、page.tsx)では、Celo のテストネットに viem の celoSepolia を使用していますが、wagmi.ts では引き続き celoAlfajores が指定されています。メインネットには影響しませんが、テストネットで WalletConnect のフローを利用する場合は、両者を統一する必要があります。
LEGACY_RECORD_CHAIN は意図的に対となる設定で、CELO のままです。これは、blockchain 列が空の保存済みレコードを読み込むために使います。このようなレコードは列への書き込みが始まる前に作成されたもので、すべて Celo 上のものでした。この値は DEFAULT_CHAIN に追従させてはいけません。古い Celo の支払いを Base として読み込むと、Webhook が誤ったチェーンでレシートを探すことになり、実際の支払いが未払い出しのまま取り残されます。
ステーブルコインは、両方のチェーンに USD₮ と USDC、Celo メインネットにのみ USAT があります。各チェーンでどのトークンを先頭に表示するか、残りをどの順序で表示するかは、src/constants/index.ts の TOKEN_ORDER_BY_CHAIN で定義されています。Base:USDC、USD₮ の順。Celo:USD₮、USDC、USAT の順です。Pay タブ、Agent Hub、チャットエージェント、MCP ツールはすべて tokensForChain() を使用します。関数を一つにすることで、4 つすべてで各チェーンが提供するトークンを常に一致させています。
📁 プロジェクト構成
src/
├── app/
│ ├── page.tsx # Main storefront (pay flow, wallet connect, history, env detection)
│ ├── admin/page.tsx # Admin ops dashboard (incl. the kill-switch toggles)
│ ├── docs/page.tsx # Docs & FAQ page
│ ├── terms/, privacy/ # Legal pages (standalone routes; the in-app modals live in components/Modals.tsx)
│ ├── .well-known/ # OAuth discovery metadata, incl. the RFC path-insertion variants
│ │ ├── oauth-authorization-server/{route.ts, api/mcp/route.ts}
│ │ └── oauth-protected-resource/{route.ts, api/mcp/route.ts}
│ └── api/
│ ├── pay/ # Core payment + vending endpoint (pay/x402/ is the x402 rail)
│ ├── paymaster/ # Server-side proxy for Base gas-sponsorship (keeps the CDP paymaster key off the client)
│ ├── providers/ # Live VTpass provider catalogue for the browser's pickers
│ ├── requery/ # Delayed/timeout transaction requery
│ ├── rate/, admin/rate/ # Exchange rate endpoints
│ ├── variations/ # VTpass service variation lookups
│ ├── intl/, foreign/ # International bill pay (countries/products/operators/rates)
│ ├── verify/ # Meter/account/customer verification
│ ├── admin/ # Admin data, actions, refunds, health
│ ├── discounts/ # Discount campaign lookup
│ ├── schedules/ # Recurring + one-off scheduled bill execution
│ ├── user/points/ # AbaPoints balance
│ ├── agent/ # Agent link/allowance management (Agent Hub)
│ ├── deai/ # Conversational AI agent
│ ├── mcp/ # MCP server (describe_capabilities, check_balance, list_plans, pay_bill, pay_bill_batch, schedule_bill, list_schedules, cancel_schedule)
│ ├── oauth/{register,authorize,token}/ # OAuth 2.1 (DCR, consent page, token endpoint) for MCP
│ ├── cleanup/ # Stale pre-flight intent sweeper
│ ├── webhook/, webhook/vtpass/ # VTpass + on-chain webhooks
│ ├── monnify/ # Moniepoint bank list, account resolve/verify, transfer webhook
│ ├── telegram/webhook/, whatsapp/webhook/, x/webhook/ # Bot channel webhooks
│ └── support/ # Support ticket submission
├── components/ # Shared UI (AppFooter, Modals — Terms/Privacy/FAQ/Receipt —, tabs, AIChat, AgentHub, Admin panels)
├── config/wagmi.ts # Wallet/chain configuration (injected, Base Account, WalletConnect)
├── constants/ # Supported tokens, services, initial country list
├── lib/
│ ├── vtpassCatalog.ts # ⭐ Live VTpass provider catalogue + per-provider amount limits
│ ├── providerFallback.ts # Offline seed used only when VTpass is unreachable
│ ├── monnify.ts # Moniepoint (Monnify) API client — banks, verify, transfer
│ ├── monnifyVend.ts # Bank transfer vend + finalize (success/failure/refund)
│ ├── serviceRules.ts # Kill switches, operator agent caps, min/max amounts
│ ├── refunds.ts # Refund queue (enqueue on vend failure + user notification)
│ ├── vend.ts # Shared vend execution for the contract and x402 rails
│ ├── attribution.ts # Celo (Open Rails + Builders) and Base on-chain attribution (ERC-8021 dataSuffix)
│ ├── parity.ts # Shared validation so chat/MCP match the web form
│ ├── deai/ # Intent parsing, capabilities, selection, relayer (payBillFor),
│ │ # mcpAuth.ts (API key), mcpOAuth.ts (OAuth token lifecycle)
│ └── ... # VTpass, Telegram, WhatsApp, scheduler, discount helpers
└── utils/ # Supabase client, admin auth, PIN hashing
contracts/
├── AbaPay.sol # V1 — original escrow/vault smart contract
├── AbaPayV2.sol # V2 — hardened (see below)
└── AbaPayV3.sol # V3 — adds agent-initiated payments
scripts/
└── deployV4.ts # Deploy V4 (whitelists tokens, sets relayer + per-tx caps)
⚙️ 環境変数
プロジェクトのルートに .env.local ファイルを作成してください。このファイルを GitHub にコミットしないでください。
アプリ/ネットワーク設定
NEXT_PUBLIC_APP_MODE=sandbox # sandbox | production
NEXT_PUBLIC_NETWORK=celo-sepolia # celo-sepolia | celo | base | base-sepolia
NEXT_PUBLIC_FIXED_RATE=1550.00 # Fallback NGN exchange rate
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=your_walletconnect_project_id
NEXT_PUBLIC_WC_RELAY_URL= # Optional. Override the WalletConnect relay — see "Blocked networks" below
ブロックされているネットワーク
外部ウォレットへの接続には、一部のネットワークでフィルタリングされているサードパーティーのホストが必要です。主なものは、relay.walletconnect.org(WalletConnect リレー)と api.web3modal.org(ウォレット選択画面)です。リレーは WebSocket を使用するため、ブロックされるとエラーではなく何も表示されません。その結果、ユーザーには「接続」ボタンが機能していないように見えます。
少なくとも 1 つの通信事業者でこの問題が確認されています。VPN 経由では接続フローが機能しますが、VPN を使用しないと停止します。ただし、影響を受けるネットワークや地域の数はわかっていません。意図的に、ユーザー向けの文言で通信事業者名や国名を挙げていません。その人が該当する通信事業者を利用していない場合に、通信事業者 X が原因だと伝えても、メッセージを信用してもらえなくなるだけです。/network-check は、実際にそのユーザーの環境でブロックされているものを報告します。
対処法は 2 つあります。
/network-check— どのユーザーでも開けるページです。ユーザー自身の接続環境から各依存先を調べ、接続に失敗したものを示します。接続失敗バナーと FAQ からリンクされており、関係することが判明した通信事業者や規制当局に苦情を申し立てる際の証拠としても使えます。NEXT_PUBLIC_WC_RELAY_URL— これを、フィルタリングされていない自分のドメイン上にある WebSocket リバースプロキシ(例:wss://relay.abapays.comからwss://relay.walletconnect.orgに転送)に設定すると、対象ネットワークでも WalletConnect ウォレットが使えるようになります。リレーの通信はエンドツーエンドで暗号化されるため、プロキシは通信を中継するだけで、マン・イン・ザ・ミドルではありません。Vercel の関数では、長時間接続する WebSocket をプロキシできません。Cloudflare Workers、Fly.io、またはproxy_passとUpgradeヘッダーを設定した nginx を実行する VPS でホストしてください。
MiniPay、Base App、Farcaster はこれらのホストを必要としません。前者 2 つはプロバイダーをページに直接挿入し、Farcaster は Mini App SDK を通じて独自のウォレットを提供します。これらはフィルタリングされたネットワークでも安定して動作するため、接続に失敗した場合にアプリが推奨する手段です(src/lib/walletEnv.ts の RELAY_FREE_SURFACES)。
スマートコントラクト(チェーンごと)
NEXT_PUBLIC_ABAPAY_ADDRESS=0xYourDefaultContractAddress
NEXT_PUBLIC_ABAPAY_CELO_ADDRESS=0xYourCeloContractAddress
NEXT_PUBLIC_ABAPAY_BASE_ADDRESS=0xYourBaseContractAddress
ADMIN_WALLET_ADDRESSES=0xYourOpsWallet # optional; admins for /admin (never the vault owner). See ENV_SETUP.md
CELO_PRIVATE_KEY=your_deployer_private_key # Used only by Hardhat for deployment — never expose client-side
Paymaster(Base のガス代スポンサー)
PAYMASTER_URL=https://api.developer.coinbase.com/rpc/v1/base/your_cdp_api_key # Server-only — never NEXT_PUBLIC. The app proxies wallet paymaster requests through /api/paymaster so this key never reaches the browser.
⚠️ この環境変数だけでは対応できないことが 2 つあります。どちらも外部のダッシュボードで設定します。
- Coinbase Developer Platform:
NEXT_PUBLIC_ABAPAY_BASE_ADDRESSコントラクト(できれば特定のpayBill/approveセレクターも)を許可リストに追加する Paymaster Policy を作成し、スポンサー用の残高を入金するか、予算を設定してください。 - Alchemy webhook の設定: Base webhook で 「Token」 アクティビティカテゴリが有効になっていることを確認してください(「External」だけでは不十分です)。ガス代スポンサーを利用する場合、トップレベルのトランザクションの
toは AbaPay コントラクトではなく、バンドラー/EntryPoint コントラクトになります。呼び出しの深さにかかわらず確実に検知できるのは、Token カテゴリ(ERC-20 Transfer ログ)の監視だけです。
VTpass(請求書支払いプロバイダー)
VTPASS_API_KEY=your_api_key
VTPASS_PUBLIC_KEY=PK_your_public_key
VTPASS_SECRET_KEY=SK_your_secret_key
VTPASS_MSG_TOKEN=VT_PK_your_token
VTPASS_MSG_SECRET=VT_SK_your_secret
Monnify(Moniepoint の API — 銀行振込プロバイダー)
MONNIFY_API_KEY=MK_your_api_key
MONNIFY_SECRET_KEY=your_secret_key
MONNIFY_CONTRACT_CODE=your_contract_code
MONNIFY_SOURCE_ACCOUNT_NUMBER=your_wallet_account_number
これらの確認方法と MFA/webhook の設定手順については、ENV_SETUP.md §9b を参照してください。
Supabase(データベース)
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_anon_key
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key # Server-side only — full DB access
Email(Resend)
RESEND_API_KEY=re_your_resend_key
AI エージェント(DeAI)
ANTHROPIC_API_KEY=sk-ant-... # Claude powers the DeAI intent engine (replaced Gemini).
DEAI_INTERNAL_SECRET=any_long_random_string # Optional. Signs internal calls to the DeAI brain so /api/deai/* can't be hit directly from the internet, AND signs the agent's payment deep links. Falls back to SUPABASE_SERVICE_ROLE_KEY if unset.
**DeAI の実際の支払い方法(非カストディアル):**ユーザーのキーをサーバー側で保持することはありません(あってはならず、保持すれば AbaPay がカストディアンになってしまいます)。そのため、エージェントはキーを保持すること以外のすべてを行います。方法は 2 つあります。
- ディープリンク(V1/V2 コントラクト、または利用限度額の設定がないユーザー):エージェントは Claude を使ってリクエストを解析し、実際の VTpass に対してメーター/アカウントを検証して、チャットで詳細を確認した後、アプリを入力済みの状態で開く有効期限 15 分の署名済みディープリンクを返します。ユーザーがタップすると、自分のウォレットで署名が行われ、ウェブアプリと同じ検証済みパイプラインを通じて支払いが実行されます。
- **委任された利用限度額(AbaPayV3、
src/lib/deai/relayer.ts):**ユーザーがオンチェーンのspendingAllowance(下記の AbaPayV3 を参照)を付与している場合、リレイヤーがpayBillFor()を直接呼び出します。ディープリンクも支払い時の署名も不要で、すべてユーザーが設定した利用限度額の範囲内に制限され、ユーザーはいつでも取り消せます。ブロードキャストの前に、preflight_<wallet>_<timestamp>のトランザクション行が書き込まれます(ウェブアプリが署名前に発行するpreflight_<uuid>インテントと同じ考え方です)。確認後に実際の tx ハッシュへと名前が変更されるため、他のすべての決済手段とまったく同じ検証済みパイプラインを通じて支払いが提供され、古くなった試行や放棄された試行も宙ぶらりんのまま残されず、自動的に整理されます。RPC が時間内にレシートを確認できない場合(ブロードキャスト直後のネットワーク障害など。ユーザーが PIN を入力した直後も含みます)、エージェントは失敗ではなく保留中として報告し、同じインテントに対する重複した支払いリンクを決して発行しません。これにより、支払いの喪失と二重請求の両方を回避します。支払いに必要なチェーン/トークンで利用限度額が承認されていない場合、エージェントはリレーを試みる前にそれを検知し、Agent Hub で今すぐ承認するか、この 1 回の支払いだけを署名済みディープリンクで完了するかを選べるようにします。
Telegram
TELEGRAM_BOT_TOKEN=your_admin_bot_token
TELEGRAM_ADMIN_CHAT_ID=your_admin_chat_id
TELEGRAM_CHAT_ID=your_default_chat_id
TELEGRAM_WEBHOOK_SECRET=your_webhook_secret
SUPPORT_TELEGRAM_BOT_TOKEN=your_support_bot_token
DEAI_TELEGRAM_BOT_TOKEN=your_deai_bot_token
WhatsApp Cloud API
WHATSAPP_ACCESS_TOKEN=your_whatsapp_access_token
WHATSAPP_PHONE_NUMBER_ID=your_phone_number_id
WHATSAPP_VERIFY_TOKEN=your_verify_token
WHATSAPP_APP_SECRET=your_meta_app_secret # ⚠️ REQUIRED. Verifies the X-Hub-Signature-256 on inbound webhooks so senders can't be spoofed.
WHATSAPP_SCHEDULE_TEMPLATE_NAME=schedule_update # Approved utility template used when the 24h window has closed. Unset = scheduled payments go unreported on WhatsApp.
WHATSAPP_SCHEDULE_TEMPLATE_LANG=en # Must match the template's language exactly ('en' and 'en_US' are different templates).
WHATSAPP_PIN_FLOW_ID= # Published secure-PIN WhatsApp Flow (scripts/whatsapp-pin-flow.mjs). Set = PINs are entered in a masked WhatsApp form, never typed into the chat. Unset = PIN asked as text.
WHATSAPP_PIN_FLOW_MODE= # 'draft' only while testing an unpublished Flow.
WHATSAPP_BUSINESS_ACCOUNT_ID= # WABA id; only the Flow scripts need it.
WHATSAPP_CHECKOUT_FLOW_ID= # Published checkout Flow (scripts/whatsapp-checkout-flow.mjs). Set = "menu"/"buy"/greetings open a one-form purchase (src/lib/whatsappCheckout.ts); ~3 billed replies per purchase instead of 6–8.
WHATSAPP_CHECKOUT_FLOW_MODE= # 'draft' only while testing an unpublished checkout Flow.
WHATSAPP_FLOW_PRIVATE_KEY= # PEM private key for the Flow data endpoint (/api/whatsapp/flow). Its public key is registered with WhatsApp. Sensitive.
WHATSAPP_DAILY_USER_CAP=20 # Bot replies per user per day (src/lib/whatsappBudget.ts). Past it: one note pointing to Telegram/web, then silence until midnight WAT. A chat waiting for its PIN is never cut off.
WHATSAPP_DAILY_USER_CAP_PAID=10 # The cap once the month passes Meta's free allowance.
WHATSAPP_FREE_MONTHLY_REPLIES=1000 # Meta's free service replies per number per month.
WHATSAPP_MONTHLY_ALERT_AT=800 # Telegram alert to the operator at this many replies in a month (and again past the free allowance).
24 時間のウィンドウと、スケジューラーにテンプレートが必要な理由
🔵 WhatsApp では、ユーザーの最後のメッセージから 24 時間以内に限り、ビジネスが自由形式のテキストを送信できます。ウィンドウの外では、Meta はエラー 131047 で送信を拒否し、送信できるのは事前承認済みのテンプレートだけです。
ビジネス認証を行っても、この制限は解除されません。 認証で決まるのは、ウィンドウの外でメッセージを送信できる相手の人数(250 → 1,000 → それ以上)であり、相手に何を送信できるかには関係ありません。この 2 つは独立しています。
この影響を受ける呼び出し元は src/lib/scheduler.ts です。明日予定された支払いの報告は、スケジュールを作成したチャットが途絶えてからずっと後に送信されます。つまり、設計上 24 時間のウィンドウ外になるため、この報告は常に自由形式のテキストではなく、承認済みの WHATSAPP_SCHEDULE_TEMPLATE_NAME テンプレートを使用して送信されます。
sendWhatsAppMessage() は、まずテキストを送信するようになりました(無料で、ウィンドウが開いている間は適切です)。131047 の場合に限り、テンプレートを使って再試行します。トークンの期限切れや受信者によるブロックなど、それ以外のエラーでは再試行しません。再送信しても品質評価が不必要に下がるだけだからです。
動作させるには、WhatsApp Manager → Templates で、カテゴリを Utility に設定し、本文変数を 1 つだけ使用したテンプレートを作成してください。
AbaPay scheduled payment update:
{{1}}
Open AbaPay to see the full receipt in your History.
次に、WHATSAPP_SCHEDULE_TEMPLATE_NAME にテンプレート名を設定します。ウィンドウが開いている間に送信する Utility テンプレートは無料なので、通常のケースではフォールバックに費用はかかりません。
⚠️ **テンプレート本文のパラメーターに改行、タブ、または 4 つ以上連続するスペースを含めることはできません。**含まれていると、Meta は送信全体を拒否します。スケジューラーのメッセージはすべて複数行なので、toTemplateParameter() でフラット化し(段落の区切りは — に置き換えます)、Meta の 1024 文字の上限で切り詰めます。tests/whatsapp.test.ts でテストしています。
⚠️ WHATSAPP_APP_SECRET は必須であり、任意ではありません。 Webhook はフェイルクローズします。これが未設定の場合、POST /api/whatsapp/webhook は 503 Webhook not configured を返し、Meta からのすべての配信が拒否されます。その結果、他に症状が出ないままボットが完全に応答しなくなります。これは意図的な動作です。シークレットがなければ、受信 Webhook の X-Hub-Signature-256 を検証できず、誰でも任意の送信者になりすませてしまいます。そのため、設定を忘れると、ボットが壊れたように見えます。
稼働中のデプロイを確認するには、Webhook に署名なしの本文を POST して、ステータスを確認します。503 ならシークレットが未設定、401 Invalid signature ならシークレットが設定済みで、検証が機能しています。値は Meta App Dashboard → App Settings → Basic → App Secret で確認できます。同じフェイルクローズのルールは TELEGRAM_WEBHOOK_SECRET と X_CONSUMER_SECRET にも適用されます。
X(Twitter)
X_BEARER_TOKEN=your_bearer_token
X_CONSUMER_SECRET=your_consumer_secret # ⚠️ REQUIRED — the webhook returns 503 without it (same fail-closed rule as WhatsApp).
X_BOT_ACCOUNT_ID=your_bot_account_id
オンチェーン Webhook(Alchemy)
ALCHEMY_WEBHOOK_SECRET=your_alchemy_base_webhook_secret
ALCHEMY_CELO_WEBHOOK_SECRET=your_alchemy_celo_webhook_secret
コントラクトの検証
ETHERSCAN_API_KEY=your_etherscan_or_celoscan_api_key
エージェントリレイヤー(AbaPayV3 — 自律型の請求書支払い)
RELAYER_PRIVATE_KEY=0x... # ⚠️ HOT KEY. Only needed if you deploy AbaPayV3 and enable agent payments.
NEXT_PUBLIC_APP_URL=https://abapays.com # Used to build agent payment deep links.
⚠️ 有効にする前に、影響範囲を把握してください。 リレイヤーキーで支出できるのは、各ユーザーのオンチェーン残高のある許容量までで、payBillFor 経由に限られます。ユーザーのウォレットから資金を引き出したり、誰かの許容量を増やしたり、Vault から資金を引き出したりすることはできません。これらの制限はバックエンドではなく、コントラクトによって適用されます。キーが漏洩した場合、オーナーが setRelayer(address(0)) を呼び出せば、即座に無効になります。ガス代のみを補充してください。トークン残高を保持させてはいけません。
エージェント ID(ERC-8004)— 登録は一度のみ
ERC8004_AGENT_URI=https://abapays.com/.well-known/agent.json # Used only by scripts/register8004.ts
ERC8004_REGISTRY_CELO_MAINNET=0x8004A169FB4a3325136EB29fA0ceB6D2e539a432 # Optional override
ERC8004_REGISTRY_CELO_SEPOLIA=0x8004A818BFB912233c491871b3d84c89A494BD9e # Optional override
ERC8004_REGISTRY_BASE_MAINNET=0x8004A169FB4a3325136EB29fA0ceB6D2e539a432 # Optional override — same address as Celo mainnet, confirmed byte-identical via eth_getCode
NEXT_PUBLIC_ERC8004_AGENT_ID= # Optional. Set after registering, for UI display.
Hardhat にすでに設定されている同じ CELO_PRIVATE_KEY を使用します。これは ID の登録のみを行い、支払いには一切関与しません。
登録方法: ID はチェーンごとです。チェーンをまたいだエージェントレコードはないため、チェーンごとに一度実行します。どちらの登録も同じ agent.json URL を参照します。
public/.well-known/agent.jsonをデプロイします(先にwallet.addressを実際のRELAYER_ADDRESSに編集してください)。次の URL からアクセスできるようにします:https://<your-domain>/.well-known/agent.json。- 上記の
ERC8004_AGENT_URIにその URL を設定します。 - まず
npx hardhat run scripts/register8004.ts --network sepoliaを実行します。Celo Sepolia Celoscan でトランザクションを確認し、Registeredイベントの URI とエージェント ID が正しいことを確認します。 - その実行が成功してから、
npx hardhat run scripts/register8004.ts --network celoを実行します。実際のガス代がかかり、Celo の ID が永続的にミントされます(AbaPay の Celo 本番環境のエージェント ID:9687)。 - 別途、
npx hardhat run scripts/register8004.ts --network baseを実行します。Base の ID がミントされます(AbaPay の Base 本番環境のエージェント ID:59561)。同じ URI ですが、レジストリとチェーンが異なるため、エージェント ID も異なります。 NEXT_PUBLIC_ERC8004_AGENT_IDに、スクリプトが出力するエージェント ID を設定します。どちらの ID も 8004scan.io で検索できます。
どちらの登録でも保存されるのはURLのみで、カードの内容は保存されません。そのため、agent.json(たとえば、新しく宣言したサービスを追加する場合)を編集すれば、新たなトランザクションなしで URL の返す内容が変わります。ただし、それだけでは 8004scan などのスキャナーに変更を検知させられません。インデクサーは定期的に URL をポーリングするのではなく、登録時にカードの内容をスナップショットとして保存しているようです。そのため、変更を知らせるオンチェーンのシグナルはありません。scripts/update8004uri.ts はこの問題を解決します。レジストリの setAgentURI(agentId, sameURI) を呼び出し、URI 自体は変更せずに、新しい URIUpdated イベントを再発行して、インデクサーが反応できる新たな情報を提供します。
ERC8004_AGENT_ID=9687 ERC8004_AGENT_URI=https://abapays.com/.well-known/agent.json npx hardhat run scripts/update8004uri.ts --network celo
ERC8004_AGENT_ID=59561 ERC8004_AGENT_URI=https://abapays.com/.well-known/agent.json npx hardhat run scripts/update8004uri.ts --network base
agent.json の内容(上記の mcp サービスエントリーなど)を変更し、登録済みの ID を再読み込みさせたい場合は、いつでもこれを実行してください。
x402 決済(メインアプリ、Celo + USDC/USD₮/USA₮)
CELO_X402_API_KEY=your_x402_celo_org_api_key # Server-side: settles via api.x402.celo.org
クライアント側の SDK キーは必要ありません。支払人の EIP-3009 認証は、2 つ目のウォレット SDK ではなく、ユーザーがすでに接続しているウォレット(src/lib/x402Pay.ts)で署名されます。
NEXT_PUBLIC_X402_ENABLED= # Default ON. Set to "false" to use the contract call instead
x402 は両方のチェーンでデフォルトの決済手段です。Celo では USDC、USD₮、または USA₮(それぞれ独自の EIP-712 ドメインで決済)を、Base では USDC を使用します。これにより、支払いはコントラクト呼び出しとしてラベルを付け替えるのではなく、x402scan に実際にインデックスされます。EIP-3009 を使わないものは、通常のコントラクト呼び出しを使用します。x402 では支払いごとに新しい署名が必要なため、エージェント起点のフローには一切関与しません。上記の RELAYER_PRIVATE_KEY とは別のインフラです。
チャレンジで提示されるチェーン — エージェントのデフォルトは Celo です。 チェーン(blockchain と token)を指定するリクエストは、指定されたチェーンでのみチャレンジされます。クローラーのプローブ、汎用 x402 クライアント、Celo の buy マーケットプレイス CLI など、チェーンを指定しないリクエストには、まず Celo の 3 種類のステーブルコイン(USDC、USD₮、USA₮。それぞれ独自の EIP-712 ドメインを使用)が提示され、Base が設定されている場合は続けて Base の USDC が提示されます。価格はすべて同じです。支払い後のリトライは、支払いヘッダーの署名に使われたチェーンとトークンで決済されます。AGENT_DEFAULT_CHAIN(src/constants)は、エージェント向けのすべての経路(x402、MCP、A2A、エージェントリンク、リレイヤー)で Celo です。DEFAULT_CHAIN(Base)は、コンシューマーアプリ専用です。意図的に選択したトレードオフがあります。Coinbase のバリデーター(POST /platform/v2/x402/validate)は accepts[0] を評価するため、Celo を先頭にすると、チェーンを指定しないプローブは通過しなくなります。そのため、このエンドポイントが Coinbase Bazaar に掲載されることは想定していません。Celo のインデックス(Celo の buy カタログ、agent402.tools、x402scan)はいずれも Celo のエントリーを参照します。
⚠️ シグネチャについてウォレットに警告される場合があります。その理由を理解しておいてください。 x402 では EIP-3009 の transferWithAuthorization を使って決済します。これは、第三者によるトークン移動を許可するシグネチャです。
この仕組みは、トークンドレイナーが要求するものと構造的に同じため、一部のウォレットセキュリティスキャナーで警告されます。たとえば、通常の請求書の支払いで、AbaPay 自身のリクエストが Zerion に「悪意のあるリクエスト — 承認すると、すべての資産を失う危険性があります。」と表示されたことがあります。一方、同じ支払いでもコントラクト呼び出し経由なら、通常の送金として表示され、「リスクは見つかりませんでした」となります。これは x402 の仕組みに固有のものです。x402scan で支払いを証明可能にするまさにその特性が、スキャナーから問題視されるのであり、リクエストに不備があるわけではありません。意図的なトレードオフです。
チェーンごとの回避策:NEXT_PUBLIC_X402_ENABLED=false にすると、すべてコントラクト呼び出しの経路に切り替わります。NEXT_PUBLIC_BASE_X402_ENABLED=false にすると、Base のみ切り替わります。どちらもデフォルトでは有効です。
x402 はすべてのウォレット、すべての環境、両方のチェーンで動作します。Valora の WalletConnect 接続が停止する問題(上記のウォレット接続セクションを参照)は、シグネチャを伴わない通常の eth_sendTransaction でも同じ段階で発生します。そのため、x402 をブラウザー内のウォレットに限定しても、この問題は解決しません。決済経路は原因ではありません。
🔵 実際の制限は、チェーンでもウォレットでもなく、トークンにあります。 x402 は EIP-3009 の transferWithAuthorization シグネチャを使って決済するため、この機能を実装しているトークンでのみ動作します。Celo の USDC、USD₮、USA₮ はすべて対応しています。Base では USDC は対応していますが、Tether の USD₮ は対応していません。このコントラクトには、署名対象となるそのような関数がありません。
このため、チェーンの主要なステーブルコインが非常に重要です。Base の主要トークンは USDC(TOKEN_ORDER_BY_CHAIN)なので、Base のデフォルト経路は x402 です。トークンのリセット処理はチェーン ID に基づいています。チェーンを切り替えると、ユーザーが意図的に別のトークンを選択していない限り、選択中のトークンは常にそのチェーンの主要なステーブルコインにリセットされます。以前選択されていたトークンが新しいチェーンに存在しない場合だけリセットするわけではありません。USD₮ は両方のチェーンに存在しますが、Celo でのみ x402 による決済が可能です。そのため、トークンの存在確認だけでリセットすると、USD₮ が Celo から Base にそのまま引き継がれ、ユーザーは気づかないうちにコントラクト呼び出しの経路に切り替わってしまいます。
決済には thirdweb ではなく、Celo 独自の x402 ファシリテーター(メインネットでは api.x402.celo.org、テストネットでは api.x402.sepolia.celo.org。Celo Core Co. が構築)を使用します。thirdweb は引き続きクライアント側のみで、useFetchWithPayment のウォレット署名処理に使用されます(プロトコルに依存せず、レスポンス本文から支払いチャレンジを読み取るため、thirdweb 独自のファシリテーターに限らず、準拠したファシリテーターで動作します)。thirdweb 独自のファシリテーターではなくこちらを選んだ理由は、前払いクレジットによる決済あたり $0.001 の定額料金で、thirdweb の約 0.3%の手数料より安いこと、メインネットでの決済に請求プランが不要なこと(thirdweb ではプランがないと、メインネットの決済がすべて DELEGATION_CHECK_FAILED で失敗します)、そして真に非カストディアルであることです。署名済みの支払い認可によって、仲介者のウォレットを経由せず、直接ボールトに支払われます。
API キーの取得方法:
- x402.celo.org にアクセスし、ウォレットを接続します(どのウォレットでも利用できます。これはトランザクションではなく、無料でガス代不要のメッセージに署名するためだけのものです)。
- API キーがすぐに発行され、無料クレジットも付与されます(執筆時点ではメインネットで500、テストネットで1000)。完全なキーが表示されるのは一度だけなので、すぐにコピーしてください。
CELO_X402_API_KEYにそのキーを設定します。同じキーをメインネットとテストネットの両方のエンドポイントで使えますが、クレジットプールは別々に管理されています。- クレジットがなくなる前に、同じダッシュボードからチャージします(USDC を入金、1ドル ≈ 1,000クレジット)。クレジットが0になると
/settleは402を返し、その際にアプリから Telegram アラートが送信されます(src/app/api/pay/x402/route.tsを参照)。 - ほかに登録は不要です。クライアント側で SDK アカウントを作成する必要はありません。支払いの承認には、ユーザーがすでに接続しているウォレットを使い、アプリ独自の viem ウォレットクライアント(
src/lib/x402Pay.ts)経由で署名します。 - 変数を
.env.localとホスティングプロバイダーの本番環境変数に追加してから、再デプロイします。NEXT_PUBLIC_*変数はビルド時に埋め込まれるため、再ビルドしないと既存のデプロイには変更が反映されません。
プライベート RPC(任意、推奨)
CELO_RPC_URL=https://celo-mainnet.core.chainstack.com/<key> # Server-only. Tried FIRST for Celo mainnet.
BASE_RPC_URL=https://base-mainnet.core.chainstack.com/<key> # Server-only. Tried FIRST for Base mainnet.
有料エンドポイント(例:Chainstack)は、src/lib/chain.ts の rpcUrlsFor でパブリックエンドポイントの前に設定します。パブリックエンドポイントは自動フォールバックとしてその後ろに残るため、どちらかが停止しても支払いは止まりません。これらは意図的に NEXT_PUBLIC_ を付けていません。URL にプロバイダーのアクセスキーが含まれており、Next.js は非公開変数をブラウザーに送信しないため、ブラウザーは引き続きパブリックリストを使用し、キーがサーバーの外に出ることもありません。BASE_RPC_URL は Base のデプロイ時に hardhat.config.ts でも使用されます。夜間のフォークテストでは、アーカイブアクセスが必要な GitHub シークレット CELO_FORK_RPC_URL / BASE_FORK_RPC_URL をそれぞれ使用します。
Cron / メンテナンス
CRON_SECRET=any_long_random_string # Optional. Protects /api/cleanup and both /api/schedules/run* endpoints.
放棄された古い事前処理インテントは、Webhook 内から自動的かつ適宜に(実行頻度を制限し、ブロックせずに)クリーンアップされます。Vercel cron は不要で、無料の Hobby プランでも動作します。/api/cleanup は手動実行や、外部の無料スケジューラー(cron-job.org、GitHub Actions)からの実行にも引き続き利用できます。静かな時間帯にも確実な間隔で実行したい場合に使用してください。
定期請求 / 自動支払いエージェント — 実際に実行するには、どちらも外部 cron が必要です: 上記の Webhook 駆動のクリーンアップとは異なり、これらは自動では呼び出されません。
/api/schedules/run— 定期請求(月次/週次/日次)。cron-job.org(無料)で1日に1回または2回、POST https://<your-domain>/api/schedules/runを呼び出すジョブを登録します。ヘッダーにはAuthorization: Bearer <CRON_SECRET>(またはx-cron-secret: <CRON_SECRET>)を指定します。/api/schedules/run-instant— DeAI チャットからの将来の単発支払い(「今後10分以内に MTN の通話時間を買って」など)。指定時刻に近いタイミングで実行するには、より短い間隔が必要です。cron-job.org で別の無料ジョブを登録し、1~5分ごとにPOST https://<your-domain>/api/schedules/run-instantを呼び出します。この頻度でも負荷は軽微です。クエリ対象はfrequency = 'once'の行だけなので、ほとんどの実行では期限到来分がなく、すぐに戻ります。
これらを登録しなくても、ユーザーはチャットからスケジュール(定期または単発)を作成できます。しかし、実行されることはなく、取得する cron がないため is_active のまま残ります。
オンチェーン統計 — AbaPay 独自のチェーンインデックス:
stats.abapays.com は独立したサイトです(docs.abapays.com と同様です)。
概要(両方のチェーン)、Celo、
Base があります。middleware.ts はこのホストを src/app/stats/ に書き換えます。
以前の agents.abapays.com/stats のリンクはここにリダイレクトされます。AbaPay のボールトへのすべての支払いをダッシュボードに表示します。固定表示の 7D/30D/90D/All 切り替えですべての期間指定可能なパネルを操作します。
カウンター(支払い数、ユニークウォレット数、取引量、エージェントネイティブの割合、新規およびリピーターのウォレット、平均支払額、ハッカソンタグ付きの支払い)、インタラクティブな日次チャート(レール別の支払い数と取引量、新規とリピーターのウォレット、累計取引量)、円グラフ(チェーン別、レール別、トークン別の取引量)、アトリビューションタグ数が表示されます。「アクティブなウォレット」セクションには、今日時点の DAU、WAU、MAU、DAU/MAU の定着率、DAU/WAU/MAU の折れ線グラフが表示されます。続いて、各取引へのリンク付きで最新の支払いが表示されます。すべてのパネルには説明、「計算方法」の注記、パネルを PNG として保存するカメラボタン、共有可能なリンク(#panel-id)、チェーン名と日付を示すフッターがあり、パネル単体の画像としても内容がわかります。時刻はナイジェリア時間(WAT)、日はラゴスの日付です(マイグレーション 040)。開いたページのデータは 5 分ごとに更新されます。チャートには Recharts を使用しています。/api/onchain/export?chain=CELO(または BASE、ALL)から全件の CSV をダウンロードできます。エージェントのホームページにあるヒーロー数値も同じインデックスを参照しています。
- インデクサー:
src/lib/onchain/indexer.tsは、各チェーンの RPC(最初にCELO_RPC_URL/BASE_RPC_URL、次に公開エンドポイント)を通じて、ボールトのイベントとボールトへのステーブルコイン送金を読み取り、各支払いをレール別に分類します。5 つすべてのボールトに対応しています。Celo V1、V3、V4 と Base V1、V4 です。 - ストレージ:
onchain_paymentsとonchain_cursors(マイグレーション 038)。onchain_summary()は全期間の数値と日次系列を 1 回のクエリで返します。onchain_windows()(マイグレーション 041)は、直近 7/30/90 日と全期間の集計に加えて、DAU/WAU/MAU の系列を返します。041 がなくてもサイトは読み込まれますが、表示されるのは全期間のカウンターのみです。顧客データは保存されません。PaymentReceived内の請求アカウント番号がテーブルに読み込まれることはありません。 - スケジュール:
.github/workflows/onchain-index.ymlは、15 分ごとにPOST /api/cron/onchain-indexを呼び出します(APP_URLとCRON_SECRETのシークレットを使用)。呼び出しごとに最後にインデックスしたブロックから処理を再開します。038 を適用した後、履歴を読み込むため、ワークフローを backfill にチェックを入れて手動で一度実行してください。 - 信頼性の高い 15 分間隔のトリガー: GitHub は実行負荷が高いとスケジュール済みワークフローを遅延させることがあり、数時間遅れる場合もあります。そのため、上記のスケジュール実行と同様に、無料の cron-job.org ジョブをメインのトリガーとして登録してください。15 分ごとに
POST https://www.abapays.com/api/cron/onchain-indexを実行し、ヘッダーにx-cron-secret: <CRON_SECRET>を設定し、リクエストのタイムアウトはプランで許される限り長く設定します。進捗はブロック範囲ごとに保存されるため、途中で終了した呼び出しでもインデックスは進みます。GitHub のスケジュールはバックアップとして残します。行は upsert されるため、呼び出しが重複しても問題ありません。
🚀 インストールとセットアップ
-
リポジトリをクローンする
git clone https://github.com/investorphem/abapay.git cd abapay -
依存関係をインストールする
npm install -
環境変数を設定する — 上記の変数を
.env.localにコピーします。 -
開発サーバーを起動する
npm run dev -
アプリケーションにアクセスする
- ユーザーストアフロント:http://localhost:3000
- ドキュメントと FAQ:http://localhost:3000/docs
- 管理オペレーションセンター:http://localhost:3000/admin
スマートコントラクト開発(Hardhat)
npx hardhat compile # Compile contracts
npm run test:contracts # Run the Solidity test suite
npx hardhat run scripts/deployV4.ts --network <network> # Deploy V4 (agent-initiated payments, adjustable withdrawal delay)
npx hardhat run scripts/register8004.ts --network <network> # Register the agent identity (ERC-8004)
npx hardhat run scripts/update8004uri.ts --network <network> # Re-push the agent URI so an indexer (8004scan) re-reads it
AbaPayV2.sol — セキュリティを強化したコントラクト
contracts/AbaPayV2.sol は、以下の強化上の不足に対処した、元の AbaPay.sol のセキュリティ強化版です。payBill のシグネチャと PaymentReceived イベントは V1 とバイト単位で完全に同一のため、フロントエンドと共有の PaymentReceived 検証機能(src/lib/paymentProof.ts)は、バックエンドを変更せずに動作します。
| 強化項目 | 理由 |
|---|---|
SafeERC20 | 規格に準拠していないトークン(たとえば、一部の USDT デプロイメント)は bool 値を返しません。require(transfer(...)) を直接使うと、そうしたトークンで失敗します。 |
ReentrancyGuard | setTokenSupport では任意のトークンをホワイトリストに登録できます。そのため、フックを持つトークンを使うと、対策しなければ payBill が再入可能になります。 |
Pausable | V1 には緊急停止機能がなかったため、デプロイ後に脆弱性が発覚しても停止できませんでした。一時停止中も返金は有効なため、ユーザーへの返金が可能です。 |
Ownable2Step | 入力ミスのあるアドレスに所有権を移転して、コントラクトを永久に操作不能にする事態を防ぎます。 |
| タイムロック付きの出金 | V1 の最大のリスクです。 侵害された所有者キーが 1 つあるだけで、プールされた資金全額が即座に引き出される可能性がありました。出金はキューに登録し、遅延時間の経過後に実行する必要があります。WithdrawalQueued を監視し、自分が登録した出金でなければキャンセルしてください。V2/V3 では 24 時間に固定されます。V4 では所有者が変更できます(以下を参照)。 |
| 上限付きの返金 | V1 の refundUser は、出金制御を回避できる、制限なしの「任意の金額を任意の宛先に送金する」経路でした。現在はトークンごとに上限が設定され、上限を設定するまでは安全側に失敗します。 |
| 残高差分による計上 | 実際に受け取った 金額をイベントに出力するため、送金時に手数料が差し引かれるトークンによって、バックエンドが過剰に販売することを防げます。 |
メインネット公開前に:
- 専門家による監査を受けてください。 このコントラクトは顧客資金をプールして保有するため、静的なレビューだけでは不十分です。
ABAPAY_OWNERには EOA ではなく、マルチシグ(Safe)を設定してください。 タイムロックによって検知する時間を確保できます。ただし、盗まれたキーだけで出金のキャンセルと再登録ができない場合に限り、攻撃者を阻止できます。また、遅延時間が 0 に設定されている場合は、何の効果もありません(以下の V4 を参照)。- テストネットにデプロイし、まず決済フロー全体をエンドツーエンドで実行してください。
- トークンごとに
setMaxRefundを呼び出してください。上限が設定されるまで、返金はリバートします。
payBillは引き続きtransferFrom(msg.sender, …)を使用するため、支払者本人が署名者である必要があります。委任された支出(DeAI の「ソーシャルメディアから支払う」機能)には、オンチェーンの追加の承認メカニズムが必要です。このセキュリティ強化作業の対象外とし、個別の変更として設計・監査する方針です。
AbaPayV3.sol — エージェント起点の支払い
contracts/AbaPayV3.sol は、V2 を基盤として上記の問題を解決します。Telegram/WhatsApp には署名に使えるウォレットがないため、これまではエージェントがユーザーにアプリで署名するためのディープリンクを渡すことしかできませんでした。V3 では、代わりに セッションキー/支出委任のパターンを導入します。
- ユーザーは自身のウォレットから、一度だけ次の 2 つの操作を行います。標準の ERC-20
approve(AbaPayV3, X)と、ユーザー自身が管理するオンチェーンの上限であるsetSpendingAllowance(token, X)です。 - その後、認可されたリレイヤー(バックエンドのホットキー、
RELAYER_PRIVATE_KEY)は、ユーザーに代わってpayBillFor()を呼び出せます。ただし、呼び出しのたびにコントラクト自体が残りの許容量を確認して減算するため、上限はバックエンドではなくオンチェーンで適用されます。
| 上限 | 適用するもの |
|---|---|
| ユーザーあたりの総リスク額 | spendingAllowance[user][token] — ユーザーのみ設定可能で、いつでも 0 にして取り消せます |
| 1 回の取引あたりの上限 | maxAgentPaymentPerTx[token] — オーナーが設定し、ユーザー自身の許容量に加えてもう 1 つの上限となります |
| リレイヤーキーが盗まれた場合の被害範囲 | ユーザーの残りの許容量までしか支出できず、payBillFor 経由に限られます。ウォレットから資金を引き出したり、誰かの許容量を増やしたり、ボルトから出金したりすることはできません |
| 緊急停止スイッチ | オーナーが setRelayer(address(0)) を呼び出すとエージェントを即時に無効化でき、pause() を呼び出すとすべての支払いを停止できます |
デモではテストネットにデプロイしてください。メインネットでは、まず maxAgentPaymentPerTx と maxRefundPerTx を小さく設定します(scripts/deployV4.ts では、トークンごとに 10 ドル相当がデフォルトです)。その後、setMaxAgentPayment/setMaxRefund で引き上げます。稼働中の V4 ボルトでは、サポート対象の各ステーブルコインについて、エージェントによる支払いの上限は 50、返金の上限は 400 に設定されています。期待値は config/vaults.json に記載されており、node scripts/vault-config.mjs でチェーン上の値との差分を確認できます。
payBillFor は V1/V2 と同じ PaymentReceived イベントを発行するため、Webhook に変更は不要です。また、バックエンドやその他の監視者が「ユーザーが署名した」のか「エージェントが許容量を使って支出した」のかを区別できるよう、追加で AgentPayment イベントも発行します。
AbaPayV4.sol — 調整可能な出金遅延
V4 は V3 に 1 つの変更を加えたものです。出金のタイムロックは 24 時間にハードコードされておらず、オーナーが setWithdrawalDelay(n) で引き上げたり、引き下げたり、0 に設定したりできる withdrawalDelay になっています。新規デプロイ時のデフォルトは 24 時間です。稼働中の両方のボルトでは 0 に設定されています。これはオーナーが意図的に選択したものであり、そのため出金は即時に行われます(以下の警告を参照)。V4 は両方のメインネットにデプロイされています。
| チェーン | AbaPayV4 |
|---|---|
| Base | 0xC0A4dAA04DEd9c54D1239507B5A5E645761ef488 |
| Celo | 0x5df8aE2B963165b735B18Ca86B1ea448d2AA032C |
⚠️ 以前の Celo コントラクト 0x42Fa4637… は V3 です。setWithdrawalDelay がないため、24 時間のタイムロックは固定されており、即時出金に変更することはできません。そのため、Celo は再設定ではなく再デプロイされました。現在、このコントラクトは廃止済みです。すべてのトークンがサポート対象外で、リレイヤーは 0x0 に設定され、コントラクトは一時停止されています。以前の Celo V1(0x1d125198…)も、どのトークンも受け付けなくなっています。
キュー自体を削除することはできません。バイトコードにコンパイルされており、直接呼び出せる withdraw() はありません。遅延が 0 の場合、出金は queueWithdrawal の後に executeWithdrawal を続けて行います。2 つのトランザクションが必要で、待ち時間はありません。
⚠️ 遅延を変更しても、すでにキューに入っている出金には適用されません。 出金の executableAt はキューに追加された時点で設定されるため、遅延を短くしても、すでにキューに入っている出金がすぐに実行可能になるわけではありません。cancelWithdrawal でキャンセルし、新しい遅延のもとでキューに入れ直す必要があります。キャンセルによって資金が移動することはなく、トークンがボルトから出ることはありません。
⚠️ 遅延を 0 にすると、タイムロックが提供する保護はなくなります。 この保護があるため、オーナーキーが盗まれても、誰かが気づく前にボルトから資金を引き出すことはできません。遅延が 0 の場合、キーを持つ人は同じ 1 分以内にキューに追加して実行できます。緊急時の設定として扱い、その後は遅延を元に戻してください。
# --chain defaults to base; pass --chain celo for the Celo vault
node scripts/instant-withdrawals.mjs --chain celo # show live state, change nothing
node scripts/instant-withdrawals.mjs --chain celo --apply # delay -> 0, clear a stuck queue entry
node scripts/instant-withdrawals.mjs --apply --withdraw # …and push the queued one through
node scripts/instant-withdrawals.mjs --chain celo --restore-delay 86400 # put the 24h timelock back
ERC-8004 エージェント ID
scripts/register8004.ts は、AbaPay の DeAI エージェントを Celo 上の実際のオンチェーン ID として、ERC-8004「Trustless Agents」 レジストリ(メインネット上の 0x8004A169FB4a3325136EB29fA0ceB6D2e539a432)に登録します。これにより、ほかのオンチェーン ID と同様に、8004scan.io / AgentScan で見つけて閲覧できるようになります。登録時に ERC-721 が発行され、その tokenId がエージェント ID となります。また、エージェントのオンチェーンアドレスとして運用リレイヤーウォレットを記載した、公開エージェントカード(public/.well-known/agent.json)を参照します。
これは ID のみを対象とし、決済には影響しません。 リレイヤーの署名不要な payBillFor フローには、まったく影響しません。登録しても、登録しなくても、請求書の支払い方法は変わりません。メインネットで実行する前に、Celoscan 上の検証済みソースを確認して register(string) セレクターを検証してください。スクリプトの先頭にあるコメントを参照してください。
MCP サーバー(AI エージェント決済)
src/app/api/mcp/route.ts は、Streamable HTTP(JSON-RPC 2.0:initialize、tools/list、tools/call)経由で動作する、実際の MCP(Model Context Protocol)サーバーを実装しています。Claude やほかの AI エージェントがツールを呼び出す際に使うものと同じオープン標準で、追加の依存関係は不要です。Telegram/WhatsApp/X をすでに支えているのと同じ実行エンジンへの4 つ目のチャネルであり、独自のルールを持つ並列システムではありません。
abapays.com を人間が一度も開くことなく、AbaPay 自体と連携するエージェントを構築しますか? docs/AGENT_INTEGRATION.md をご覧ください。x402 によるセットアップ不要の方法と、ウォレット署名のみでセルフサービスの MCP API キーを取得する、完全なヘッドレス方式を説明しています。TypeScript を使っていますか?
sdk/(npm のabapay-sdk)を使えば、どちらの方式も 2 つの関数で利用できます。Python を使っていますか?python-sdk/は同じ内容を一つひとつ対応させて実装しており、PyPI からも利用できます。pip install abapay-sdkでインストールできます。Trusted Publisher 経由でpy-sdk-v*タグから公開されるため、トークンシークレットは不要です。実際の検証を行ったのは CI です(作成環境には Python インタープリターがなかったため、ローカルでは実行していません)。単一のドキュメントではなく、まとまったガイドをお探しですか?docs/ gitbook/では、同じ内容を 12 章に分けて説明しています(SUMMARY.mdは目次です。Markdown 形式なので、GitBook やリポジトリを直接読み込むドキュメントホスティングサービスにそのまま同期できます)。すぐに使えるエージェントをお探しですか? abapay-reference-agent(MIT)は、独自のウォレットから x402 を使って Celo 上の請求書を支払います。支出上限、承認プロンプト、Claude による任意の自然言語リクエスト、任意の Self Agent ID に対応しています。クイックスタートは 10 分以内です。 | ツール | 内容 | 必要なもの | |---|---|---| |describe_capabilities| AbaPay で支払えるものと、現在一時停止中のものを人が読める形で一覧表示します | 不要 — 公開 | |list_plans| DATA/CABLE/EDUCATION で現在実際に購入できるプランを、正確なvariation_codeと VTpass の最新価格とともに一覧表示します | 不要 — 公開 | |list_international_options| 最新の国際カタログ(140 か国以上)を、国 → 商品タイプ → 事業者 → 価格付きプランの順に、一度に 1 階層ずつ表示します | 不要 — 公開 | |check_balance| 接続済みウォレットの最新残高と承認済みエージェント上限を、チェーン上のトークンごとに読み取ります | OAuth Bearer token またはapi_key| |transaction_history| 接続済みウォレットの最近の実際の取引を一覧表示します。アプリの「履歴」タブと同じデータです | OAuth Bearer token またはapi_key| |get_payment_status| tx hash または request id で 1 件の支払いを照会します。配達済み、確認中、失敗、返金済みのいずれかと、返金状態を確認できます。接続済みウォレットに限定されます | OAuth Bearer token またはapi_key| |pay_bill| 実際の請求(エアタイム、データ、電気、ケーブルテレビ、教育用 PIN、または国際エアタイム/データ)をオンチェーンで最初から最後まで支払います | (OAuth Bearer token またはapi_key)+pin、必須 | |pay_bill_batch| 1 回の呼び出しで 2~20 人の受取人にエアタイム/データを支払います。バッチ全体で 1 つの PIN を使用します | (OAuth Bearer token またはapi_key)+pin、必須 | |schedule_bill| エアタイム、データ、電気、またはケーブルテレビの定期/単発支払いを設定します。Telegram/WhatsApp/X でサポートされる自動化と同じです | (OAuth Bearer token またはapi_key)+pin、必須 | |list_schedules| 接続済みウォレットの有効なスケジュールを一覧表示します | OAuth Bearer token またはapi_key| |cancel_schedule|idで 1 件のスケジュールをキャンセルするか、provider/all: trueで複数をキャンセルします(後者には PIN が必要です)。いずれも指定しない呼び出しは拒否されます | OAuth Bearer token またはapi_key(+一括処理には PIN) |
list_plans があるため、エージェントが variation_code を推測する必要はありません。
その説明とサーバーレベルの instructions の両方で、これら 3 つのサービスについて pay_bill の前に呼び出し、返されたコードをそのまま渡すようクライアントに指示しています。プラン、コード、価格を推測してはいけません。利用可能な結果が返されない場合(実際に起こり得ます。この加盟店アカウントでは JAMB が有効になっていません)、正しい対応はその旨を伝えることであり、コードを捏造することではありません。
pay_bill は、エアタイム/データ/電気/ケーブルに加えて教育にも対応しています。これは、MCP をチャットと同じ信頼境界とし、それより狭い範囲にしないためです。必要なルールはすべて共通で、すでに実装されています。requiresVariation() は variation_code を必須にし、checkAccountNumber() は JAMB の 10 文字以上のプロフィール ID を検証し、requiresVerifiedName() は加盟店が本人確認を行うのが JAMB のみであると判定します(WAEC には確認対象のアカウントがありません)。また、事前にプロバイダーを最新の VTpass カタログと照合して検証するため、エージェントが VTpass で販売できないプロバイダーを指定し、資金が移動した後になって初めてそれに気付くことはありません。
service: "INTERNATIONAL" は、チャットとは異なり購入を完了します。 チャットの INTERNATIONAL 処理(src/app/api/deai/core/route.ts)はリクエストを検証した後、アプリで手続きを完了するようユーザーに案内するだけです。実際に販売したことは一度もありません。MCP の pay_bill は購入を完了します。list_international_options は VTpass の国 → 商品タイプ → 事業者 → バリエーションの階層を順にたどり、pay_bill は選択されたバリエーションを自身で再取得して、その variation_rate/charged_amount から NGN 換算価格を算出します。オンチェーンの請求額を決める値であるため、クライアントが送信した金額を信頼することはありません。現時点でこの方法で支払えるのは固定価格のプランのみです。金額を自由に指定するプランは引き続きアプリに誘導されます。
pay_bill が成功するたびに、テキスト行だけでなく、プレミアムな領収書を返すようになりました。 確認テキストに加えて、ブランド入りの領収書カード(next/og の ImageResponse を使ってサーバー側でレンダリングされ、追加の依存関係はありません。src/lib/deai/receiptCard.tsx を参照)と、共有可能な公開領収書ページ /receipt/[request_id] へのリンク(src/app/receipt/)がレスポンスに含まれます。このリンクのキーには tx_hash ではなく request_id を使います。トランザクションハッシュは、オンチェーンでボールトアドレスを監視している人なら誰でも確認できるためです。公開・共有可能な領収書ページから、ブロックチェーンの監視者が支払いと顧客の確認済みの名前や住所を関連付けられてはなりません。そのため、購入したコードや PIN も表示しません。transaction_history にも同様のリッチな表示が加わり、過去の取引を確認するためのプレーンテキストの一覧に、明細カードの画像が添えられます。アプリを開く必要はありません。
pay_bill、pay_bill_batch、transaction_history、check_balance、list_schedules、describe_capabilities も、MCP Apps(SEP-1865)を通じて、本物のインタラクティブカードとして表示されます。 これは、特定のファーストパーティーだけの仕組みではなく、オープンな MCP 拡張機能(io.modelcontextprotocol/ui。2026-01-26 にプロトコル初の公式拡張機能として公開)です。上記のフラットな PNG は、Satori のフォントサブセット化の制限により、固定画像です(₦/₮ には回避策が必要です。receiptCard.tsx 内のコメントを参照)。一方、インタラクティブカード(src/lib/deai/mcpUiTemplates.ts)は、ホストがサンドボックス化された iframe 内でレンダリングする、本物の HTML/CSS/JS です。ui/notifications/tool-result を通じてツールの structuredContent を受け取り、本物の ₦/₮ グリフを表示します。テーマにも対応しており、ライト/ダーク用のホストの CSS 変数を読み取ります。また、「領収書を表示」リンクは ui/open-link 経由で機能します。これら 6 つのツールで _meta.ui.resourceUri を宣言し、新たに追加された 2 つの /api/mcp メソッド resources/list/resources/read(src/app/api/mcp/route.ts)を通じて提供します。これは追加のみの変更です。拡張機能のネゴシエーションを行わないホストは resources/read を呼び出さないだけで、すべてのツールは以前とまったく同じように動作します(テキスト ± PNG 画像)。これは仕様が定めるグレースフルデグラデーションのルールに沿っています。
領収書カードは、全額成功の場合だけでなく、pay_bill の実際に起こり得るすべての結果を表示します。 finalizePayBillResult は、実際の txHash が生成されたすべての結果について、完全な領収書(画像+structuredContent)を作成します。対象は SUCCESS、オンチェーンで確認中の PENDING、請求済みで返金待ちの FAILED_VENDING です。それぞれカード上で固有のステータスカラー、ラベル、アイコンを使います。チェーンに到達しなかった支払いだけは、テキストのみの表示にフォールバックします。
カードは真にプレミアムな表示になり、「AbaPay が提供するすべてのもの」がここに集約されます。 すべてのビュー(receipt、history、balance、schedules、batch、capabilities)で、グラデーションの細い境界線とドロップシャドウ、インライン SVG アイコン(ステータスごとに異なるアイコン――成功/保留/販売失敗/失敗、およびサービスごとのアイコン――通信料金/データ通信/電気/ケーブル/教育/銀行/国際サービス)、さらに再レンダリングのたびに再実行されるフェードインのトランジションを共有します。そのため、履歴のページ送りや「更新」/「キャンセル」を押しても、静的な画面の差し替えのようには感じません。describe_capabilities のカード(src/lib/deai/capabilities.ts の getCapabilitiesForCard() を介した新しい capabilities ビュー)には、すべての AbaPay サービスと、その利用例の表現、ここ MCP から利用できるかアプリ専用か、そしてチャットが参照するものと同じキルスイッチ状態から取得した「一時停止中」/「利用可能」のリアルタイムバッジが表示されます。サードパーティーがレンダリングする iframe 内に PIN 入力欄を置かずに安全に表示できるメニューとしては、最も充実したものです(PIN を含むフォームをカードに表示しない理由については、以下の信頼境界に関する注記を参照してください)。
カードはツールを呼び出し直すことができ、表示するだけではありません。 transaction_history のカードは Prev/Next を使って履歴をページ送りし(offset をずらしてツールを呼び出し直します。transaction_history はこれを受け付けるようになりました)、check_balance のカードには Refresh ボタンがあり、list_schedules のカードには各スケジュールに Cancel ボタンがあります。いずれも仕様の「Interactive Updates」パターンを使用しています(View がホスト経由で tools/call を送信し、通知経由ではなく通常の結果を直接受け取ります)。意図的に pay_bill/pay_bill_batch/schedule_bill には拡張していません。これらには PIN が必要であり、サンドボックス化されたサードパーティーの iframe に支払い用 PIN を入力するのは、チャットに直接入力するよりも信頼境界が弱いためです。このドキュメントの他の部分は、その境界を守ることを前提にしています。単一のスケジュール id を指定する cancel_schedule には PIN が不要なため、カードで公開している書き込みアクションはこれだけです。複数のスケジュールを一度にキャンセルする場合(provider または明示的な all: true)は PIN が必要なため、カードには表示されません。
接続済みのクライアントは、手動で再接続しなくても新しいツールや変更されたツールを(おおむね)取得します。 initialize は tools: { listChanged: true } を宣言し、Accept: text/event-stream を指定した GET /api/mcp は実際の SSE ストリームを開き、クライアントがリッスンを開始した時点で notifications/tools/list_changed をプッシュして、新しい tools/list の取得を促します。これは、すでに接続済みのクライアントが、新しいチャットが始まっただけでは initialize を再実行しないため重要です。コネクターへの永続的な接続をそのまま使い続けます。⚠️ ただし、これだけで保証されるわけではありません。Anthropic 独自の MCP コネクタートラッカーには、手動で再接続してもリモートコネクターのツール一覧が古いままになるという、報告済みで未解決の事例があります(anthropics/claude-ai-mcp#137、#476)。これはこのサーバーでは制御できない、クライアント/プラットフォーム側のキャッシュの問題です。プラットフォーム側で修正されるまでは、Claude.ai のコネクター独自の ⋮ メニューから「ツール一覧を更新」を選ぶのが最も手早い手動での代替手段です。完全に切断して再接続するより手軽で、再認証も不要です。
どちらも省略可能な chain/token の上書きを受け付けます。デフォルトでは API キーの作成時に承認された値が使われますが、呼び出し元は、そのデフォルトで不足が生じた場合でも別の値を指定できます。check_balance はチェーン上のすべてのステーブルコインについて、残高と承認済みの上限を返します(デフォルトのものだけではありません)。そのため、エージェントは別の通貨が実際に使えるかどうかを事前に確認できます。デフォルトの通貨を使って pay_bill を試み、残高またはオンチェーンの使用許可額が不足していた場合、エラー自体が同じチェーン上の別のトークンについて、残高と承認済みの上限がすでに十分かどうかを確認し、名前を挙げて知らせます。たとえば「USD₮ は不足していますが、USDC は残高と承認済みの上限がすでに十分です。token: 'USDC' を指定して再試行してください」のように表示し、失敗したトークン名だけを示す行き詰まるメッセージにはなりません。
pay_bill_batch — 1 回の呼び出しで複数の受取人に支払い、チャットの一括支払いに対応します。
チャットのインテントエンジンは、intentEngine.ts のルール 14 により、「X に 500、Y に 1000 を送る」を複数の受取人として解析しますが、MCP の pay_bill は 1 人しか処理できませんでした。pay_bill_batch は、チャット独自の一括処理ハンドラーが呼び出すものとまったく同じプリミティブ(groupByChainToken、checkAutonomousCapacity、executeAgentPayment — すべて src/lib/deai/batch.ts 内)を再利用します。受取人は (chain, token) ごとにグループ化され、各グループの利用可能額はそのグループの小計と照合されますが、どのグループでも利用可能額が不足していれば、資金が動く前に一括処理全体が拒否されます。利用可能額の確認を通過した後、受取人への支払いは 1 人ずつ行われ(リレイヤーの nonce の競合を避けるため、並列処理はしません)、レスポンスではそれぞれの結果を個別に報告します。途中で売買処理に失敗しても、一括処理全体が失敗したかのように報告されることはありません。チャットとの違いが 1 つあり、単に同等というだけでなく、こちらのほうが高機能です。チャットの ParsedRecipient の形状には受取人ごとの variation_code フィールドがないため、チャット経由の DATA の一括処理では受取人ごとのプランを指定できません。pay_bill_batch の構造化スキーマでは指定できるため、この制約を引き継がず、DATA の受取人ごとに必須となります。AIRTIME と DATA のみが対象で、1 回の呼び出しにつき受取人は最大 20 人です。
schedule_bill/list_schedules/cancel_schedule により、MCP はチャットの自動化機能と同等になります。 Telegram/WhatsApp/X は、定期的な請求と 1 回限りの予約請求に対応しています(scheduled_bills テーブルを src/lib/scheduler.ts の cron が実行します)。MCP も同じ機能を備えているため、エージェントは請求をすぐに支払うことも、後で支払うよう予約することもできます。schedule_bill は、チャットで複数回のやり取りを通じて収集する情報を 1 回の呼び出しで受け取り、pay_bill とまったく同じ検証(requiresVariation、checkAccountNumber、checkAmountLive、ライブカタログのプロバイダーチェック)と PIN ゲートを再利用します。スケジュールは継続的な支出となるためです。このツール自体は料金を請求しません。ウォレットで承認済みのオンチェーン利用枠が金額をすでにカバーしている場合、スケジュールは期限が来るたびに自動で支払いを行います。それ以外の場合は通知専用として保存され、期限が来た際に別の処理から pay_bill を呼び出す必要があります。チャットと同様、EDUCATION と INTERNATIONAL は予約できません。MCP には後から「請求が実行されました」というメッセージを送信する永続的なチャネルがないため、schedule_bill ではそのためのオプションとして customer_email を受け付けます。指定しない場合は、何が起きたかを確認するために、自分で list_schedules/transaction_history を確認してください。
この機能がある理由: 8004scan.io などのサードパーティ製エージェントスキャナーは、宣言された a2a または mcp サービスに対してのみヘルスチェックを実行します。両方を宣言している public/.well-known/agent.json の services 配列を参照してください。この宣言の背後にある MCP サーバーは実際に動作しており、スキャナーを満足させるためだけに追加されたスタブではありません。
チャットと同じ信頼モデルであり、新たなモデルではありません。 pay_bill はセキュリティロジックを再実装せず、Telegram/WhatsApp/X および複数受取人への一括処理フローをすでに支えている関数を直接呼び出します。具体的には、checkPinAllowed/verifyPin/recordPinFailure(同じ段階的なロックアウト)、checkServiceAllowed(キルスイッチ)、checkAccountNumber/checkAmount(同等の検証)、checkAgentSpendAllowed(オペレーターが設定する 1 回あたり/1 日あたりの上限)、checkAutonomousCapacity + executeAgentPayment(オンチェーン利用枠の確認、共通の割引エンジン、売買処理)、および notifySpendOutOfBand(資金が動いた瞬間に、メールとリンク済みのすべてのチャネルに通知されるため、漏えいした API キーも盗まれたチャットセッションとまったく同じように検知されます)。
レート制限は多層防御として上記に追加されるものであり、上記の代わりではありません。 すべての MCP ツール呼び出しに適用される IP 単位の一律の上限に加え、特に pay_bill、schedule_bill、pay_bill_batch には、より厳しい ID 単位の制限があります。具体的な数値と仕組みについては、「セキュリティアーキテクチャ」のレート制限を参照してください。
チェーンに依存しません — 紐付けたウォレットが承認した Celo または Base を使用します。 MCP キーは、作成時に記録されたapproved_chain/approved_tokenを引き継ぎます(Telegram/WhatsApp/X ですでに使われているものと同じフィールドです)。また、check_balanceではchainを明示的に上書きできます。MCP レイヤー自体に Celo 固有または Base 固有の要素はありません。ほかのすべてのチャネルと同じマルチチェーンのリレイヤー(src/lib/deai/relayer.ts)と残高リーダー(src/lib/deai/services.ts)を共有しています。
OAuth 2.1 — ブラウザーで一度認証すれば、API キーを再入力する必要はありません。
api_key/pinツールの引数は使えますが、新しい Claude の会話には何も記憶されないため、ユーザーは毎回 API キーを貼り付ける必要がありました。サーバーは現在、PKCE(S256 のみ)と動的クライアント登録を備えた完全な OAuth 2.1 認可コードフローを実装しています。
| エンドポイント | 目的 |
|---|---|
/.well-known/oauth-protected-resource(+ /.well-known/oauth-protected-resource/api/mcp) | このリソースが OAuth で保護されていることと、認可サーバーの場所をクライアントに伝えます |
/.well-known/oauth-authorization-server(+ /.well-known/oauth-authorization-server/api/mcp) | 認可サーバーのメタデータ |
/api/oauth/register | 動的クライアント登録(RFC 7591) — 手動でのクライアント設定は不要です |
/api/oauth/authorize | 同意ページ — ユーザーに表示される唯一のページです |
/api/oauth/token | コード交換+リフレッシュ。リフレッシュトークンをローテーションします |
クライアントごとに異なるパスを確認するため、どちらのディスカバリードキュメントも、通常の/.well-known/…パスと、RFC のパス挿入形式である/api/mcpを追加したパスの両方で提供されます。
この実装について知っておくとよいこと:
- 認可コードが発行される前に、同意ページで API キーと PIN の両方が確認されます。 API キーだけを盗まれても、コネクターを認可することはできません。
- OAuth では支払いは認可されません。 Telegram/WhatsApp と同様、
pay_billを呼び出すたびに PIN が必要です。Bearer トークンだけでできるのは、残高の読み取りまでです。 - 実際の HTTP 401 が返されるのは、認証情報がまったく指定されていない場合だけです(
WWW-Authenticate+resource_metadata付き)。この 401 が、MCP クライアントが「このサーバーは OAuth に対応している」と判断し、接続ボタンを表示するための唯一のシグナルです。API キーの誤り、PIN の誤り、不正な引数は、インバンドのツールエラーとして扱われます。これらを 401 にすると、タイプミスのたびにクライアントがブラウザーでのフロー全体を再実行することになります。 redirect_uriは何もレンダリングされる前に登録済みのリストと照合されます。 検証に失敗した場合は、リダイレクトするのではなく、シンプルなエラーページを表示します。未検証の URI にリダイレクトすると、認可コードが漏えいするオープンリダイレクトの脆弱性がまさに生じるためです。- 同意ページは、手書きの自己完結型 HTML 文字列です(インラインの
<style>のみ。スクリプト、フォント、サードパーティのアセットはありません)。そのため、例外なくアプリの CSP に準拠し、アプリ内のほかの要素の影響で表示が崩れることもありません。スタイルには AbaPay 独自のエメラルド色のワードマークを使用しています。 - リフレッシュトークンは使用するたびにローテーションされます(パブリッククライアントに対する OAuth 2.1 の要件です)。そのため、正規のクライアントがリフレッシュすると盗まれたリフレッシュトークンは使えなくなり、盗難は検知できるようになります。
紐付け(新しい環境変数もテーブルも不要): agent_links.channelに'MCP'という値が追加されました(supabase/migrations/019_mcp_channel.sql)。既存のTELEGRAM/WHATSAPP/Xと同様です。これらとは異なり、リンクコードを「取得」するボットはありません。アプリのAgent HubタブでMCP(AI Agents)タイルを選び、PIN を設定すると、256 ビットの API キーが生成されます。このキーは一度だけ表示され(保存されるのは SHA-256 ハッシュのみです)、ウォレットには複数の MCP キーを同時に保持できます。キーはエージェント/ツールごとに 1 つで、それぞれに独自のラベルを付けられます。
AI エージェント(エンドユーザー)の接続:
- AbaPay アプリで、エージェントに使用させたいチェーン/トークンのオンチェーン支出上限を承認します(Agent Hub → ステップ 1)。これが実際の上限であり、以下の設定でこれを超えることはありません。
- Agent Hub で MCP (AI Agents) を選び、必要に応じてキーに名前(例:「Claude」)を付け、PIN を設定して、表示された API キーを保存します。API キーは再表示されません。
- MCP クライアントで、
https://agents.abapays.com/api/mcpをリモート(Streamable HTTP)サーバーとして指定します。- 最も簡単な方法 — claude.ai(ウェブ)、ファイル編集不要: Settings → Connectors(一部のアカウントでは「Integrations」)→ Add custom connector →
https://agents.abapays.com/api/mcpを貼り付け → Save。これで、新しいチャットであればどれでも、ツールソースとして利用できます。 - Claude Desktop(ローカル設定ファイル) —
%APPDATA%\Claude\claude_desktop_config.jsonに以下をマージします(すでに他のサーバーを設定している場合、ファイル全体を上書きしないでください)。{ "mcpServers": { "abapay": { "url": "https://agents.abapays.com/api/mcp" } } } - その他の MCP クライアント: 同じ URL を Streamable HTTP トランスポートで使用します。接続時に API キーや認証ヘッダーは不要です。ステップ 4 を参照してください。
- 最も簡単な方法 — claude.ai(ウェブ)、ファイル編集不要: Settings → Connectors(一部のアカウントでは「Integrations」)→ Add custom connector →
- 認証します。 クライアントが OAuth に対応していれば(claude.ai と Claude Desktop は対応しています)、Connect ボタンが表示されます。これをクリックすると、ブラウザーで AbaPay 独自の同意ページが開き、API キーと PIN の入力を一度だけ求められます。以後の会話では自動的に再接続されます。クライアントが OAuth に対応していない場合は、代わりに
api_keyツール引数として API キーを渡します。これはツール引数であり、HTTP ヘッダーではないため、アプリレベルで別途認証する手順はありません。 - いずれの場合も、支払いのたびに PIN の入力が求められます。 コネクターの認証は、支出の承認ではありません。エージェントが PIN なしで支払えると言う場合は、何か問題があります。
すべてのツールで annotations(title、readOnlyHint/destructiveHint、idempotentHint、openWorldHint)が宣言されています。pay_bill は、実際のお金を移動し、2 回呼び出すと 2 回支払われるため、破壊的/非冪等として正しく設定されています。一方、describe_capabilities/check_balance は読み取り専用です。これにより、クライアントはエージェントにツールの実行を許可する前に、適切な警告をユーザーに表示できます。
claude.ai の Connectors Directory への掲載(URL を貼り付ける代わりに、ユーザーが閲覧/検索して AbaPay を見つけられるようにすること)は、ここで構築したものとは別の手順です。コードの変更ではなく、Anthropic を通じた組織としての申請が必要です。
- claude.ai の Team または Enterprise 組織が必要です(申請は admin settings → Directory → New submission から行います)。申請できるのは Owners(Enterprise では委任されたロールのユーザーも可)のみです。
- Anthropic の申請ドキュメントで確認されている要件:ツールの
titleとreadOnlyHint/destructiveHintアノテーション(✅ 上記のとおり対応済み)、公開ドキュメントの URL(https://abapays.com/docs— 公開中)、プライバシーポリシーの URL(https://abapays.com/privacy— 公開中)、アイコン、審査担当者用のテストアカウント認証情報。 - OAuth 2.0 は実装済みです。 Directory では認証が必要なコネクターに OAuth が必須です。AbaPay には OAuth が実装されています(上記の OAuth セクションを参照)。認可コード+PKCE、Dynamic Client Registration、ディスカバリーメタデータ、クライアントが検出できる実際の 401 応答に対応しています。Directory への掲載に必要な残りの作業は、Team/Enterprise 組織、アイコン、審査担当者用のテストアカウント認証情報を用意して、組織として申請することです。コードの変更ではありません。
- 申請して承認されるまでは、上記の URL を使った「Add custom connector」で、現在も制限なく利用できます。Directory によって追加されるのは見つけやすさであり、機能ではありません。
A2A サーバー(Agent2Agent)
同じツールを、MCP ではなく A2A 経由で 他のエージェント から利用できます。 新たな機能はなく、ファイルが2つあります。
| サーフェス | パス |
|---|---|
| エージェントカード(検出) | /.well-known/agent-card.json — src/app/.well-known/agent-card.json/route.ts |
| JSON-RPC エンドポイント | /api/a2a — src/app/api/a2a/route.ts |
1つの実装、2つのプロトコル。 ツールの定義と実装は src/app/api/mcp/route.ts から src/lib/deai/mcpTools.ts に移され、両方のルートがこれをインポートします。
/api/mcp と /api/a2a は現在、1つの callTool() を利用するトランスポート用シムです。これは意図的な設計です。
A2A には資金にアクセスする独自の経路はありません。PIN ゲート、段階的に厳しくなるロックアウト、オンチェーンの利用許可上限、キルスイッチ、運用者の支出上限はすべて両方のルートの 下位 にあるため、新しいプロトコルによって変わるのはエージェントの要求方法であり、実行可能な操作ではありません。(移行は機械的なもので、ツールのロジックはバイト単位で同一です。)
⚠️ /.well-known/agent-card.json は /.well-known/agent.json ではありません。 後者は AbaPay の ERC-8004 登録カード で、8004scan と Aigora が読み取るオンチェーン ID です。
歴史的に、この2つの仕様は agent.json というファイル名で衝突していました。そのため、A2A はカードのファイル名を agent-card.json に変更しました。
ドキュメントも利用者も異なるため、一方を他方で上書きすると、エージェントのオンチェーン掲載が気づかれないまま壊れます。
呼び出し経路に LLM はありません。 チャットチャネルでは、人間が入力した自由形式のテキストを parseIntent() に渡して処理します。
A2A はマシン間の通信であるため、呼び出しには { skill, args } を含む構造化された DataPart を使用し、MCP が公開するものと同じ TOOLS スキーマに照らして検証します。
テキストパートには推測で応答せず、スキルカタログを返します。エージェント間の 決済 経路で「5000を送って」を言語モデルに再解釈させても、メリットがないまま障害要因が増えるだけです。
設計上、同期処理です。 すべてのスキルはリクエスト内で完了するため、message/send は最終的な Message を返します(仕様に準拠)。Task は作成されません。
そのため、カードでは streaming: false と pushNotifications: false を宣言し、tasks/* と message/stream は半端な実装にするのではなく、-32004 UnsupportedOperation を返します。照会可能なタスクストアはなく、存在しない機能を宣言すると、更新が届くことのない状態でピアを待たせることになるためです。
認証。 Authorization: Bearer … では、プレフィックスに基づき、MCP が受け付ける2種類の認証情報のいずれかを受け付けます。aba_mcp_… Agent Hub キー、または OAuth 2.1 アクセストークンです。
A2A には呼び出しごとの api_key 引数がないため、認証情報を渡せる場所はヘッダーだけです。認証情報が ない 場合と、認証情報が 誤っている 場合は、MCP と同じ規則に従い、引き続き区別されます。pay_bill では引き続き、毎回 PIN が必要です。
運用者による制御。 platform_settings.kill_switches の CHANNEL_A2A は、CHANNEL_MCP とは別に設定されます。そのため、一方のサーフェスを停止しても、もう一方は停止しません。
このカラムは自由形式の JSONB で、キーが存在しない場合は有効として扱われるため、マイグレーションは不要です。
呼び出し方:
curl -X POST https://agents.abapays.com/api/a2a \
-H "Authorization: Bearer aba_mcp_…" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"message/send","params":{"message":{
"kind":"message","role":"user","messageId":"1",
"parts":[{"kind":"data","data":{"skill":"check_balance","args":{}}}]}}}'
x402 決済(メインアプリのみ)
⚠️ デフォルトで有効です。NEXT_PUBLIC_X402_ENABLED=false にすると、コントラクト呼び出しにフォールバックします。上記の環境変数セクションに記載したウォレット警告のトレードオフに注意してください。x402 で必要となる EIP-3009 署名は、構造上、資産を抜き取る攻撃者が求めるものと同じであるため、一部のスキャナーで警告が表示されます。
ウェブアプリの決済フローでは、ユーザーが Celo 上で USDC、USD₮、または USA₮ を使って支払う場合、x402 を介して決済します。利用するのは thirdweb ではなく、Celo 独自のファシリテーター(api.x402.celo.org、Celo Core Co. が構築。src/app/api/pay/x402/route.ts を参照)です。Circle の USDC と Tether の USD₮/USA₮ のデプロイでは EIP-712 ドメインが共通ではないため、それぞれのトークンは個別の EIP-712 ドメイン(同ルートの X402_TOKEN_EIP712)に対して決済されます。同じ「確認して支払う」ボタンで、Celo 上の 3 種類のトークンはいずれも x402 経由となり、それ以外はすべて通常の payBill コントラクト呼び出しを経由します(EIP-3009 に対応していないトークン、NEXT_PUBLIC_BASE_X402_ENABLED=false の場合の Base、または x402 が未設定の場合)。x402 の決済には、その決済に対する支払人の EIP-3009(transferWithAuthorization)署名が必要なため、単にトランザクションの表示名を変えたものではなく、決済が実際に x402scan 上で確認できます。この要件こそが、スキャナーで問題視される理由でもあります。
402 チャレンジ自体は内製しています(プレーンな x402 v1 のボディベース応答)。SDK のデフォルトには依存していません。これは意図的な選択です。thirdweb 独自の settlePayment() は常に、JSON ボディが空のまま、新しいチャレンジを Base64 ヘッダーで返します。x402scan の検出クローラーを含む汎用 x402 スキャナーはこれを解析できず、見た目は正しい 402 ステータスが返るのに利用可能なチャレンジがなく、登録が何も通知されずに失敗します。
クライアント側では、src/lib/x402Pay.ts がチャレンジの読み取り、署名、再試行を行います。すべてのコントラクト呼び出しで使うものと同じ viem ウォレットクライアントを使用するため、フロー全体でウォレット接続は 1 つだけで、wagmi と並行する別のスタックはありません。署名処理を内製することで、サーバーからの実際の応答もページ上で確認できます。tx_hash を含む決済失敗は、すでに資金が移動したことを意味するため、アプリはコントラクト呼び出しの経路で決して「再試行」してはなりません。
ボディベースの v1 チャレンジには hint フィールドも含まれます。これは x402 仕様の範囲外で追加されているフィールドですが、既知のフィールドしか読み取らないクライアントでは無視されます。EIP-3009 のオーソリゼーションに署名できる能力と、チャレンジ/署名/再試行のシーケンスを実行するクライアントを備えていることは、別々の能力です。前者は備えていても後者を備えていないウォレットでは、先に進む手段のない 402 がそのまま返されます。hint は、x402 が不要な通常のガイド付きフロー(abapays.com)を、その呼び出し元に案内します。
決済を拒否された場合は、決済経路を放棄する前に再試行します
🔵 x402 決済が拒否されても、すぐにフォールバックせず、実際に再試行する理由。 transferWithAuthorization のシミュレーションがリバートすると、ファシリテーターは unable to estimate gas/invalid_payload を返しますが、トランザクションは発行されず、資金も移動しません。この場合にコントラクト呼び出しへフォールバックすると、x402 経路なら 2 回目の試行で 1 回のプロンプトだけで決済できることが多い請求に対し、さらに 2 回(承認 + payBill)のプロンプトが必要になります。そのため、再試行は次の 3 つのレベルで自動的に行われます。
- ファシリテーターが呼び出される前の段階です。
src/lib/x402Settle.tsは、ペイロードから確認できる EIP-3009 のリバート条件、つまり有効期間の期限切れ、クロックずれによるvalidAfter、受取人の誤り、サーバーが必要額として宣言しようとしている金額との不一致を判定します。そのため、決済できないペイロードには、不透明なリバートではなく、新しいチャレンジを返します。paymentRequirementsが宣言する値は、再計算した価格ではなく、署名済みのvalueです。DB の行と返金記録もこの値を使います。為替レートの変動により両者が異なることがあり、不一致の組み合わせを受け取ったファシリテーターは拒否します。 - サーバー側では、同じ認可に対して一度だけです。 EIP-3009 の nonce は一度しか使えないため、何回送信してもトークンが受け付けるのは最大一度です。リトライでリクエストが重複することはあっても、送金が重複することはありません。
- クライアント側では、新しい署名で一度だけです。サーバーが拒否を
retryableと判定し、tx_hashを含まない場合に限ります。2 回目のプロンプトは事前に通知します。説明のないプロンプトは、アプリが最初のプロンプトを無視したように見えるためです。
🔵 チェーンが送金されていないと示すまで、リトライは提示されません。 ここが重要です。unable to estimate gas/invalid_payload は、その仕組み上、曖昧なエラーです。EIP-3009 の nonce は一度しか使えないため、すでに成功した認可を再シミュレーションすると必ずリバートし、ガス推定中のリバートは、まだ何も起きていない場合も、すでにすべて完了している場合も同じように報告されます。メッセージの照合だけでは、この 2 つを区別できません。誤った判定でリトライを提示すると、1 件の請求に対して支払者に新しい nonce への署名と 2 回目の支払いを求めることになります。そのため、コードはメッセージに一切依存しません。すべての EIP-3009 トークンは authorizationState(authorizer, nonce) -> bool を公開しています。これは、該当の認可が消費されたかどうかを示すチェーン独自の記録であり、リトライを提示する前に必ず確認します(決済ルートの buildAuthorizationStateCall と authorizationWasConsumed)。使用済みの場合は、トランザクションハッシュなしで決済済みとして報告します。クライアントは処理を停止し、コントラクト呼び出しへのフォールバックは抑止され、オペレーターへのアラートには、手動で照合できるよう支払者と nonce が含まれます。回答を読み取れない場合も、使用済みと同じ扱いにします。不要なフォールバックならプロンプトが 2 回表示されるだけですが、誤って提示されたリトライでは支払者が実際に損をします。
文言による判定は、今も簡易的な最初のフィルターとして使われます。FiatTokenV2: authorization is used or canceled は即座に拒否されます。一方、nonce too low と replacement transaction underpriced(別の nonce に関する、ファシリテーター自身のEOA 間の競合)は、引き続きリトライ可能です。ただし、リトライを実際に許可するのはフィルターではなくチェーンです。
決済にどの経路を使うかは、トークンだけでなくウォレットにも左右されます。 x402 の決済では eth_signTypedData_v4 の署名がページに返される必要があります。このメソッドをネゴシエーションしていない WalletConnect セッションでは、リクエストが破棄され、プロンプトもエラーもなく、何も返りません。確認するのはこの点だけで、セッション自体を調べます。src/lib/walletEnv.ts の walletCanSignTypedData を参照してください。
🔵 ウォレットの名前によって経路を決めることはありません。 すべてのウォレットに型付きデータへ署名できるかを確認し、特定のウォレットの動作についてハードコードされた想定ではなく、ウォレット自身の回答に基づいて決定します。たとえば Valora は、型付きデータのリクエストを「Verify wallet」として表示し、署名を返さないまま接続成功を報告することがあります。回答が返ってこない場合は署名がタイムアウトし、決済のための送信はまだ行われていません(src/lib/x402Pay.ts は署名を待機した次の行で決済リクエストを送信するため、処理を巻き戻せば、遅れて届いた署名が誰にも送られることはありません)。その後、ページは自動的にコントラクト呼び出しへフォールバックします。
この署名要件があるからこそ、これはメインアプリのみに限定されています。上記のエージェント起点のフローは、支払い時に署名をまったく行わないこと(setSpendingAllowance の本来の目的)を前提としており、x402 の都度署名するモデルとは根本的に両立しません。Telegram/WhatsApp/X と自律型スケジューラーは x402 を使用せず、影響も受けません。これらの支払いはすでに RELAYER_ADDRESS から実行されており、これは以下で ERC-8004 の ID に登録されているものと同じウォレットなので、x402 を使わなくてもすでにエージェントに帰属しています。
- 対象:Celo + USDC/USD₮/USA₮。本番稼働を確認済み — 単なる注意事項ではありません。ネイティブ Celo USDC(Circle の FiatTokenV2)とネイティブ Celo USD₮(Tether のデプロイ)はどちらも EIP-3009 の
transferWithAuthorizationを実装しています。USDm は実装していません(Mento トークンが公開するのは EIP-2612 のpermit()のみ)。そのため、決済に使える署名方式がありません。これがまさに、ピッカーで USDm が USA₮ に置き換えられた理由です。独自に設けた制限ではありません。後から別のトークンのサポートを追加する場合も、トークンと小数桁数はすでにresolveTokenOnChainで汎用的に解決されるため、その EIP-712 ドメインを追加するだけで済みます。 - 請求サブスクリプションではなく、前払いクレジットです。 Celo のファシリテーターは、前払いの USDC クレジット残高(
CELO_X402_API_KEY)から決済ごとに一律 $0.001 を請求します。チャージは x402.celo.org で行ってください。クレジットが 0 になると、/settleは 402 を返し始め、アプリから Telegram アラートが送信されます(これは支払者ではなく運用者側の問題です。チャージされるまで再試行しても解決しません)。 - x402 が失敗しても、コントラクト呼び出しフローへ自動フォールバックしません。 サーバー到達後に x402 でエラーが発生した場合、ファシリテーターでの決済が実際には完了していて応答だけが通信中に失われていると、コントラクト呼び出し経由で再試行することでユーザーに二重請求するおそれがあります。これは、
processBlockchainPayment自体のペイマスター・フォールバック処理が慎重に対処しているのと同種のリスクです。ユーザーには明確なエラーが表示され、代わりに手動で再試行できます。 - 資金は同じボールトに入ります。
payToは既存のAbaPayV3コントラクトアドレスに設定されています。管理者ダッシュボードが残高を読み取り、返金や出金を管理するのに使っているものと同じアドレスです。ボールトのbalanceOfはトークンの受け取り方法を区別しないため、x402 で決済された資金も、既存の管理ツールからはコントラクト呼び出しで入金された資金と区別できません。コントラクトの変更は不要です。 - 販売/返金のロジックは共通であり、重複していません。 オンチェーン経由(
/api/pay)と x402 経由(/api/pay/x402)のどちらも、支払いの検証後に同じexecuteVend()(src/lib/vend.ts)を呼び出すため、自動返金のセーフティネットはどちらの決済経路にも同じように適用されます。 transactions.payment_method(010_x402_payment_method.sql)では、既存のsource_channel(別の軸である UI チャネルと決済経路)とともに、CONTRACTとX402をひと目で区別できます。
🔌 プロバイダーデータと運用者向けコントロール
ライブプロバイダーカタログ(VTpass提供)
src/lib/vtpassCatalog.ts は、アプリ内のすべてのプロバイダー一覧の唯一の信頼できる情報源です。
VTpass の /services?identifier=… を呼び出し、プロバイダーの実際の serviceID、VTpass 独自の商品名、VTpass 独自のロゴ URL、および公開されている minimium_amount(原文ママ)と **maximum_amount**を返します。
VTpass が実際に受け付ける識別子をキーとして、5 つのカテゴリに対応しています(そのうち 2 つは調査して見つける必要がありました。明らかな候補では 011 "Category Does not Exist" が返されます)。
| アプリの概念 | VTpass 識別子 |
|---|---|
| Airtime | airtime(国際サービスは独立したフローのため、foreign-airtime を除外) |
| データ/インターネット | data — mobile-data/internet ではありません |
| 電気 | electricity-bill |
| ケーブルテレビ | tv-subscription — cable-tv ではありません |
| 教育 | education |
キャッシュと障害時の動作(重要なポイントです):
- モジュールレベルの
Mapキャッシュを使用し、TTL は 1 時間で、プロセス内で共有されます。ブラウザーからはGET /api/providers?category=…(IP ごとに 1 分あたり 60 回までレート制限、Cache-Control: public, max-age=300, s-maxage=3600, stale-while-revalidate=86400)を通じてアクセスします。チャットと MCP はgetCatalog()を直接呼び出します。どちらも同じキャッシュを利用するため、信頼できる情報源は一つだけです。 - 失敗時にキャッシュが削除されることはなく、成功時にのみ上書きされます。 フォールバックの順序は
有効なキャッシュ → ライブ取得 → 古いキャッシュ → 同梱シード です。
getCatalog()は例外をスローせず、 空のリストも返しません。そのため、ピッカーは常に表示でき、VTpass の一時的な障害によって購入途中に ピッカーが空になることもありません。 - 古いデータを返す場合は
stale: trueが設定され、/api/providersはそのデータをno-storeで配信します。 これにより、短時間の障害がエッジキャッシュに 1 時間固定されることを防ぎます。 src/lib/providerFallback.tsは最後の手段となるオフライン用シードで、ハードコードされたプロバイダーデータとして 残っている唯一のものです。ロゴには意図的にローカルファイルを使用しています。「VTpass に接続できない」場合に 実行される唯一のコードパスで、多数のリモート画像が壊れた状態で表示されるのを避けるためです。
ハードコードされた一覧を置き換えた理由: 以前の一覧には showmax、spectranet、jamb が掲載されていましたが、
この加盟店アカウントでは 3 つすべてが Service is Not Valid を返します。そのため、ユーザーがいずれかを選んで
フォームに入力し、オンチェーンで支払った後に、ようやく販売処理が失敗して返金フローに進む可能性がありました。
また、稼働中なのに利用できなかった glo-sme-data と 9mobile-sme-data も一覧から漏れていました。
金額制限も同じレコードから取得します。limitsFor(category, serviceID) と
limitsForIntent(intent, provider) は、プロバイダーごとの最新の {min, max} を返し、VTpass が公開していない場合は
null を返します。そのため、「不明」は「無制限」と誤認されず、呼び出し元のサービスレベルのデフォルト値にフォールバックします。
キルスイッチ(2階層:マスター+プロバイダー別)
platform_settings.kill_switches には、管理者ダッシュボードが書き込む2階層のキーシステムが格納されています。
| レベル | キー |
|---|---|
| サービスごとのマスター | MASTER_AIRTIME、MASTER_INTERNET、MASTER_ELECTRICITY、MASTER_CABLE、MASTER_EDUCATION、MASTER_INTERNATIONAL |
| プロバイダー別(VTpass serviceID をキーに使用) | AIRTIME_mtn、INTERNET_airtel-data、ELEC_ikeja-electric、CABLE_dstv、EDU_waec、… |
スイッチは明示的に false に設定されていない限りオンです(キーが存在しない場合は有効)。どちらか一方の階層がオフの場合、支払いは拒否されます。これは Web アプリが使用するものと同じ || です。
src/lib/serviceRules.ts の killSwitchKeysFor(intent, provider) は、エージェントのインテントをこれらのキーにマッピングします。まず resolveServiceId を通じてプロバイダーを正規化するため、チャットで指定された曖昧な "ikeja" も、オペレーターが実際に切り替えた ELEC_ikeja-electric キーに解決されます。続いて checkServiceAllowed() が、チャット、MCP、A2A、スケジューラーが通過するゲートになります。Web の決済経路(/api/pay および /api/pay/x402)は checkWebPayment() を通過します。これは同じスイッチに加え、VTpass のプロバイダーごとの最新の金額上限も確認し、署名や決済確定の前に判定します。以前は Web ページでボタンをグレーアウトするだけに頼っていたため、直接 API や x402 の呼び出し元は、スイッチで停止されたサービスにも支払いができていました。International は Web アプリ独自のルールに従い、MASTER_INTERNATIONAL が存在し、かつ false でない場合を除いてオフになります。設定は30 秒間キャッシュされるため、スイッチの切り替えは全体で最大30秒以内に反映されます。
自動フロートブレーカー。 VTpass が 018(フロート残高が空)を返すか、Monnify が D04 を返すと、platform_settings.provider_circuits のブレーカーが作動します(src/lib/circuitBreaker.ts、マイグレーション 028)。作動中は、そのプロバイダーが提供するすべてのサービスが、すべての決済経路で顧客が支払う前に拒否されます。以前は各支払者に請求した後で返金していました。プロバイダーの残高がアラートしきい値を超えると、ブレーカーは自動で閉じます。スケジュールされた残高チェックで検知されるほか、顧客がブレーカーに遭遇した際にも、レート制限付きのプローブで検知されます。オペレーターは管理アクション RESET_PROVIDER_CIRCUIT を使って閉じることもできます。ブレーカーは kill_switches とは別に保存され、そちらを変更することはありません。そのため、オペレーターが設定したスイッチは常に優先されます。CIRCUIT_BREAKER_ENABLED=false にすると、この機能は無効になります。
⚠️ このマッピングが重要な理由:
killSwitchKeysFor()によって、ダッシュボードで「電気サービスを一時停止」すると、Web サイトだけでなくチャット、MCP、自律スケジューラーも実際に停止します。4つすべての経路で同じインテントが同じMASTER_/プロバイダー別キーに解決されるため、ある場所で切り替えたスイッチがどこでも適用されます。
サービスごとのスイッチとは別に、checkAgentSpendAllowed() はエージェント固有のオペレーター制御を適用します。agent_enabled(エージェントによるすべての支払いを停止するマスターキルスイッチ)、agent_autonomous_enabled(無人/スケジュール実行のみを停止)、agent_max_ngn_per_tx、agent_daily_cap_ngn(ユーザーごと、UTC の1日あたり)、およびアプリ内ウィジェット用の ai_chat_enabled です。これらはオンチェーンの利用許可に追加して適用され、利用許可の代わりにはなりません。
Bank Transfer には独立したダッシュボードトグル(BANK キー)があります。VTpass プロバイダーの選択ではなく Monnify/Moniepoint 経由で決済されるため、プロバイダー別の内訳はありません。これはエージェントのゲート(serviceRules.ts の BANK_TRANSFER)と、Web アプリの page.tsx にある isCurrentServiceDisabled の両方で確認されます。
さらに4つの独立したスイッチは、商品ではなくチャネル全体を一時停止します。CHANNEL_WHATSAPP、CHANNEL_TELEGRAM、CHANNEL_X、CHANNEL_MCP で、serviceRules.ts の isChannelEnabled() によって適用されます。WhatsApp/Telegram/X は、共通の /api/deai/core エンジンの最上部で一度確認されます(3つともここを経由します)。MCP は /api/mcp の tools/call で確認されます。ここでも、他のすべてのスイッチと同じく「キーが存在しない場合は有効」がデフォルトです。
🧪 テストと CI
npm test # Run the unit test suite (Vitest)
npm run test:watch # Watch mode
npm run test:coverage # Coverage report
npm run typecheck # tsc --noEmit — catches type errors before they hit a deploy
現在のテストでは、PIN のハッシュ化と検証、内部サービスの認証、決済金額とトークン検証の不変条件(ユーザーが未払いの請求を抱えないようにするチェック)など、セキュリティ上重要な純粋ロジックを対象としています。
CI は .github/workflows/ci.yml を介してプッシュまたは PR ごとに実行されます:型チェック → lint → ビルド → テスト → 依存関係の監査。型チェックのステップは、TypeScript のエラーが本番環境へのデプロイに到達する前に検出することを目的としています。
🗄️ データベースのセットアップ
コアテーブルに加えて、Supabase SQL エディターで supabase/migrations/ 内のマイグレーションを順番に実行してください:
001_rate_limits.sql—src/lib/rateLimit.tsが必要とするrate_limitsテーブルを作成します。 このテーブルがないと、レート制限は警告なしに無効化されます。そのため、課金対象の VTpass/WhatsApp/Claude エンドポイントを保護するスロットリングを利用する前に適用してください。002_customer_details.sql— 領収書に記録する顧客情報です。003_scheduled_bills.sql— Bill Pay & Autopay Agent のスケジュール設定です。004_agent_links.sql— ウォレットを Telegram/WhatsApp/X の ID に紐付け、DeAI エージェントがユーザーを認識できるようにします。セキュリティ境界となるのはこのテーブルではなく、AbaPayV3 上のオンチェーンのspendingAllowanceです。これは UX 上のミラーにすぎません。005_autonomous_schedules.sql— スケジュール済み請求を、真に無人で実行できるようにアップグレードします。AbaPayV3 のオンチェーンの許可額が最大損失を制限するため、安全です。006_agent_admin_controls.sql— エージェントの停止スイッチ(agent_enabled、agent_autonomous_enabled、ai_chat_enabled、1 取引あたり/1 日あたりの NGN 上限)をオペレーターに提供します。再デプロイやコントラクト呼び出しを行わずに、管理ダッシュボードの Agent タブから設定できます。007_transaction_source_channel.sql— オペレーターへのアラート用に、各取引の発生元チャネル(Web アプリ/Telegram/WhatsApp/X/無人スケジュール)を記録します。008_refund_queue.sql— 決済後に失敗した販売に対する、返金キューのパイプラインです。009_support_tickets.sql— Web アプリと各ソーシャルチャネルからのサポートチケットを管理し、管理者からの返信をユーザーの元のチャットに返します。010_x402_payment_method.sql—transactionsにpayment_method(CONTRACT|X402)を追加し、決済手段を区別します(x402 settlement を参照)。011_one_off_schedules.sql—scheduled_billsにrun_once_atとbatch_idを追加します。既存の月次/週次/日次の繰り返しスケジュールに加えて、1 回のチャットリクエストで、将来の 1 回限りの決済(frequency = 'once')または複数の宛先への一括決済を作成できます。012_schedule_notify_channel.sql— スケジュールの結果を通知するチャネルを記録します。014–018_discount_*.sql— 割引キャンペーンエンジンです。キャンペーン、キャンペーンごとの上限、宛先/IP ごとの上限、電話番号ごとの上限と不正利用対策の切り替え、除外設定/全ステータスの集計を管理します。 (013はありません。番号が飛んでいます。)019_mcp_channel.sql—agent_links.channelに'MCP'を追加し、TELEGRAM/WHATSAPP/Xと並ぶ、AI エージェントの第一級の紐付けチャネルとして扱えるようにします。020_mcp_oauth.sql— OAuth 2.1 のテーブルです。動的に登録されたクライアント、1 回のみ使用できる認可コード(PKCE チャレンジ付き)、アクセストークン/リフレッシュトークンのレコード(ハッシュ化、ローテーションと失効に対応)を管理します。
📱 MiniPay でのテスト
AbaPay はモバイル Web3 エクスペリエンス向けに高度に最適化されています。Celo MiniPay 環境内で dApp をテストするには:
- プロジェクトをデプロイします(例:Vercel)。
NEXT_PUBLIC_NETWORKをcelo-sepolia(テストネット)またはcelo(メインネット)に設定し、NEXT_PUBLIC_APP_MODEを適宜sandboxまたはproductionに設定します。- Android で Opera Mini ブラウザーを開き、MiniPay タブに移動して、デプロイした URL を入力します。
Farcaster Mini App としてテストする
アプリには Farcaster フレームのメタデータ(public/.well-known/farcaster.json と layout.tsx 内のフレーム設定)が含まれています。公開 URL にデプロイし、Mini Apps に対応した Farcaster クライアントでリンクを共有すると、アプリを直接起動できます。
🛡️ セキュリティアーキテクチャ
- ログに残さないキー: VTpass のシークレットキー、Supabase のサービスロールキー、Telegram のトークン、その他すべてのシークレットは、サーバーサイドの API ルート内に厳重に保管され、クライアントバンドルに公開されることはありません。
- リプレイ攻撃の防止: すべてのブロックチェーントランザクションハッシュは永続台帳(tx hash に一意制約を設けた Supabase テーブル)に記録され、1 つのハッシュが紐づけられるトランザクションは最大 1 件です。ウェブアプリの支払いインテントは、サーバーが発行する ID で作成され、挿入として書き込まれます(上書きは行いません)。データベーストリガー(
supabase/migrations/026_transaction_state_guard.sql)により、完了済みのトランザクションを pending に戻すこと、実際の tx hash を変更すること、pending から移行した後にトランザクションのプロバイダー参照を変更することが拒否されるため、決済済みまたは返金済みの支払いが再度販売されることはありません。⚠️ インメモリの追跡だけでは、サーバーレス環境で安全ではありません。コールドスタート時に状態がリセットされ、並行して動作するインスタンス間で共有されないためです。 - オンチェーン検証: 請求書を販売する前に、サーバーはトランザクションレシートを読み取り、設定済みの AbaPay vault が、リクエストの値ではなく、支払い開始時に記録されたトークン、金額、アカウント、サービス、支払者に対する
PaymentReceivedを発行したことを必須とします(src/lib/paymentProof.ts)。小数桁数はイベントで指定されたトークンから取得されるため、別のトークンでの支払いが他のトークンでの支払いとして通ることはありません。このイベントは、直接のウォレット呼び出し、Base 上のスポンサー付き ERC-4337 UserOperation、エージェントリレイヤーのいずれでも同じ方法で発行されます。販売される請求書はクライアントではなく、サーバーに保存されたレコードから読み取られます。 - イベントの相互検証: webhook は
PaymentReceivedイベントをデコードし、販売前にその支払者、トークン、金額、口座番号がすべて保留中のレコードと一致することを必須とします。これにより、ユーザーが少額の保留中インテントを作成した後、別の(またはより大きい/小さい)送金を手動でコントラクトに送り、誤ったレコードに紐づけられることを期待する攻撃を防ぎます。 - 古いインテントの期限切れ: 署名前に作成された事前チェック用インテントのうち、オンチェーントランザクションに至らなかったものは、スケジュールされたクリーンアップ(
/api/cleanup、15 分ごと)によって自動的に期限切れとなり、PENDINGのまま残り続けることはありません。この処理が対象とするのはpreflight_を接頭辞とする行だけなので、実際にブロードキャストされたトランザクションが期限切れになることはありません。 - Webhook の確認応答: リクエストが署名検証に合格すると、一致するトランザクションレコードが見つからない場合でも(テスト用 ping、無関係なアクティビティ、またはまだ同期されていない支払いインテントは正常かつ想定内の結果であり、配信失敗ではありません)、webhook は常に 2xx を返します。ここで 2xx 以外を返すと、実際には失敗ではない繰り返しの「失敗」により、Alchemy が最終的に webhook を自動無効化するおそれがあります。
- VTpass の遅延ステータス webhook:
/api/webhook/vtpassは VTpass が解析する正確な確認応答{"response": "success"}を即座に返し、その後にのみ(after()経由で)実際の処理を行います。VTpass は迅速で軽量な応答を要求し、それ以外は未確認の配信として再試行するためです。プッシュ通知自体は決して信頼しません。ハンドラーは API キーを使ってサーバー間で VTpass に再照会し、確認済みのステータスに基づいてのみ処理します。 - レートの検証: 支払われた暗号資産の金額は、販売前にサーバー側でプラットフォームの最新為替レートと照合されるため、クライアントが改ざんされていても過少払い攻撃を防げます。
- スマートコントラクトの Vault: ユーザーのステーブルコインは、イミュータブルな
AbaPay.solスマートコントラクトの Vault に直接送られます。資金を引き出せるのは、コントラクト所有者によって暗号署名されたトランザクションのみです。バックエンドサービスがユーザー資金を直接カストディすることはありません。 - 自動返金: 検証済みのオンチェーン支払いが販売に失敗した場合(プロバイダーの障害、無効な詳細情報など)、トランザクションにフラグが付けられ、ユーザーのウォレットに返金されます。返金トランザクションハッシュは台帳に記録されます。
- 返金の検証:
/api/admin/refundは、トランザクションをREFUNDEDとして記録する前にオンチェーンで返金を検証します(トランザクションの ERC-20 Transfer ログからトークン、受取人、金額をすべてデコードします)。実際には行われていない返金を管理者が記録することはできません。 - RPC フェイルオーバー: オンチェーンの読み取りでは、複数の RPC エンドポイントにわたる viem の
fallback()トランスポート(src/lib/chain.ts)を使用するため、プロバイダーが 1 つダウンしても支払い検証が停止しません。 - Content-Security-Policy:
Content-Security-Policy-Report-Onlyモードで適用されています(next.config.ts)。ウォレット接続が壊れるリスクを避けながら、違反を検出できます。実際のウォレットフローで検証した後、強制適用するContent-Security-Policyに切り替えてください。 - 管理者認証: 管理者専用 API ルートと
/adminダッシュボードは、公開ストアフロントとは別の専用認証(src/utils/adminAuth.ts)で保護されています。許可リストに登録された運用ウォレットは、Vault の所有者ではなく、ドメインに紐づいた一度だけ使用可能な nonce を含む Sign-In with Ethereum メッセージで一度ログインします。HttpOnly、Secure、SameSite=Strict のセッション Cookie が発行され、アイドル状態では 2 時間、最長で 8 時間有効です。「サインアウト」または「すべてのデバイスからサインアウト」で失効できます。保存されるのはセッションのハッシュのみです。状態を変更する管理者リクエストでは、同一サイトのOriginも必須です。オンチェーンの Vault 操作には、コントラクトによって強制されるため、引き続き所有者ウォレットが必要です。 - 内部専用 AI ルート: DeAI の「ブレイン」(
/api/deai/*)にアクセスできるのは、署名付きの内部サービス用トークン(src/utils/internalAuth.ts)を使うアプリ独自のボット webhook のみです。これにより、公開インターネット上の第三者がチャット ID/電話番号/X ID を使ってユーザーになりすましたり、Claude API の利用予算を消費したりするのを防ぎます。 - ボット webhook の署名: WhatsApp と X の webhook は、受信するすべてのペイロードについて、Meta の
X-Hub-Signature-256/X のx-twitter-webhooks-signatureHMAC を検証します(対応するシークレットが設定されている場合)。また、Telegram はシークレットトークンを検証するため、メッセージイベントを偽造できません。 - ハッシュ化されたトランザクション PIN: DeAI の PIN はソルト付き scrypt ハッシュとして保存され(
src/utils/pinSecurity.ts)、平文で保存されることはありません。従来の平文値は次回使用時に透過的にアップグレードされ、4 回の試行でロックアウトされます。 - スコープ制限付き Paymaster プロキシ: ガススポンサーシップ用プロキシ(
/api/paymaster)は ERC-7677 paymaster JSON-RPC メソッドのみを許可リストに登録するため、CDP キーで動作する汎用 RPC リレーとして悪用できません。また、各 UserOperation(スマートウォレットのexecute/executeBatch)をデコードし、AbaPay 自身の呼び出しのみをスポンサーします。対象は、サポートされているステーブルコインのapprove(vault, …)と、Vault のpayBill/setSpendingAllowanceであり、ネイティブ通貨の値を含まず、Base 上に限られます。それ以外は-32602で拒否され、CDP には到達しません。リクエストは IP ごと、およびスマートウォレットの送信者ごとにレート制限されます(src/lib/paymasterPolicy.ts)。
レート制限
src/lib/rateLimit.ts には、互いに独立した2つのレイヤーが実装されています。どちらも多層防御であり、本当の最後の防衛線ではありません(それを担うのはオンチェーンの利用上限、キルスイッチ、PIN ロックアウトです)。これらは、漏えいした認証情報の所有者が帯域外アラートに気づいて無効化するまでの間、不正利用を遅らせるためのものです。
- レイヤー1 — ルートでの IP 単位の制限。 クライアント IP(
getClientKey)をキーとする固定ウィンドウで、enforceRateLimitを通じてエンドポイントごとに適用します。IP は信頼度の高い順に、x-vercel-forwarded-for、x-real-ip、x-forwarded-forの右端のエントリーから読み取ります(呼び出し元がヘッダーの先頭に値を追加して偽装できるため、左端は使用しません)。 - レイヤー2 — 支払いアクションのみを対象とした、ID 単位の制限。
pay_bill、schedule_bill、pay_bill_batchでは、さらに、呼び出しの認証に使用された認証情報のagent_links行 ID(src/lib/deai/mcpTools.tsのcheckSpendRateLimit)でレートを制限します。IP ではないため、漏えいした API キーや OAuth トークンを使って送信元アドレスをローテーションしても、制限を回避できません。PIN チェックの後に実行されます。段階的に制限が厳しくなる PIN ロックアウトは、誤った PIN を入力した場合にのみ発動し、正しい値での連続した呼び出しを遅らせる効果はないためです。
| スコープ | 上限 | ウィンドウ | 対象 |
|---|---|---|---|
mcp | 60 | 60s | IP 単位のすべての /api/mcp ツール呼び出し |
a2a | 60 | 60s | IP 単位のすべての /api/a2a 呼び出し |
mcp-pay_bill | 10 | 60s | 認証情報単位の pay_bill |
mcp-schedule_bill | 5 | 60s | 認証情報単位の schedule_bill |
mcp-pay_bill_batch | 5 | 60s | 認証情報単位の pay_bill_batch |
oauth-authorize | 40 | 300s | GET /api/oauth/authorize |
oauth-authorize-submit | 15 | 300s | 同意フォームの送信 |
oauth-token | 60 | 300s | トークン交換 |
oauth-register | 20 | 300s | 動的クライアント登録 |
agent-link-read | 60 | 60s | リンク済みエージェントの設定の読み取り |
agent-link-write | 10 | 300s | エージェントリンクの作成/編集/無効化 |
pay | 30 | 60s | REST /api/pay |
schedules-read / schedules-write | 60 / 20 | 60s | /api/schedules |
deai-chat | 20 | 60s | Telegram/WhatsApp/X でのチャットの1ターン |
deai-resolve | 30 | 60s | インテントの解決 |
requery | 30 | 60s | /api/requery |
verify / otp-request / otp-confirm | 20 / 5 / 15 | 60s / 600s / 60s | アカウント認証、OTP の送信、OTP の確認 |
monnify-verify / monnify-banks / monnify-resolve / monnify-requery | 20 / 30 / 8 / 30 | 60s | Monnify の銀行振込照会 |
foreign | 60 | 60s | 国際カタログの読み取り |
support | 5 | 300s | サポートチケットの送信 |
仕組み: Postgres を使用した固定ウィンドウ(rate_limits テーブル)で、rate_limit_hit RPC(supabase/migrations/023_rate_limit_atomic_increment.sql)によりアトミックにカウントを増やします。これにより、並行リクエストが書き込み前にすべて「上限未満」と読み取ってしまう競合を防ぎます。このマイグレーションがまだデプロイされていない場合は、アトミックではない読み取り・変更・書き込みにフォールバックします。フェイルクローズではなく、フェイルオープンです。 rate_limits テーブルまたは RPC にアクセスできない場合、API 全体を停止させるのではなくリクエストを許可します。レート制限は不正利用を抑制するものであり、認証を担うものではないため、単一障害点になってはなりません(001_rate_limits.sql がテーブルを作成します。このテーブルがない場合、レート制限は何もせずに動作し続けます)。制限に達した呼び出しは、Retry-After ヘッダーと JSON 本文 {"success": false, "error": "Too many requests..."} を含む HTTP 429 を返します。
📖 ユーザー向けドキュメント
AbaPay の機能をユーザーに伝えるページは 4 つあります。これらは何かから自動生成されるものではありません。意図して更新しない限り、気付かないうちに古くなってしまうため、変更の付随事項ではなく変更の一部として扱ってください。
| ページ | 場所 | リンク元 |
|---|---|---|
| ドキュメントと FAQ ページ | src/app/docs/page.tsx | AppFooter の「Docs & FAQ」リンク |
| 利用規約(全文) | src/app/terms/page.tsx | AppFooter の「Terms」リンク |
| プライバシーポリシー(全文) | src/app/privacy/page.tsx | AppFooter の「Privacy」リンク |
| この README | README.md | GitHub |
⚠️ /terms と /privacy は弁護士ではなくエンジニアが作成しており、法的なレビューを受けていません。資格を持つ弁護士によるレビューが行われるまでは、いずれも法的な審査を受けたものとして扱わないでください。/terms の末尾には、その旨を明記した目に見える注意書きがあり、ページを書き換える場合も必ず残してください。
/docs はドキュメントと FAQ をまとめた唯一のページです。アプリ内に利用規約、プライバシーポリシー、FAQ 用のモーダルは別途ありません。
🏢 法人
AbaPay は、ナイジェリア連邦共和国の Corporate Affairs Commission(CAC)に RC 9524980 で登記された企業、Masonode Technologies Limited が運営しています。
📄 ライセンス
このリポジトリのコードは MIT ライセンス のもとで公開されており、AbaPay の MCP/x402 決済基盤を統合する独立したエージェントやアプリを含め、自由に利用、フォーク、拡張できます。このライセンスの対象はコードのみです。AbaPay の名称やブランドの使用を許諾するものではなく、公開中の abapays.com サービスの利用に関する法的条件について何ら主張するものでもありません。サービスの利用条件は /terms で別途定められています。
👨💻 メンテナー
Oluwafemi Olagoke(@investorphem)が開発・保守しています。
Web3、分散型 AI、スケーラブルなブロックチェーンアプリケーションに取り組んでいます。
コメント
AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。