mcpField
A
AbaPay

AbaPay

远程服务
github.com/investorphem金融数据1今天更新
代码仓库

关于 AbaPay

AbaPay 是基于 Base 和 Celo 的稳定币账单支付平台,供用户通过应用、聊天渠道或 AI Agent 支付话费、水电等账单。它支持链上限额授权、批量和定时支付,并通过 MCP 提供账单查询与支付工具。

主要功能

  1. 01使用 USDT、USDC 或 USAT 支付账单
  2. 02支持 Base 与 Celo 网络
  3. 03验证商户信息并处理失败退款
  4. 04设置限额、可撤销的 Agent 授权
  5. 05批量支付及定时自动支付
  6. 06通过 MCP 查询余额、计划和账单

适用场景

  1. 01通过聊天或 MCP 购买话费与移动数据
  2. 02在授权限额内安排周期性账单支付
  3. 03一次处理多个收款对象的账单
  4. 04核对余额和可购买套餐后发起支付

MCP 相关

AbaPay 通过 `/api/mcp` 的 Streamable HTTP JSON-RPC 向 MCP 客户端提供能力查询、余额查询、套餐列表、账单支付、批量支付和计划管理工具。支付沿用与聊天渠道相同的限额授权和安全控制;OAuth 令牌本身不能授权支出,每次 `pay_bill` 仍需 PIN。

配置

远程服务无需本地安装。选择你使用的客户端,复制配置或使用一键安装。

服务地址https://agents.abapays.com/api/mcp
一键复制安装命令
Claude CodeTerminal
Cursor.cursor/mcp.json
VS Code.vscode/mcp.json
Claude Desktopclaude_desktop_config.json
Windsurf~/.codeium/windsurf/mcp_config.json

命令在终端中运行;JSON 粘贴到对应客户端的 MCP 配置后重启。需要授权的服务会在首次连接时提示登录。

工具

11

此工具无需参数。

详细介绍

⚡ AbaPay 协议

AbaPay 是一个去中心化、原生 Web3 的实用缴费平台,构建于 Base(默认链)和 Celo 之上。用户可以使用链上稳定币(USDT、USDC、USAT)支付现实生活中的账单——话费、移动数据、电费、有线电视、银行转账、教育 PIN 码以及国际话费/数据流量;法币即时结算由服务端通过 VTpass API 处理。用户可以直接在 Web 应用中付款,也可以通过 Telegram、WhatsApp 和 X 上的对话式自主 AI Agent(“DeAI”)免手动操作——这是一个符合 ERC-8004 标准的真实链上身份,可在 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 实时目录——提供商信息不再硬编码:**话费充值、流量、电费、有线电视和教育服务的每个提供商名称、徽标和金额限额,都会从 VTpass 实时获取(src/lib/vtpassCatalog.ts,通过 /api/providers 提供给浏览器),而不是来自四个单独的硬编码列表。应用、聊天、MCP 和管理面板都读取同一个进程内缓存,因此数据只有一个可信来源。请参阅下文的实时提供商目录。
  • **按提供商设置并实时执行金额限额:**上限采用 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 成绩查询和 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 备用短信:**自动发送电费令牌/PIN 短信,绕过尼日利亚的免打扰(DND)登记系统,确保关键交易通知送达。
  • **多渠道支持与通知:**内置支持工单系统,并集成 Telegram、WhatsApp 和 X(Twitter)的 webhook,方便用户和管理员通过首选渠道交易/接收通知。
  • **对话式 AI Agent(“DeAI”):**自然语言助手(/api/deai),由 Claude(Anthropic)提供支持,用户可以通过聊天式指令查询余额和支付账单。可通过 Telegram、WhatsApp、X,以及商店内的应用内聊天组件(src/components/AIChat.tsx)访问。它理解意图,而不只是菜单编号——回复“Celo”或“usdt”,效果与回复“1”或“2”完全相同;并且在要求用户选择代币时,会显示每种代币的实时余额和已批准的 Agent 限额,让用户不必盲选。如果会话失去响应(网络中断、中途放弃),系统会自动识别并清理,而不是让会话悬而未决;如果用户输入 PIN 后紧接着发生网络故障,付款也不会悄然丢失或重复扣款——系统会持续追踪,直到链上结果确认后才向用户反馈。
  • **由 Agent 发起的付款(AbaPayV3):**用户可以向 DeAI Agent 授予有上限、链上可撤销的支出额度(setSpendingAllowance),并在 Agent Hub 标签页中按链和稳定币分别选择。这样 Agent 就能代表用户通过 Telegram/WhatsApp/X 支付账单,付款时无需钱包签名,也不会托管用户资金。如果聊天付款所需的链/代币尚未获得批准额度,Agent 会提前检测到,并提供直接选择:现在批准,或通过已签名的深层链接完成这笔付款。请参阅下文的 AbaPayV3 — Agent 额度。
  • 自主定时与自动付款 Agent:除单次聊天付款外,用户还可以让 DeAI Agent 设置定期账单(月/周/日)、一次性未来付款(“10 分钟后支付这笔账单”),或由一条请求同时处理多个收款人/账户——Agent 会按链/代币分组,并通过同一个受额度限制的中继器在计划时间自动结算每一笔,无需用户后续操作。
  • **链上归因:**AbaPay 发起的每笔 Celo 交易(应用付款、授权、Agent 中继器、管理员金库操作)都会附带一个 ERC-8021 后缀,其中包含我们的两个 Celo 标签(src/lib/attribution.ts):用于 Celo Open Rails hackathon 的 celo_2719403c6aff 和用于 Celo Builders 的 celo_9d71588659ec。x402 付款由 facilitator 而非 AbaPay 提交到链上,因此不带标签。Base 交易则使用 Base 自己的 builder 代码。
  • **链上 Agent 身份(ERC-8004):**AbaPay 的 DeAI Agent 通过 ERC-8004“Trustless Agents”注册表,在 Celo 和 Base 两条链上注册为真实的链上身份,因此可在 8004scan.io/AgentScan 上发现——这与它如何转移资金相互独立且无关。请参阅下文的 ERC-8004 Agent 身份。
  • **MCP 服务(AI Agent 付款):**任何使用 MCP 的 AI 客户端(Claude 或任何其他支持 Model Context Protocol 的 Agent)都可以通过 /api/mcp 上的 Streamable HTTP JSON-RPC 将 AbaPay 用作真正的工具服务,调用 describe_capabilities、check_balance、list_plans、pay_bill、多收款人 pay_bill_batch,以及定期/单次 schedule_bill/list_schedules/cancel_schedule。这是 Telegram/WhatsApp/X 之外的第四个渠道,并未增加新的信任边界:它采用与聊天渠道完全相同的、受额度限制、由 kill switch 控制并考虑折扣的执行流程,具体使用 Celo 或 Base,取决于关联钱包批准了什么。请参阅下文的 MCP 服务。
  • **MCP OAuth 2.1(授权一次,而非每次对话都授权):**连接器支持完整的 OAuth 2.1 授权码 + PKCE(S256)流程以及动态客户端注册(/api/oauth/register、/api/oauth/authorize、/api/oauth/token,发现端点位于 /.well-known/)。用户在浏览器中授权一次——在 AbaPay 自行手工呈现的同意页面上证明其 API key 和 PIN——之后的每次对话都会使用 Bearer 令牌重新连接,无需再次输入 API key。**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 付款协议结算——Celo 上的 USDC/USD₮/USAT 由 Celo 自己的 facilitator 处理,Base 上的 USDC 由 Coinbase CDP facilitator 处理——因此交易确实会被 x402scan 索引,而非被重新标记为合约调用。任何不支持 EIP-3009 的付款都会使用链上 payBill 流程,包括 Base 的赞助 Gas 路径。⚠️ x402 需要 EIP-3009 transferWithAuthorization 签名,而这类签名在结构上与 drainer 要求的签名相同,因此某些钱包扫描器会将其标记为有风险——这是为了让 x402scan 能够显示交易而有意接受的已知取舍;设置 NEXT_PUBLIC_X402_ENABLED=false 即可退出。由 Agent 发起的免签名流程不受影响。请参阅下文的 x402 结算。
  • **动态汇率引擎:**提供实时市场汇率换算、管理员可配置的汇率和自动利润差价计算,并在服务器端验证,防止少付款漏洞。
  • **管理层管理面板:**实时监控 VTpass 法币余额、按代币/链划分的链上金库余额、交易分析、手动退款工具和 CSV 导出,并由管理员身份验证保护。
  • 能真正停止所有渠道的 Kill Switch:面板中的“暂停服务”开关采用两级模型——按服务设置主开关(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)。任一层关闭时,付款都会被拒绝。src/lib/serviceRules.ts 中的 killSwitchKeysFor() 会将 Agent 意图(及提供商)映射到对应的键,并通过 resolveServiceId 进行规范化,确保 ELEC_ikeja 不会因与 ELEC_ikeja-electric 不匹配而漏过。因此,聊天、MCP 和自主调度器现在都会遵守与 Web 应用相同的开关。请参阅下文的 Kill switch。
  • **Base 上的赞助 Gas:**Coinbase Smart Wallet/Base Account 用户可以零 Gas 费付款——应用会通过 EIP-5792 检测 paymaster 支持,并将授权和付款合并为一笔赞助交易。不支持此功能的钱包(MetaMask、WalletConnect、Valora 等)会无缝回退到正常的自付费流程。
  • **可分享和下载的收据:**每张收据都可以通过设备原生分享菜单直接作为图片分享到 WhatsApp/Telegram 等渠道,也可以直接保存为 PNG 或 PDF。
  • **可用于 Farcaster Mini App:**内置 Farcaster frame 元数据,因此可直接在 Farcaster 客户端中启动 AbaPay。
  • Nimiq Pay Mini App,以 NIM 付款:nim.abapays.com——已列入 Nimiq Pay 的 Mini Apps(nimiq/awesome#45,于 2026-10-20 合并;首笔真实 NIM 购买于 2026-10-24)——(src/app/nim/,由 middleware.ts 路由)可销售主应用销售的所有商品和服务,但仅接受 NIM 代币付款,并通过 Nimiq Pay(@nimiq/mini-app-sdk)运行。POST /api/pay/nim 会按 NIM 的实时售价计算账单价格:从 CoinGecko 上符合条件的 nimiq-2 交易所市场(美元稳定币交易对、数据新鲜、无异常、成交量 ≥ $500)中取最低最佳买价,且不会高于 CoinGecko 综合价格;综合价格也会作为备用值。然后加上 NIMIQ_PRICE_BUFFER(默认 2%),按管理员设置的 platform_settings.exchange_rate(与 abapays.com 使用相同的奈拉汇率)计算,并向上取整到整数 NIM。系统不会在记录订单前检查付款方余额:Mini App API 没有查询钱包总余额的方法,而且计算出的余额可能遗漏 Nimiq Pay 的某个 HTLC,因此 Nimiq Pay(掌握准确余额)会自行拒绝余额不足的转账;只有付款上链后才会扣款。应用显示的余额(仅在找到余额时显示,否则显示“—”,请参阅 Nimiq Pay)来自链上读取(GET /api/pay/nim/balance?address=),而非 Nimiq Pay 的 getBalance;后者对有余额的钱包返回 0。只有链上查询失败时才会使用宿主提供的数值。Nimiq Pay 会将用户的 NIM 保存在由该用户地址提供资金的 HTLC 中(基本账户余额显示为 0),因此钱包余额等于基本余额加上它作为发送方的所有未过期 HTLC——包括旧 HTLC 超时前 Nimiq Pay 将剩余金额转入的新 HTLC(HTLC → HTLC,因此需要遍历钱包的 HTLC,而不能只查看其自身的转出交易)。如果链上显示某笔此类 HTLC 的资金由下单钱包提供(htlcFunder),则从该 HTLC 收到的付款也会计入该下单钱包。abapays.com 遵循的所有管理控制都通过同一个 checkWebPayment 检查关卡执行(kill switch、提供商熔断器、实时最小/最大金额限制),报价(GET /api/pay/nim)会附带每项服务的可用状态,以便应用将已关闭的服务显示为灰色;POST { check: true } 可检查某个提供商和金额而不写入任何数据,并返回金库地址(NIMIQ_TREASURY_ADDRESS)和订单参考号。Nimiq Pay 会将 NIM 连同该参考号作为付款的纯文本消息发送(Nimiq Pay 将 data 存储为 UTF-8,普通交易最多携带 64 字节——MAX_NIM_MEMO_BYTES;47 字节的参考号不得进行十六进制编码,否则长度会翻倍并超出限制,导致付款在 Nimiq Pay 中一直停留在 Pending)。每笔 Nimiq Pay 付款都从用户的 HTLC 中支出,并由 Nimiq 的 cosigner 联合签名;服务器会从 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 会结算应用关闭前尚未报告付款的订单(通过金库历史记录中的参考号查找),并使一小时后仍未付款的订单过期。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”按钮;点击 Disconnect 会清除此设备上记住的账户。应用的 History 标签页(POST /api/pay/nim/history)会显示付款方自己的订单——包括等待付款(Nimiq Pay 可能会将转账保持为待处理)、已过期 · 未付款(未扣款)以及正在退款的迟到付款,并附上一行说明;内部订单占位符绝不会作为付款哈希返回。仪表读数令牌和 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:**Claude(Anthropic API),用于 DeAI 对话 Agent 和应用内聊天组件
  • **Agent 身份与支付:**ERC-8004(链上 Agent 身份,Celo + Base)和 x402(使用已连接的钱包签名,内部实现于 src/lib/x402Pay.ts),用于主应用中基于 HTTP、由结算服务处理结算的支付
  • MCP、A2A 和 x402 上的 Self Agent ID(src/lib/selfAgent.ts):Agent 可为其请求签名(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 为可选项;校验失败的签名会被拒绝,并返回 401 SELF_AGENT_INVALID;已过期、已撤销或未注册的 Agent 仍可支付,但状态为未经验证。策略和示例:docs/gitbook/self-agent-id.md。
  • Grant 报告(管理员 → Grant):/api/admin/grant-report?from=&to=[&format=csv](src/lib/grantReport.ts)按时间窗口统计外部 Agent 通过 Agent 支付通道完成的 Celo 成功支付——包括 x402 Agent(source_channel AGENT)和通过中继器支付的 MCP/A2A Agent;不包括 Web 应用、AbaPay 自有聊天 Agent、退款/失败交易以及 GRANT_EXCLUDED_WALLETS(加上管理员/中继器/服务器钱包)。报告会根据 Prezenti 目标进行逐 Agent 归因(ERC-8004、OAuth 客户端、Self Agent ID),并提供最终报告所需的逐笔支付 CSV。
  • **付款方信息(仅管理员可见):**每笔 x402、MCP 和 A2A 支付都会在 payment_client_info 中记录请求的 User-Agent,以及请求声明的 Agent 名称 / ERC-8004 ID(X-Agent-Name、X-Agent-Id)。对于 MCP/A2A,还会记录协议、登录方法和 OAuth 客户端注册名称(路由在 runWithAgentClient 下执行每次工具调用,executeAgentPayment 负责记录;自有聊天 Agent 和调度器不记录)。响应后,系统会在链上查询付款钱包(src/lib/payerIdentity.ts):查询其拥有的或被声明为其钱包的 ERC-8004 Agent 身份,并通过 ownerOf / getAgentWallet 逐一确认(ID 来自 Blockscout;若不可用,则从 CELO_RPC_URL 上的注册表日志获取),同时查询其 Self Agent ID 状态(是否已验证、是否有人类证明、证明是否仍有效)。结果在 payer_identities 中缓存一天。这两个表仅服务角色可访问(迁移 044),且仅显示在管理员账本中,不会出现在收据或公共 API 中;查询不会影响支付。支持人员如何读取这些信息:docs/runbooks/README.md,“Supporting a developer whose agent paid”。
  • **Agent 工具访问:**MCP(Model Context Protocol)——位于 /api/mcp 的 Streamable HTTP/JSON-RPC 服务,向任何 MCP 客户端提供余额查询和账单支付工具;以及位于 /api/a2a 的 A2A(Agent2Agent),其卡片位于 /.well-known/agent-card.json,向对等 Agent 提供相同的工具。x402 端点也列在 /.well-known/x402 的 x402 发现清单({ version: 1, resources: [...] })中;agent402.tools 等 x402 索引器会在探测实时 402 挑战前读取该文件。AbaPay 已列入索引([agent402.tools/api/index?seller=agents.abapays.com](https://agent402.tools/api/index?seller=agents.abapays.com))。**Cursor:**已列入 [cursor.directory/plugins/abapay](https://cursor.directory/plugins/abapay);该插件位于 integrations/cursor-plugin/(.cursor-plugin/plugin.json+mcp.json指向https://agents.abapays.com/api/mcp`,并通过仓库根目录中的 .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.)——账户自动检测、名称查询验证,以及从 Moniepoint Microfinance Bank 商业账户扣款的真实 NUBAN 出款
  • **数据库 / 账本:**Supabase(PostgreSQL)——交易、平台设置、积分、退款
  • **电子邮件:**Resend(交易收据邮件)
  • **通知与机器人:**Telegram Bot API、WhatsApp Cloud API、X(Twitter)API、VTpass Messaging API(SMS)

👛 支持的钱包与环境

AbaPay 在三个不同的运行环境中运行,并在加载时于 src/app/page.tsx 检测环境(environment = MINIPAY | FARCASTER | WEB,检测前的状态为 LOADING,并设有 2 秒超时,超时后回退到 WEB)。WEB 情况下的钱包连接由 src/config/wagmi.ts 提供,该文件恰好注册了三个连接器:injected()、baseAccount() 和 walletConnect()。

钱包 / 环境连接方式备注
MiniPay(Opera Mini 内置的 Celo 钱包)直接通过 window.ethereum.isMiniPay 检测;应用自行构建 viem 钱包客户端,并锁定到 CeloGas 费用以稳定币支付(txConfig.feeCurrency),因此用户无需持有 CELO。此处有意禁用网络切换。
Farcaster Mini App通过 @farcaster/miniapp-sdk 的 sdk.context 检测;使用 sdk.wallet.ethProvider,并锁定到 Base通过静默调用 getAddresses() 读取地址,因此打开应用时绝不会强制弹出钱包窗口。Frame 元数据位于 public/.well-known/farcaster.json。旁边有自己的退出按钮(网络徽章不可交互)。
Valora仅支持 WalletConnect —— 在 Valora 的应用内浏览器中,故意跳过注入式连接路径(isValoraBrowser())通过 explorerRecommendedWalletIds 固定显示在 WalletConnect 模态框推荐列表的顶部。仅支持 Celo,应用会自动遵循这一限制(walletApprovedChainIds())。有关禁用注入式连接路径的原因,请参阅下文“Valora 仅支持 WalletConnect”。
MetaMask 和其他注入式浏览器钱包使用钱包所声明的 EIP-6963 发现连接器;若未发现,则回退到通用的 injected() 连接器wagmi 会为每个已安装的钱包发现一个连接器(multiInjectedProviderDiscovery 默认启用)。有关采用此方式而非直接读取 window.ethereum,以避免 Web3 浏览器用户看到二维码的原因,请参阅下文“连接按钮的选择方式”。
Coinbase Smart Wallet / Base AccountbaseAccount() 连接器只有这些钱包可以获得赞助 Gas —— 应用会探测 EIP-5792 付款方能力,并将 approve 和支付合并为一次赞助调用。其他情况都会回退到常规的自行支付流程。
Base App(在 Base App 自己的应用内浏览器中打开网站时,通过 isBaseAppBrowser() 检测)与上方相同的 baseAccount() 连接器,但会像 MiniPay / Farcaster 一样自动连接(见下方允许列表),并且在界面中锁定到 Base —— 网络切换器、页脚网络文字和代币选择器都只显示 Base,无法切换到 Celo这与上方“Coinbase Smart Wallet”一行不同:在普通浏览器中选择该连接器仍可使用两条链;此行仅适用于页面本身运行在 Base App 内的情况。与 Farcaster 一样,网络徽章旁有自己的退出按钮(网络徽章不可交互)。
任何其他 WalletConnect v2 钱包(Trust、Rainbow、Ledger Live 等)使用带二维码模态框的 walletConnect() 连接器代码没有针对特定钱包的处理 —— 只要支持 WalletConnect,并支持 Celo 或 Base,就可以使用。
“Connect” 按钮如何选择

始终优先使用注入式钱包:它不会接触任何第三方主机,因此在会过滤 WalletConnect 中继的网络上也能正常工作。对于浏览器中没有钱包的情况,WalletConnect 是备用选项——例如普通桌面浏览器,或与钱包应用配对的手机浏览器。

可用的钱包通过询问来确定,绝不读取 window.ethereum:probeInjectedConnectors()(src/lib/walletEnv.ts)会获取 wagmi 发现的连接器,取得每个连接器自己的 provider,并向其发送带超时的 eth_accounts 调用——这个调用绝不会弹出提示,因此在每次页面加载时都可安全执行。每个钱包会返回 authorized(已获准访问此网站)、available(真实存在,但尚未获准访问)或 none(不存在,或是从未响应的占位对象)。

  • 任意钱包处于 authorized 状态 → 不会自动执行任何操作。authorized 决定选择器可以在不弹出权限窗口的情况下提供哪些钱包,而不是决定是否连接。请参阅下文“自动连接是一个允许列表”。
  • 选择器会列出所有可用的注入式钱包、Base Account 和 WalletConnect,每项都带有自己的 EIP-6963 标志和状态徽章(已批准此网站的钱包显示 Recent,其他钱包显示 Installed)。取消操作会终止连接尝试,而不会转而显示二维码。对于同时通过 EIP-6963 通告、又挂载在 window.ethereum 上的同一个扩展,会进行去重,因此不会重复显示。
  • Base Account 与注入式钱包一同提供。 该连接器位于 src/config/wagmi.ts 中;probeInjectedConnectors() 会返回 injected 类型的连接器,而 Base Account(其类型为独立的连接器类型)则会单独添加到选项列表中。这在默认链 Base 上尤为重要,因为 Base Account 是提供赞助 Gas 的智能账户体验;verifySignatureAcrossChains 会验证其生成的 ERC-1271 签名。
  • 由于这两项始终存在,选择器总会出现——即使浏览器没有扩展,也能在使用 Base Account 登录和配对手机钱包之间做出真正的选择,而不是直接显示二维码。

🔵 WalletConnect 始终是一个选项,而不只是备用选项。 选项列表会先构建完成,再根据列表长度决定是否显示选择器——因此,即使是常见的“安装了一个扩展”的情况,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)。
  • 每次挂载仅执行一次,因此不会干扰正在进行的连接。

代价是,只有连接之后才会有对端元数据,因此这会影响接下来的操作,而非预先阻止连接。两种检测器都会使用单词边界——误报会断开正常工作的会话(或让真实的内置浏览器钱包无法使用应走的通道),而这是代价更高的错误。相关测试见 tests/walletEnv.test.ts。

在钱包中取消操作,并不总能得到响应

所有取消路径都假定钱包会报告拒绝——EIP-1193 规定它应当如此,注入式钱包也确实会这样做。但 Valora 通过 WalletConnect 不会:关闭其面板不会通过中继发送任何内容,因此没有拒绝可供捕获,也没有错误或事件。请求会一直保持打开状态,而页面则在等待一个已经作出的决定。

withWalletTimeout 会在 90s 时触发——这个时限必须保持为 90s,因为这也是用户阅读提示并批准的时间。因此,处理 15s 后,状态横幅会显示一个 停止等待 控件。它无法中止正在进行的请求(此端无法做到),并且会刻意避免声称支付已取消:如果用户稍后批准,交易仍会完成;否则就可能导致用户重复支付。

自动连接采用允许列表:MiniPay、Base App、Farcaster——仅此而已

在 Web 上,只有点击「连接」按钮才能连接钱包。 用户未主动连接前,不会连接任何钱包,即使其扩展程序几个月前就已授权此网站也一样。

🔵 Providers.tsx 中有意设置了 reconnectOnMount={false}。 wagmi 默认会通过 reconnectOnMount 保留连接器,并在每次页面加载时静默重新建立连接——这发生在 provider 内部,早于 page.tsx 中任何 effect 的运行。将其设为 false,意味着只有显式调用 connect() 才会建立会话。

自动连接本身采用允许列表(AUTO_CONNECT_SURFACES),而不是默认连接任何以前已 authorized 的钱包,再按名称排除例外——允许列表这种设计可以避免日后以另一个钱包的名义重新出现静默连接。

这三种情况在性质上不同,而非程度不同:应用运行在钱包内部,因此只有一个可能的账户;用户打开其中的 AbaPay 时便已选择该账户,而且没有隐藏选择器,因为根本没有其他选项可选。MiniPay 和 Farcaster 通过各自的 SDK 连接,完全不经过 wagmi;Base App 则通过 wagmi 接入,并由 looksLikeBaseApp() 识别——此函数会有意排除 Coinbase 桌面扩展程序,因为它会设置相同的 isCoinbaseWallet 标志,但在普通网页上,它只是普通的注入式钱包。

⚠️ 代价: 刷新页面会结束 Web 会话,用户需要再次点击连接。要求用户主动确认正是设计目的,但对于经常刷新页面的用户来说,这确实会带来不便。

页面未建立的连接,就不算连接

reconnectOnMount={false} 会阻止 wagmi 重新建立连接器,但不会阻止它恢复连接:配置会在 ssr: true 时持久化到 cookieStorage,因此页面加载时,wagmi 会从 Cookie 恢复 connections/current,而 useAccount() 会报告 isConnected 并提供一个地址——尽管尚未设置 provider,也不存在中继套接字。

🔵 从 useAccount() 看,Cookie 恢复出的会话与实时会话完全相同——两者都会报告 isConnected 并提供一个地址。因此,页面加载时会丢弃并非由页面本身建立的连接(userInitiatedConnect 用于区分这两种情况)。Base App 不受影响——它会通过显式调用 connect() 来静默连接。

每个会话只需证明一次钱包归属

🔵 由客户端编写的筛选条件并不构成权限。 历史记录绝不会通过浏览器直接读取,也不会使用带有地址参数的 anon key——类似 .ilike('wallet_address', address) 的客户端筛选条件,可信度取决于客户端本身;而钱包地址是公开信息,任何人都可以传入。

连接后,钱包会对一条措辞清晰的归属证明消息签名(src/lib/walletSession.ts——浏览器端和服务端共用,因为这段字符串若有两份副本,哪怕只差一个字符,所有签名都会失败并显示「invalid signature」)。GET /api/history 从该签名中推导地址,并使用 service-role 客户端进行查询,因此没有任何参数可以指向他人的记录。

  • verifySignatureAcrossChains 已支持 EOA 以及 ERC-1271/6492 智能账户,因此 Base Account 和 Safe 不会因为签名格式无法验证而被拒之门外。
  • 用户拒绝签名时会断开连接——这表示用户拒绝证明该地址归其所有。
  • 发生其他错误时,用户仍保持连接,但未通过归属验证:他们仍可付款,因为付款由付款签名本身授权;只有历史记录会被限制——绝不会因为此端点不需要验证的签名格式而彻底锁定用户。
  • 仅提供只读权限,且有效期为一个会话,而非 5 分钟;因为每次刷新历史记录都弹出钱包签名窗口,会让用户习惯于对看到的任何内容签名。该会话期间,签名是持有者凭证;涉及变更的操作仍使用各自独立且即时生成的签名(verifyWalletOwnership)。
恢复的 WalletConnect 会话并非有效会话

🔵 wagmi 会持久化 WalletConnect 会话(cookieStorage),并在加载时恢复会话,从而生成一个地址——而只要有地址,界面看起来就会像是已连接:余额会正常显示(余额来自公共 RPC,不会与钱包交互),支付按钮会启用,一切看起来都很正常。

但只有在中继套接字已打开时,WalletConnect 请求才能到达手机。套接字失效时,eth_sendTransaction 会被写入已关闭的管道:钱包中不会出现提示,也不会收到任何响应,而且没有错误可供捕获,因为没有任何请求被拒绝——请求只是石沉大海,无法与用户尚未查看钱包的情况区分。

walletConnectSessionLive()(src/lib/walletEnv.ts)会在任何钱包交互前检查中继;如果会话失效,就会用一句话说明情况并断开连接,让 Connect 重新配对,而不是恢复同一个失效会话。如果无法获取套接字内部信息,则会视为有效——若误判为失效,每次支付都会断开正常工作的钱包。注入式钱包会返回 null:它们在进程内运行,不存在可能失效的套接字。

现在每次钱包调用也都有超时,包括切换链的握手和原本没有超时的 Base sendTransaction。在钱包应用中,超时会提示“你的钱包从未收到请求”,并建议重新连接,因为这几乎总是超时的原因。

walletApprovedChainIds() 是相关的防护措施:WalletConnect 钱包会静默丢弃针对其已批准会话之外链的请求,因此,如果已连接的钱包从未批准当前链,应用就会切换到它已批准的链。

默认链是 Base

src/constants/index.ts 中的 DEFAULT_CHAIN 是 BASE,所有面向未来的默认行为都以它为准:新连接的钱包默认使用的链、钱包连接前代币选择器的初始链,以及调用方未指定链时 Agent 链接批准的链。Celo 得到了完整支持,也可以切换——并未移除任何功能,只是不再作为默认起点。

wagmi.ts 中注册的链依次为:Base、Base Sepolia、Celo、Celo Alfajores。wagmi 将 chains[0] 视为默认链,并将其余链作为可选的 WalletConnect 命名空间,因此仅支持 Celo 的钱包也能正常连接(参见上方的 Valora 行)。请注意,应用自身不使用 wagmi 的路径(src/lib/chain.ts、page.tsx)使用 viem 的 celoSepolia 作为 Celo 测试网,而 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、聊天 Agent 和 MCP 工具共用一个 tokensForChain(),这样四处对各条链提供哪些代币的认知始终一致。


📁 项目结构

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,连接受阻时只会没有响应,而不是报错,用户会觉得“连接按钮坏了”。

至少已确认一家运营商存在这种情况——使用 VPN 时连接流程正常,不使用时则会卡住——但我们没有数据表明有多少网络或地区受到影响。面向用户的文案不会提及任何运营商或国家/地区,这是有意为之:如果用户不在运营商 X 的网络上,却告诉他们问题出在运营商 X,只会让他们不再信任这条提示。/network-check 会报告当前用户实际遇到的屏蔽情况。

以下两种方式可以应对:

  • /network-check — 任何用户都可以打开此页面,通过自己的网络连接逐一探测各项依赖,并指出哪些连接失败。连接失败横幅和常见问题解答中都提供了此页面的链接;用户也可以将探测结果作为证据,向实际涉及的运营商或监管机构投诉。
  • NEXT_PUBLIC_WC_RELAY_URL — 将其指向一个部署在未受过滤的自有域名上的 WebSocket 反向代理(例如 wss://relay.abapays.com 转发到 wss://relay.walletconnect.org),这样 WalletConnect 钱包就能在这些网络上正常工作。中继流量采用端到端加密,因此代理只是管道,不是中间人。请注意,Vercel 函数无法代理长连接 WebSocket——请将其部署在 Cloudflare Workers、Fly.io,或运行 nginx 并配置 proxy_pass 和 Upgrade 请求头的 VPS 上。

MiniPay、Base App 和 Farcaster 均不需要这些主机——前两者会直接向页面注入 provider,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 Gas 赞助)
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.

⚠️ 仅配置此环境变量无法涵盖以下两项,两者都需要在外部控制面板中配置:

  • Coinbase Developer Platform: 创建 Paymaster Policy,将你的 NEXT_PUBLIC_ABAPAY_BASE_ADDRESS 合约加入允许列表(最好也指定 payBill/approve 选择器),并为赞助账户充值或设置预算。
  • Alchemy webhook 配置: 确保 Base webhook 已启用 “Token” 活动类别(而不只是 “External”)。在 Gas 赞助模式下,顶层交易的 to 指向 bundler/EntryPoint 合约,而不是直接指向你的 AbaPay 合约——只有 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 Agent(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 就会成为托管方),因此 Agent 会完成除持有密钥之外的所有操作。有两种方式:

  1. 深层链接(V1/V2 合约,或用户未设置额度):Agent 使用 Claude 解析请求,根据真实的 VTpass 验证电表/账户,在聊天中确认详情,然后返回一个有效期为 15 分钟的签名深层链接,打开应用时会预先填入信息。用户点击后,由其自己的钱包签名,付款将通过与 Web 应用相同的验证流程完成。
  2. **委托额度(AbaPayV3,src/lib/deai/relayer.ts):**如果用户已在链上授予 spendingAllowance(见下文的 AbaPayV3:Sol/Agent 发起的付款),中继器会直接调用 payBillFor()——无需深层链接,也无需在付款时签名——付款金额完全受用户设置的额度限制,且用户可随时撤销。广播前会写入一条 preflight_<wallet>_<timestamp> 交易记录(与 Web 应用在签名前由服务器签发 preflight_<uuid> 意图的做法相同),确认后再将其重命名为真实交易哈希。这样,付款就会通过与其他所有渠道完全相同的验证流程处理;过期或放弃的尝试也会自动清理,不会一直悬而未决。如果 RPC 未能及时确认收据(例如广播后网络出现短暂故障,包括用户输入 PIN 后立即发生的故障),Agent 会将其报告为待处理,而不是失败,并且绝不会针对同一意图再次提供付款链接,从而避免付款丢失或重复扣款。如果某笔付款所需的链/代币尚未获批额度,Agent 会在尝试中继前检测到这一点,并提供选择:立即在 Agent Hub 中批准额度,或通过签名深层链接仅完成这笔付款。
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 → 更高);但不会影响你可以向他们发送什么。两者互不相关。

src/lib/scheduler.ts 是受此限制影响的调用方:一笔安排在明天支付的款项,其报告会在创建该付款的聊天停止活跃很久后才发出——按设计,此时已超出 24 小时窗口。这就是为什么该报告始终使用已批准的 WHATSAPP_SCHEDULE_TEMPLATE_NAME 模板,而不是自由文本。

sendWhatsAppMessage() 现在会先发送文本(免费,且在窗口开放时是正确的做法),并且仅在遇到 131047 时通过模板重试。其他任何失败——令牌过期、收件人已屏蔽——都不会重试,因为重复发送只会无谓地损害质量评分。

要使其正常工作,请在 WhatsApp Manager → Templates 中创建模板,类别选择 Utility,并且正文变量必须恰好有一个:

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 发送未签名的请求正文并查看状态码: 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
Agent 中继器(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 使用。它不能耗尽用户的钱包、提高任何人的额度或提取金库资金——这些限制由合约而非后端强制执行。如果密钥泄露,所有者调用 setRelayer(address(0)) 即可立即使其失效。仅为其提供 Gas 资金;它不应持有任何代币余额。

Agent 身份(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 — 这仅用于注册身份,不会触及支付。

注册方式: 身份是按链区分的 — 不存在跨链 Agent 记录,因此每条链都要运行一次,并且两次注册都指向同一个 agent.json URL。

  1. 部署 public/.well-known/agent.json(先将其中的 wallet.address 编辑为你的真实 RELAYER_ADDRESS),确保可通过 https://<your-domain>/.well-known/agent.json 访问。
  2. 将 ERC8004_AGENT_URI 设置为上述 URL。
  3. 先运行 npx hardhat run scripts/register8004.ts --network sepolia — 在 Celo Sepolia Celoscan 确认交易,并检查 Registered 事件中的 URI 和 Agent ID 是否正确。
  4. 只有确认上述步骤成功后,才运行:npx hardhat run scripts/register8004.ts --network celo — 这会消耗真实 Gas,并永久铸造 Celo 身份(AbaPay 的 Celo 线上 Agent ID:9687)。
  5. 另外,运行 npx hardhat run scripts/register8004.ts --network base — 这会铸造 Base 身份(AbaPay 的 Base 线上 Agent ID:59561)。URI 相同,但注册表和链不同,因此 Agent ID 也不同。
  6. 将 NEXT_PUBLIC_ERC8004_AGENT_ID 设置为脚本输出的 Agent ID。可在 8004scan.io 查询任一身份。

两次注册都只会存储 URL,而不会存储卡片内容,因此编辑 agent.json(例如添加新的声明服务)后,URL 返回的内容会随之改变,无需发起新交易。但仅这样还不足以让 8004scan 之类的扫描器注意到变化:索引器似乎会在注册时对卡片进行快照,而不是按计划轮询 URL,因此链上没有任何信号告知它们内容已变更。scripts/update8004uri.ts 可弥补这一缺口 — 它会调用注册表的 setAgentURI(agentId, sameURI),重新发出一条新的 URIUpdated 事件(URI 本身不会改变),仅用于为索引器提供可响应的新事件:

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 服务条目),且你希望已注册的身份被重新读取时,就运行此脚本。

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 授权由用户已连接的钱包签名(src/lib/x402Pay.ts),而不是由第二个钱包 SDK 签名。

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 的支付会使用常规合约调用。此流程不会触及由 Agent 发起的支付,因为 x402 要求每笔支付都提供新的签名。此基础设施与上文的 RELAYER_PRIVATE_KEY 不同。

质询提供哪条链 — Celo 是 Agent 的默认链。 指定链(blockchain 和 token)的请求会在该链上接受质询。未指定链的请求,例如爬虫探测、通用 x402 客户端或 Celo 的 buy 市场 CLI,会先获得 Celo 选项,包含全部三种稳定币(USDC、USD₮、USA₮,各自使用独立的 EIP-712 域);配置了 Base 后,还会提供 Base USDC,且价格相同。重试并完成付款时,会使用支付头部签名所对应的链和代币。AGENT_DEFAULT_CHAIN(src/constants)对所有面向 Agent 的路径(x402、MCP、A2A、Agent 链接、转发器)都设为 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 结算——这是一种允许第三方转移代币的签名。它在结构上与代币盗取者发出的请求相同,因此一些钱包安全扫描器会将其标记出来:在常规账单支付中,Zerion 曾将 AbaPay 自身的请求显示为“恶意请求——批准此请求可能导致全部资产损失。”,而通过合约调用进行的同一笔支付则显示为普通的发送操作,并标注 “未发现风险”。这是 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,使该用户降级到合约调用通道。

结算通过Celo 自有的 x402 促进器(主网为 api.x402.celo.org,测试网为 api.x402.sepolia.celo.org,由 Celo Core Co. 构建)进行,而非 thirdweb。thirdweb 仍仅在客户端使用,为 useFetchWithPayment 提供钱包签名处理(与协议无关——它会从响应正文读取支付挑战,因此可与任何兼容的促进器配合使用,不限于 thirdweb 自有的促进器)。之所以选择它而非 thirdweb 自有的促进器,是因为:通过预付额度按每笔结算固定收取 $0.001,而 thirdweb 会抽取约 0.3%;在主网上结算无需订阅计费方案(thirdweb 要求订阅计费方案,否则所有主网结算都会因 DELEGATION_CHECK_FAILED 而失败);并且真正实现了非托管——签名后的支付授权会直接向金库付款,不会经由促进器自有钱包中转。 如何获取 API 密钥:

  1. 前往 x402.celo.org → Connect wallet(任何钱包都可以——这里只需签署一条免费的免 Gas 消息,不是交易)。
  2. 系统会立即为你发放 API 密钥和免费额度(截至撰写本文时,主网为 500,测试网为 1000)——完整密钥只会显示一次,请立即复制。
  3. 将 CELO_X402_API_KEY 设置为该密钥——主网和测试网端点都使用同一个密钥,但额度分别独立计算。
  4. 在额度用尽前,从同一个控制面板充值额度(存入 USDC,$1 ≈ 1,000 个额度)——额度为 0 时,/settle 会开始返回 402,发生这种情况时应用会发送 Telegram 警报(参见 src/app/api/pay/x402/route.ts)。
  5. 无需注册其他账号——客户端无需 SDK 账号。支付授权由用户已连接的钱包通过应用自己的 viem 钱包客户端签署(src/lib/x402Pay.ts)。
  6. 将此变量添加到 .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.

在 src/lib/chain.ts 的 rpcUrlsFor 中,将付费端点(例如 Chainstack)放在公共端点之前;公共端点仍会作为自动回退选项,以便任一侧发生故障时支付仍可继续。这些变量有意不使用 NEXT_PUBLIC_:URL 中包含服务商的访问密钥,而 Next.js 不会将非公开变量发送到浏览器,因此浏览器仍会使用公共列表,密钥也不会离开服务器。BASE_RPC_URL 也供 hardhat.config.ts 用于部署到 Base。每晚运行的分叉测试使用各自的 CELO_FORK_RPC_URL / BASE_FORK_RPC_URL GitHub 密钥,这些密钥需要具备归档数据访问权限。

定时任务 / 维护
CRON_SECRET=any_long_random_string   # Optional. Protects /api/cleanup and both /api/schedules/run* endpoints.

系统会从 webhook 内部自动且按需清理过期的未完成预检意图(带节流,不会阻塞)——无需 Vercel 定时任务,免费/Hobby 计划也能运行。/api/cleanup 仍可用于手动运行,或交由外部免费调度器(cron-job.org、GitHub Actions)运行;如果你希望在低活跃时段也能保证执行频率,可使用此方式。

计划账单 / 自动付款 Agent——这两个端点确实需要外部定时任务才能运行: 与上述由 webhook 驱动的清理不同,这两个端点不会自行触发。

  • /api/schedules/run — 定期账单(每月/每周/每日)。在 cron-job.org(免费)每天设置一次或两次,调用 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' 的行,因此大多数任务运行时都找不到到期记录,会立即返回。

如果不注册这些定时任务,用户仍可在聊天中创建计划(定期或一次性),但这些计划永远不会执行——它们会一直处于 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 切换器控制所有可选时间范围的面板: 计数器(付款笔数、唯一钱包数、交易量、Agent 原生份额、新钱包与回访钱包数、 平均付款金额、带有黑客马拉松标签的付款笔数)、可交互的每日图表(按支付通道统计的付款笔数和交易量、新钱包与回访钱包、累计交易量)、饼图(按链、支付通道和代币统计的交易量) 以及归因标签计数。「活跃钱包」部分显示截至今天的 DAU、WAU 和 MAU、DAU/MAU 黏性指标,以及 DAU/WAU/MAU 折线图。随后是最新付款,每笔都链接到其 交易。每个面板都带有说明、「计算方式」注释、可将面板保存为 PNG 的相机按钮、可分享的链接(#panel-id)以及标明链和日期的 页脚,因此单个面板的图片也能独立呈现。时间使用尼日利亚时间(WAT),日期按拉各斯日划分(迁移 040)。打开的页面每 5 分钟刷新一次数据。图表使用 Recharts。/api/onchain/export?chain=CELO(或 BASE、ALL)可下载完整 列表的 CSV 文件。agents 首页的核心数字来自同一索引。

  • 索引器:src/lib/onchain/indexer.ts 会通过各链的 RPC(优先使用 CELO_RPC_URL / BASE_RPC_URL,然后使用公共端点)读取金库事件和转入金库的 稳定币转账,并按支付通道对每笔付款进行分类。它涵盖全部五个金库:Celo V1、V3 和 V4,以及 Base V1 和 V4。
  • 存储:onchain_payments 和 onchain_cursors(迁移 038)。onchain_summary() 一次查询即可返回所有历史数据和每日序列;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 写入,并发调用不会造成问题。

🚀 安装与设置

  1. 克隆代码库

    git clone https://github.com/investorphem/abapay.git
    cd abapay
    
  2. 安装依赖

    npm install
    
  3. 设置环境变量 — 将上面的变量复制到 .env.local 中。

  4. 运行开发服务器

    npm run dev
    
  5. 访问应用

智能合约开发(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 部署)不会返回布尔值;直接使用 require(transfer(...)) 会导致这类代币无法使用。
ReentrancyGuardsetTokenSupport 可以将任何代币列入白名单;否则,带钩子的代币可能导致 payBill 被重入。
PausableV1 没有紧急停止开关——部署后发现漏洞时无法叫停。暂停期间退款仍可进行,以便用户拿回资金。
Ownable2Step防止因将所有权转给输错地址而永久锁死合约。
提款时间锁V1 最大的风险: 单个被攻破的所有者密钥就能立即提走整个资金池中的资金。现在必须先排队发起提款,再等待延迟期结束后执行——监控 WithdrawalQueued 事件;如果不是你发起的,就取消提款。V2/V3 中的延迟固定为 24h;V4 中可由所有者调整(见下文)。
退款上限V1 的 refundUser 是不受限制的“向任意地址发送任意金额”路径,可绕过所有提款控制。现在退款金额按代币设置上限(未设置上限时会安全失败)。
余额差额记账发出的金额是实际收到的金额,因此手续费代币不会导致后端超额出售。

上主网前:

  1. 请进行专业审计。 此合约持有客户资金池中的资金;仅靠静态审查远远不够。
  2. 将 ABAPAY_OWNER 设置为多签钱包(Safe),而不是 EOA。 时间锁可以争取发现问题的时间——只有在被盗密钥无法单独取消并重新排队提款时,才能阻止攻击者;如果延迟被设为 0,则完全无法提供保护(见下文 V4)。
  3. 先部署到测试网,并端到端运行完整支付流程。
  4. 对每种代币调用 setMaxRefund——配置上限前,退款交易都会回退。

payBill 仍使用 transferFrom(msg.sender, …),因此付款人必须是签名者。委托支出(DeAI 的“通过社交媒体付款”功能)需要额外的链上授权机制,本次加固有意不包含此项——应将其作为独立变更进行设计和审计。

AbaPayV3.sol——由 Agent 发起付款

contracts/AbaPayV3.sol 在 V2 的基础上解决了上述问题:在 Telegram/WhatsApp 上没有可用于签名的钱包,因此此前 Agent 只能向用户提供深层链接,让用户在应用中签名。V3 则新增了一种会话密钥 / 委托支出模式:

  1. 用户使用自己的钱包,只需完成两项操作:标准 ERC-20 approve(AbaPayV3, X),以及 setSpendingAllowance(token, X)——这是由用户掌控的链上上限。
  2. 此后,经授权的中继者(后端热密钥 RELAYER_PRIVATE_KEY)可以代表用户调用 payBillFor()——但合约本身会在每次调用时检查并扣减剩余额度,因此上限由链上强制执行,而不是由后端执行。
限额强制执行方式
每位用户的总风险敞口spendingAllowance[user][token]——只能由用户设置,也可立即撤销并设为 0
单笔交易上限maxAgentPaymentPerTx[token]——由所有者设置,在用户自行设定的额度之外再增加一道限制
中继者密钥被盗后的影响范围最多只能花费用户剩余的授权额度,且只能通过 payBillFor——无法清空钱包、提高任何人的额度或提取金库资金
紧急停止开关所有者调用 setRelayer(address(0)) 可立即停用 Agent,或调用 pause() 停止所有付款

演示时部署到测试网;在主网上,将 maxAgentPaymentPerTx 和 maxRefundPerTx 初始设为较小值(scripts/deployV4.ts 默认值为每种代币等值 10 美元),然后通过 setMaxAgentPayment / setMaxRefund 调高。当前运行中的 V4 金库为每种受支持的稳定币分别设置了 50 的单笔 Agent 付款上限和 400 的单笔退款上限。预期值位于 config/vaults.json,运行 node scripts/vault-config.mjs 可将其与链上配置进行比较。

payBillFor 会发出与 V1/V2 相同的 PaymentReceived 事件(因此 webhook 无需更改),还会额外发出 AgentPayment 事件,让后端或任何观察者都能区分“用户签名”和“Agent 使用授权额度付款”。

AbaPayV4.sol——可调节的提款延迟

V4 在 V3 的基础上只做了一项更改:提款时间锁不再是硬编码的 24 小时,而是一个变量 withdrawalDelay,所有者可以通过 setWithdrawalDelay(n) 调高、调低或设为 0。新部署的默认值为 24 小时。当前运行的两个金库均设为 0,这是所有者的主动选择,因此从中提款无需等待(请参阅下方警告)。两个主网部署的都是 V4:

链AbaPayV4
Base0xC0A4dAA04DEd9c54D1239507B5A5E645761ef488
Celo0x5df8aE2B963165b735B18Ca86B1ea448d2AA032C

⚠️ 旧版 Celo 合约 0x42Fa4637… 是 V3——它没有 setWithdrawalDelay,因此其 24 小时时间锁是固定的,永远无法设为即时提款。这就是 Celo 重新部署而非重新配置的原因。该合约现已弃用:不再支持任何代币,其中继者地址为 0x0,且合约已暂停。旧版 Celo V1(0x1d125198…)也不再接受任何代币。

提款队列无法移除——它已编译进字节码,且没有直接的 withdraw() 函数。延迟设为 0 时,提款操作是先调用 queueWithdrawal,再立即调用 executeWithdrawal:两笔交易,无需等待。

⚠️ 更改延迟不会追溯生效。 提款的 executableAt 会在排队时确定,因此降低延迟并不会立即释放队列中已有的提款——你必须调用 cancelWithdrawal 取消,然后按新延迟重新排队。取消不会转移资金;代币不会离开金库。

⚠️ 延迟设为 0 会失去时间锁原本提供的保护。 时间锁的作用是防止所有者密钥被盗后,攻击者在他人察觉前清空金库。延迟为 0 时,持有密钥的人可以在同一分钟内排队并执行提款。请仅将此设为紧急配置,并在之后调高延迟。

# --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 Agent 身份

scripts/register8004.ts 会通过 ERC-8004「Trustless Agents」 注册表(主网上的地址为 0x8004A169FB4a3325136EB29fA0ceB6D2e539a432),将 AbaPay 的 DeAI Agent 注册为 Celo 上真实的链上身份,使其可以像任何链上身份一样,在 8004scan.io / AgentScan 上被发现和浏览。注册会铸造一个 ERC-721,其 tokenId 就是该 Agent 的 ID,并指向公开的 Agent 卡片(public/.well-known/agent.json);卡片将实际运营的中继钱包列为 Agent 的链上地址。

这仅涉及身份,不会影响付款。 上文提到的中继钱包免签名 payBillFor 流程完全不受影响;注册与否都不会影响账单的支付方式。在主网上运行前,请对照 Celoscan 上已验证的源码核实 register(string) 选择器——详见脚本的头部注释。

MCP 服务(AI Agent 付款)

src/app/api/mcp/route.ts 实现了一个真正的 MCP(模型上下文协议)服务,通过 Streamable HTTP 提供服务(JSON-RPC 2.0:initialize、tools/list、tools/call),无需额外依赖。Claude 和其他 AI Agent 使用同一开放标准调用工具。它是接入同一执行引擎的第四个渠道;该引擎也为 Telegram/WhatsApp/X 提供支持,并非另设一套规则的平行系统:

正在构建一个能集成 AbaPay、完全无需人工打开 abapays.com 的 Agent? 请参阅 docs/AGENT_INTEGRATION.md,了解 x402 零配置接入方式,以及完全无头(仅需钱包签名)自助获取 MCP API 密钥的方法。使用 TypeScript? sdk/(npm 上的 abapay-sdk)将两者封装为两个函数。使用 Python? python-sdk/ 与其逐项对应,也发布在 PyPI 上—— pip install abapay-sdk,通过 Trusted Publisher 发布(无需令牌密钥),由 py-sdk-v* 标签触发;实际验证由 CI 完成(而非本地运行——编写时的环境中没有 Python 解释器)。相比单篇文档,你更想要一整本指南? docs/ gitbook/ 以 12 个章节涵盖相同内容(SUMMARY.md 是目录,采用 Markdown 格式,可直接同步到 GitBook 或任何能直接读取代码仓库的文档托管平台)。想要一个可直接上手的 Agent? abapay-reference-agent (MIT)使用自己的钱包,通过 x402 在 Celo 上支付账单,并支持支出上限、审批提示词、通过 Claude 发起可选的自然语言请求,以及可选的 Self Agent ID。快速入门不到 10 分钟。 | 工具 | 功能 | 需要 | |---|---|---| | describe_capabilities | 以易读方式列出 AbaPay 可支付的项目及当前暂停的项目 | 无 — 公开 | | list_plans | DATA / CABLE / EDUCATION 当前实际可购买的套餐,包含准确的 variation_codes 和 VTpass 实时报价 | 无 — 公开 | | list_international_options | 逐级浏览国际服务实时目录(140 多个国家):国家 → 产品类型 → 运营商 → 定价套餐 | 无 — 公开 | | check_balance | 按代币读取关联钱包的实时余额和已批准的 Agent 限额(指定链) | OAuth Bearer token 或 api_key | | transaction_history | 列出关联钱包的近期真实交易 — 与应用「历史记录」标签页中的数据相同 | OAuth Bearer token 或 api_key | | get_payment_status | 通过交易哈希或请求 ID 查询一笔支付:已到账、仍在确认、失败或已退款,并显示退款状态。范围限定为关联钱包 | OAuth Bearer token 或 api_key | | pay_bill | 在链上端到端支付真实账单(话费、流量、电费、有线电视、教育 PIN 或国际话费/流量) | (OAuth Bearer token 或 api_key)+ pin,始终需要 | | pay_bill_batch | 一次调用为 2-20 位收款人支付话费/流量,整个批次使用一个 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 取消一个计划,或通过 provider / all: true 取消多个计划(这些操作需要 PIN);未提供以上任何参数的调用会被拒绝 | OAuth Bearer token 或 api_key(批量操作需要 PIN) |

提供 list_plans 是为了让 Agent 无须自行编造 variation_code。 它的描述和服务端级别的 instructions 都会告知客户端:对于这三项服务,在调用 pay_bill 前先调用它,并原样传回返回的代码 — 绝不能猜测套餐、代码或价格。如果它没有返回可用结果(确实会发生 — 此商户账户未启用 JAMB),正确做法是如实说明,而不是伪造代码。

pay_bill 支持 EDUCATION,也支持话费/流量/电费/有线电视,因为 MCP 的信任边界应与聊天相同,而不是更窄。所需规则都是共享的,且早已存在:requiresVariation() 强制要求 variation_code,checkAccountNumber() 要求 JAMB 的个人资料 ID 至少有 10 个字符,而 requiresVerifiedName() 则确定只有 JAMB 需要商户验证(WAEC 没有账户可供验证)。它还会预先根据 VTpass 实时目录验证服务商,因此 Agent 不再能传入 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/)的链接。链接使用 request_id 而非 tx_hash:任何关注链上金库地址的人都能看到交易哈希,而收据页按设计是公开且可分享的,因此绝不能让区块链观察者将某笔付款与客户经验证的姓名/地址关联起来,所以页面也不会显示购买的代码/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)则是真正的 HTML/CSS/JS,由宿主在沙箱 iframe 中渲染,并通过 ui/notifications/tool-result 接收工具的 structuredContent——支持真实的 ₦/₮ 字形、适配主题(读取宿主的 CSS 变量以适应浅色/深色主题),并通过 ui/open-link 接入“查看收据”链接。这六个工具通过 _meta.ui.resourceUri 声明卡片资源,并由新增的两个 /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 的卡片(新的 capabilities 视图,通过 src/lib/deai/capabilities.ts 中的 getCapabilitiesForCard() 提供)会列出 AbaPay 的每项服务、对应的示例说法、服务是否可直接通过 MCP 使用还是仅限应用使用,以及从聊天界面读取的同一熔断开关状态中获取的实时“已暂停/可用”标记。这是无需在第三方渲染的 iframe 中放入 PIN 字段、又能安全展示的最完整菜单(关于为何卡片中不提供带 PIN 的表单,请参阅下方的信任边界说明)。 卡片不仅显示工具,还会回调工具。 transaction_history 的卡片通过 Prev/Next 翻页查看历史记录(使用偏移后的 offset 再次调用工具——transaction_history 现在接受该参数),check_balance 的卡片有 Refresh 按钮,list_schedules 的卡片则为每个日程安排提供 Cancel 按钮——这些都采用规范中的“交互式更新”模式(视图通过宿主再次发送 tools/call,并直接收到正常结果,而不是通过通知)。我们刻意没有将此模式扩展到 pay_bill/pay_bill_batch/schedule_bill:这些操作需要 PIN,而在沙盒化的第三方 iframe 中输入消费 PIN,比直接在聊天中输入的信任边界更薄弱;本文其余内容所要保护的正是后者。取消单个日程安排的 id 不需要 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 会返回链上每种稳定币的余额和已批准限额(而不只是默认币种),因此 Agent 可以预先判断是否存在可行的替代方案。如果使用默认值调用 pay_bill 时余额或链上授权额度不足,错误信息本身会检查同一条链上的其他币种是否已经同时满足这两项条件,并直接指出币种名称——例如 “USD₮ 余额不足,但 USDC 的余额和已批准限额均已充足——请使用 token: 'USDC' 重试”——而不是只点名失败币种,给出无从下手的提示。 pay_bill_batch — 一次调用即可为多个收款方付款,与聊天中的批量付款一致。 聊天意图引擎已能将“向 X 转账 500,再向 Y 转账 1000”解析为多个收款方,这是从 intentEngine.ts 的规则 14 开始实现的;MCP 的 pay_bill 则始终只能处理一个收款方。pay_bill_batch 复用了聊天自身批量处理程序调用的完全相同的基础函数(groupByChainToken、checkAutonomousCapacity、executeAgentPayment,均位于 src/lib/deai/batch.ts):收款方会按 (chain, token) 分组,并针对每组各自的小计检查额度;但只要任意一组额度不足,就会在资金发生任何变动前拒绝整个批次。额度检查通过后,会逐个向收款方付款(绝不并行,以避免中继器 nonce 冲突),并在响应中分别报告每笔付款——即使中途某笔出账失败,也绝不会谎报整个批次都失败。与聊天相比,这项能力不只是持平,而是更强:聊天的 ParsedRecipient 结构没有每个收款方的 variation_code 字段,因此由聊天驱动的 DATA 批次无法为每位收款方指定套餐;pay_bill_batch 的结构化架构支持这一字段,所以每位 DATA 收款方都必须提供该字段,弥补了这一缺失。仅支持 AIRTIME 和 DATA,每次调用最多 20 位收款方。

schedule_bill/list_schedules/cancel_schedule 让 MCP 的自动化能力与聊天一致。 Telegram/WhatsApp/X 支持周期性和一次性的定时缴费(使用 scheduled_bills 表,由 src/lib/scheduler.ts 的 cron 运行);MCP 也具备相同能力,因此 Agent 可以立即缴费,也可以安排稍后缴费。schedule_bill 一次调用即可收集聊天通过多轮对话获取的信息,并复用 pay_bill 完全相同的校验逻辑(requiresVariation、checkAccountNumber、checkAmountLive、实时目录供应商检查)和 PIN 验证,因为定时缴费属于持续性支出。它本身不会扣费:如果钱包已获批的链上额度足以支付该金额,计划就会在每次到期时自动付款;否则,计划会以仅通知模式保存,到期时必须由其他方式调用 pay_bill。EDUCATION 和 INTERNATIONAL 不支持定时,与聊天一致。MCP 没有持久化渠道可在之后主动推送“您的账单刚刚已缴”消息,因此 schedule_bill 接受可选的 customer_email 来实现该通知——如果不提供,请自行检查 list_schedules/transaction_history,了解发生了什么。

存在的原因: 8004scan.io 等第三方 Agent 扫描器只会对已声明的 a2a 或 mcp 服务执行健康检查——参见 public/.well-known/agent.json 的 services 数组,其中声明了这两项服务。该声明背后的 MCP 服务是真实且可用的,并非为了应付扫描器而添加的空壳。

信任模型与聊天相同,而非另起一套。 pay_bill 不会重新实现任何安全逻辑,而是直接调用现有的函数,这些函数已经为 Telegram/WhatsApp/X 和多收款方批量流程提供支持:checkPinAllowed/verifyPin/recordPinFailure(相同的逐步升级锁定机制)、checkServiceAllowed(紧急停用开关)、checkAccountNumber/checkAmount(校验逻辑保持一致)、checkAgentSpendAllowed(运营方单笔/每日限额)、checkAutonomousCapacity + executeAgentPayment(链上额度检查、共享折扣引擎和出账),以及 notifySpendOutOfBand(资金一发生变动,就会通过电子邮件和其他所有已关联渠道通知用户,因此泄露的 API 密钥会像被盗用的聊天会话一样被及时发现)。

速率限制是纵深防御措施,叠加在上述机制之上,而非取代它们——对每个 MCP 工具调用实施统一的 IP 限制,并专门对 pay_bill、schedule_bill 和 pay_bill_batch 设置更严格的身份级限制。具体数值和机制请参阅 Security Architecture 中的速率限制。 与链无关——使用 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 密钥。服务器现在实现了完整的 OAuth 2.1 授权码流程,采用 PKCE(仅限 S256) 和 动态客户端注册:

端点用途
/.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/… 路径提供,也通过附加 /api/mcp 的 RFC 路径插入变体提供,因为不同客户端探测的路径各不相同。

关于此实现,有几点值得了解:

  • 同意页面会在签发授权码之前,验证 API 密钥和 PIN 两者。 仅窃取 API 密钥不足以授权连接器。
  • OAuth 绝不会授权支出。 每次调用 pay_bill 仍然都需要 PIN,和 Telegram/WhatsApp 完全一样。仅凭 Bearer 令牌只能读取余额,不能执行其他操作。
  • 只有一种情况会返回真正的 HTTP 401(附带 WWW-Authenticate 和 resource_metadata):完全未提供凭据。这是 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 并列。与这些渠道不同,MCP 没有可以用来“认领”关联码的机器人——在应用的 Agent Hub 标签页中选择 MCP (AI Agents) 磁贴,设置 PIN,即可生成一枚 256 位 API 密钥,并且只显示一次(系统只会存储其 SHA-256 哈希)。一个钱包可以同时持有多个 MCP 密钥——每个 Agent/工具各一个,每个密钥都有自己的标签。 连接 AI Agent(最终用户):

  1. 在 AbaPay 应用中,为你希望 Agent 使用的链和代币批准链上支出限额(Agent Hub → 第 1 步)——这是实际上限;以下任何设置都不能超出此限额。
  2. 仍在 Agent Hub 中,选择 MCP (AI Agents),可选择为密钥添加标签(例如“Claude”),设置 PIN,然后保存显示的 API 密钥——它不会再次显示。
  3. 将 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 步。
  4. 授权。 如果你的客户端支持 OAuth(claude.ai 和 Claude Desktop 均支持),它会提供一个 Connect 按钮——点击该按钮,AbaPay 自己的授权页面就会在浏览器中要求你输入 API 密钥和 PIN,一次即可。之后每次对话都会自动重新连接。如果你的客户端不支持 OAuth,则将 API 密钥作为 api_key 工具参数传入——它是工具参数,而非 HTTP 标头,因此无需单独进行应用级身份验证。
  5. 无论采用哪种方式,每次付款时都会要求输入 PIN。 授权连接器并不等于授权支出。如果 Agent 声称可以不输入 PIN 就付款,那一定有问题。

每个工具都声明了 annotations(title、readOnlyHint/destructiveHint、idempotentHint、openWorldHint)——pay_bill 被正确标记为破坏性/非幂等(它会转移真实资金,调用两次就会支付两次),而 describe_capabilities/check_balance 则为只读——因此客户端可以在允许 Agent 调用工具前,适当地向用户发出警告。

要收录到 claude.ai 的 Connectors Directory 中(这样用户就能通过浏览/搜索找到 AbaPay,而不必粘贴 URL),需要执行此处所构建内容之外的单独步骤——这是通过 Anthropic 提交的组织申请,而不是代码更改:

  • 需要 Team 或 Enterprise claude.ai 组织(在 admin settings → Directory → New submission 中提交);只有所有者(或 Enterprise 中受委派的角色)可以提交。
  • 根据 Anthropic 自己的提交文档确认的要求:工具 title + readOnlyHint/destructiveHint 注释(✅ 已完成,见上文)、公开文档 URL(https://abapays.com/docs — 已上线)、隐私政策 URL(https://abapays.com/privacy — 已上线)、图标,以及审核人员的测试账户凭据。
  • OAuth 2.0 已就绪。 该目录要求已验证连接器使用 OAuth,AbaPay 已支持此功能(见上文 OAuth 部分):授权码 + PKCE、动态客户端注册、发现元数据,以及真实的 401 响应,以便客户端发现服务。要收录到目录中,剩下的是组织申请本身(Team/Enterprise 组织、图标和审核人员的测试账户凭据)——而不是代码。
  • 在提交/获批之前,使用上述 URL 通过“Add custom connector”即可正常使用,不受限制——目录只会提高可发现性,不会增加功能。

A2A 服务(Agent2Agent)

相同的工具可供其他 Agent通过 A2A 调用,而不是通过 MCP。 只需两个文件,不会新增任何能力:

接口路径
Agent Card(发现)/.well-known/agent-card.json — src/app/.well-known/agent-card.json/route.ts
JSON-RPC 端点/api/a2a — src/app/api/a2a/route.ts

一份实现,两种协议。 工具定义及其实现已从 src/app/api/mcp/route.ts 移至 src/lib/deai/mcpTools.ts,两个路由都会导入该文件。/api/mcp 和 /api/a2a 现在都是基于同一个 callTool() 的传输适配层。这是有意为之:A2A 没有通往资金的私有路径——PIN 门禁、逐步加重的锁定、链上授权额度上限、紧急停止开关和运营方支出上限都在两个路由之下,因此新增协议只会改变 Agent 发出请求的方式,不会改变它能做什么。(这次迁移只是机械调整;工具逻辑逐字节保持不变。)

⚠️ /.well-known/agent-card.json 不是 /.well-known/agent.json。 后者是 AbaPay 的 ERC-8004 注册卡——8004scan 和 Aigora 读取的链上身份。这两份规范过去都使用 agent.json 这个文件名,正因如此,A2A 才将其卡片改为 agent-card.json。它们是不同的文档,面向不同的使用方;用其中一个覆盖另一个,会在毫无提示的情况下破坏 Agent 的链上列表信息。

调用路径中不含大模型。 聊天渠道会通过 parseIntent() 处理自由文本,因为这是人类输入的内容。A2A 是机器对机器通信,因此调用使用携带 { skill, args } 的结构化 DataPart,并根据 MCP 发布的同一份 TOOLS 架构进行验证。收到文本部分时会返回技能目录,而不是猜测意图——在 Agent 对 Agent 的支付路径中,用语言模型重新解释“发送 5000”只会引入没有任何好处的故障模式。

按设计采用同步方式。 每项技能都会在请求内完成,因此 message/send 会返回最终的 Message(符合规范),不会创建 Task。因此,卡片声明 streaming: false 和 pushNotifications: false,而 tasks/* 和 message/stream 会返回 -32004 UnsupportedOperation,而不是只实现一半——没有可供查询的任务存储;声明并不具备的能力,会让对等方一直等待永远不会到来的更新。

身份验证。 Authorization: Bearer … 可通过前缀识别 MCP 接受的任一种凭据: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₮ 付款时,Web 应用的付款流程会通过 x402 结算——使用的是 Celo 自有的协调器(api.x402.celo.org,由 Celo Core Co. 构建,参见 src/app/api/pay/x402/route.ts),而非 thirdweb。每种代币都通过各自的 EIP-712 域结算(该路由中的 X402_TOKEN_EIP712),因为 Circle 的 USDC 部署与 Tether 的 USD₮/USA₮ 部署并不共用一个域。同一个“确认并付款”按钮会在 Celo 上为这三种代币通过 x402 路由处理,其他情况则通过常规的 payBill 合约调用处理(不支持 EIP-3009 的代币、NEXT_PUBLIC_BASE_X402_ENABLED=false 时的 Base,或未配置 x402 时)。因此,这笔付款会真实显示在 x402scan 上,而非只是重新标记的交易,因为 x402 结算要求付款方针对这笔特定付款提供 EIP-3009(transferWithAuthorization)签名。这项要求也正是扫描器提出异议的原因。

402 挑战本身由项目内部构建(纯 x402 v1、基于正文的响应),而不是依赖任何 SDK 的默认实现——这是有意为之,因为 thirdweb 自带的 settlePayment() 总是通过 base64 编码的标头和空 JSON 正文发送新的挑战,通用 x402 扫描器(包括 x402scan 的发现爬虫)无法解析这种格式,因此注册会悄然失败:状态码看起来是正确的 402,但没有可用的挑战。

在客户端,src/lib/x402Pay.ts 使用与所有合约调用相同的 viem 钱包客户端读取挑战、签名并重试——整个流程只需连接一次钱包,无需在 wagmi 之外再搭建一套系统。在项目内部处理签名也让页面能够读取服务器的实际响应:如果结算失败响应中带有 tx_hash,就表示资金已经转移,应用绝不能再通过合约调用通道“重试”。

基于正文的(v1)挑战还包含一个 hint 字段——它不属于 x402 规范,是额外添加的;只读取已知字段的客户端会忽略它。能够签署 EIP-3009 授权,与拥有能够驱动挑战/签名/重试流程的客户端,是两种不同的能力;拥有前一种能力但不具备后一种能力的钱包,只会收到一个无处可去的裸 402 响应。hint 会将调用方引导到常规引导式流程——abapays.com,该流程完全不需要 x402。

放弃该通道之前,先重试被拒绝的结算

🔵 为什么 x402 结算遭拒后要实际重试,而不是立即回退。 transferWithAuthorization 模拟可能会回退(协调器返回 unable to estimate gas / invalid_payload),且不会产生交易,也不会转移资金。在这种情况下回退到合约调用,会为一笔 x402 通道通常可在第二次尝试时以一次提示完成结算的账单,再增加两次提示(批准 + payBill)——因此,重试会自动在三个层面执行:

  1. 在调用 facilitator 之前。 src/lib/x402Settle.ts 会判断 EIP-3009 中可从载荷中看出的回退条件——有效期已过、validAfter 因时钟偏差而无效、收款方错误,以及金额与服务端即将声明的应付金额不一致——因此,对于无法完成结算的载荷,会返回新的挑战,而不是含糊不清的回退。paymentRequirements 声明的金额,以及数据库记录和任何退款记录中使用的金额,都是签名中的 value,而非重新计算出的价格:两者可能因汇率变动而相差一个最小单位;facilitator 收到不匹配的一对值时会拒绝处理。
  2. 服务端只处理一次,使用同一授权。 这是由设计保证安全的——EIP-3009 nonce 只能使用一次,因此无论提交多少次,代币最多只会接受一次。重试可能导致请求重复,但绝不会导致重复转账。
  3. 客户端只重试一次,并使用新签名——仅当服务端将拒绝标记为 retryable,且拒绝中不带 tx_hash 时才会重试。系统会提示用户即将再次签名,因为没有说明的第二次提示,会让人觉得应用忽略了第一次操作。

🔵 只有确认区块链表明资金未转移后,才会提供重试。 这是关键所在。unable to estimate gas / invalid_payload 天生就有歧义:由于 EIP-3009 nonce 只能使用一次,对已成功的授权重新模拟必然会回退;而估算期间发生回退时,无论交易尚未执行,还是交易早已成功,返回的结果都相同。仅靠匹配错误消息无法区分这两种情况——如果判断错误还提供重试,就等于让付款人重新签署一个新 nonce,为同一笔账单支付第二次。因此,代码根本不依赖错误消息。每个 EIP-3009 代币都提供 authorizationState(authorizer, nonce) -> bool,这是链上对该授权是否已被使用的记录;在提供任何重试前,代码都会查询它(结算路由中的 buildAuthorizationStateCall 和 authorizationWasConsumed)。如果授权已使用,支付会被报告为已结算但没有交易哈希——客户端会停止操作,抑制合约调用回退,并向运营人员发送告警,其中包含付款人和 nonce,以便人工对账。无法读取查询结果时,也会按授权已使用处理:不必要的回退会多出两次提示;错误地提供重试则会让付款人蒙受实际损失。

错误措辞仍会作为一种低成本的初筛——FiatTokenV2: authorization is used or canceled 会直接拒绝;而 nonce too low 和 replacement transaction underpriced(指 facilitator 自己的 EOA 发生竞速,涉及的是完全不同的 nonce)仍可重试——但是否真的授权重试,取决于链,而不是初筛。

付款采用哪种通道,也取决于钱包,而不只是代币。 x402 通过 eth_signTypedData_v4 签名进行结算,而签名必须返回页面。如果 WalletConnect 会话从未协商过该方法,请求就会被直接丢弃——没有提示、没有错误,也没有任何返回。这里只检查这一点,且直接检查会话本身:参见 src/lib/walletEnv.ts 中的 walletCanSignTypedData。

🔵 绝不根据钱包名称决定路由。 每个钱包都会被询问是否能签署类型化数据,由钱包自己的回答决定,而不是根据对特定钱包行为的硬编码假设(例如,Valora 会将类型化数据请求显示为“Verify wallet”,并且可能报告连接成功,却始终不返回签名)。如果始终收不到答复,签名请求就会超时;此时尚未发送任何内容进行结算(src/lib/x402Pay.ts 会在等待签名完成后的下一行才发送结算请求,因此取消等待后,即使签名迟到,也不会发送给任何地方),页面便会自行回退到合约调用。 这个签名要求正是此功能仅限主应用的原因:上文由 Agent 发起的流程依赖于在付款时完全不需要签名(这正是 setSpendingAllowance 的意义),这与 x402 的逐笔付款签名模式根本不兼容。Telegram/WhatsApp/X 和自主调度器从不使用 x402,因此不受影响——这些付款本来就由 RELAYER_ADDRESS 执行,该地址也是下方 ERC-8004 身份注册的钱包,因此无需 x402,这些付款也已可归因于该 Agent。

  • 范围:Celo + USDC/USD₮/USA₮,已确认可用——这不是谨慎起见的限制。原生 Celo USDC(Circle 的 FiatTokenV2)和原生 Celo USD₮(Tether 的部署)都实现了 EIP-3009 transferWithAuthorization;USDm 则不支持(Mento 代币仅提供 EIP-2612 permit()),因此没有可用于结算的签名方案——这正是它被 USA₮ 替换并移出选择器的原因。这不是自行设定的限制——如果之后添加对其他代币的支持,除了添加其 EIP-712 域之外,无需更改代码,因为代币和小数位数已通过 resolveTokenOnChain 通用解析。
  • 预付额度,而非计费订阅。 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)可一目了然地区分 CONTRACT 和 X402,并与现有的 source_channel 字段并列(两者维度不同——UI 渠道与结算通道)。

🔌 服务商数据与运营方控制

实时服务商目录(数据来源:VTpass)

src/lib/vtpassCatalog.ts 是应用中所有服务商列表的唯一事实来源。 它会调用 VTpass 的 /services?identifier=…,并返回服务商真实的 serviceID、VTpass 自己的产品名称、VTpass 自己的徽标 URL,以及其公布的 minimium_amount(原文如此) 和 maximum_amount。

支持五个类别,使用 VTpass 实际接受的标识符作为键(其中两个必须经过探索才能发现——显而易见的猜测会返回 011 "Category Does not Exist"):

应用概念VTpass 标识符
话费充值airtime(过滤掉 foreign-airtime——国际业务属于独立流程)
数据 / 互联网data——不是 mobile-data/internet
电费electricity-bill
有线电视tv-subscription——不是 cable-tv
教育education

缓存与故障处理行为(这是重点):

  • 模块级 Map 缓存,TTL 为 1 小时,在进程内共享。浏览器通过 GET /api/providers?category=… 访问它(每个 IP 限流 60 次/分钟,Cache-Control: public, max-age=300, s-maxage=3600, stale-while-revalidate=86400);聊天和 MCP 则直接调用 getCatalog()。两者使用同一个缓存,因此只有一个事实来源。
  • 缓存绝不会在故障时被清除——只会在成功时被覆盖。 回退链为 新鲜缓存 → 实时获取 → 过期缓存 → 内置种子数据,getCatalog() 绝不会抛出错误,也绝不会返回空列表,因此可以始终渲染选择器,VTpass 短暂故障也不会在购买过程中导致列表变空。
  • 返回过期数据时会附带 stale: true,此时 /api/providers 会使用 no-store 提供数据,避免短暂故障导致数据在边缘缓存中冻结一小时。
  • src/lib/providerFallback.ts 是最后的离线种子数据——目前仅剩的硬编码服务商数据。其徽标特意使用本地文件,因为唯一处理“VTpass 无法访问”的代码路径,不应该渲染出一堆失效的远程图片。

为何要替换硬编码列表: 旧列表列出了 showmax、spectranet 和 jamb——这三者在此商户账户下都会返回 Service is Not Valid,因此用户可能选择其中一项、填写表单、通过链上支付,之后才发现售卖失败并进入退款流程。旧列表还遗漏了 glo-sme-data 和 9mobile-sme-data,它们实际可用,却无法访问。

金额限制来自相同的记录:limitsFor(category, serviceID) 和 limitsForIntent(intent, provider) 会返回实时的服务商级别 {min, max};若 VTpass 未公布限制,则返回 null——这样,“未知”会回退到调用方的服务级别默认值,而不会被误认为“无限制”。

熔断开关(两级:主开关 + 各服务提供商开关)

platform_settings.kill_switches 保存由管理后台写入的两级键值系统:

级别键
各服务主开关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) 会将 Agent 意图映射到对应的键,并先通过 resolveServiceId 规范化服务提供商,因此聊天中的宽泛名称 "ikeja" 会解析为运营人员实际切换的 ELEC_ikeja-electric 键。随后,聊天、MCP、A2A 和调度程序都会经过 checkServiceAllowed() 这一关卡。Web 支付入口(/api/pay 和 /api/pay/x402)会通过 checkWebPayment(),它使用相同的开关以及 VTpass 实时的各服务提供商金额限额,确保在签名或结算任何内容之前完成检查。过去这些入口依赖网页禁用付款按钮,因此直接调用 API 或 x402 的调用方仍可能为已关闭的服务付款。国际服务遵循 Web 应用自身的规则:除非 MASTER_INTERNATIONAL 存在且不为 false,否则视为关闭。设置会缓存 30 秒,因此切换开关后,最多半分钟即可在所有地方生效。

自动余额熔断器。 当 VTpass 返回 018(我们的余额已耗尽)或 Monnify 返回 D04 时,platform_settings.provider_circuits 中的熔断器会打开(src/lib/circuitBreaker.ts、迁移 028)。熔断器打开期间,该服务提供商所支持的所有服务都会在所有支付入口上、客户付款之前遭到拒绝;此前每位付款者都会先被扣款,再收到退款。服务提供商的余额恢复到提醒阈值以上后,熔断器会自动关闭。定时余额检查会发现这一情况;客户下次触发熔断器时,也会执行经过限流的探测。运营人员还可以通过管理操作 RESET_PROVIDER_CIRCUIT 手动关闭熔断器。熔断器与 kill_switches 分开存储,不会更改其中的开关,因此运营人员设置的开关始终优先生效。设置 CIRCUIT_BREAKER_ENABLED=false 可关闭此功能。

⚠️ 此映射为何重要: killSwitchKeysFor() 使得在管理后台“暂停电力”后,聊天、MCP 和自主调度程序也会真正停止相关服务,而不只是网站停止服务——这四个入口都会将同一意图解析为相同的 MASTER_/各服务提供商键,因此在任何一处切换的开关都会在所有入口生效。

除了各服务开关外,checkAgentSpendAllowed() 还会执行运营人员针对 Agent 本身设置的控制项:agent_enabled(关闭所有 Agent 付款的主开关)、agent_autonomous_enabled(仅关闭无人值守/计划执行)、agent_max_ngn_per_tx、agent_daily_cap_ngn(按用户和 UTC 日计算)以及 ai_chat_enabled(控制应用内小组件)。这些控制项是在链上额度之上附加的限制,绝不会取代链上额度。

银行转账设有独立的管理后台开关(BANK 键——没有按服务提供商细分,因为它通过 Monnify/Moniepoint 结算,而非从 VTpass 服务提供商列表中选择),Agent 关卡(serviceRules.ts 中的 BANK_TRANSFER)和 Web 应用中的 page.tsx(isCurrentServiceDisabled)都会检查此开关。

另有四个独立开关用于暂停整个渠道,而非某项产品——CHANNEL_WHATSAPP、CHANNEL_TELEGRAM、CHANNEL_X、CHANNEL_MCP——由 serviceRules.ts 中的 isChannelEnabled() 执行。WhatsApp/Telegram/X 会在共享的 /api/deai/core 引擎入口处检查一次(三者的路由都经过该入口);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 — 账单支付和自动支付 Agent 的调度。
  • 004_agent_links.sql — 将钱包关联到 Telegram/WhatsApp/X 身份,以便 DeAI Agent 识别用户。安全边界是 AbaPayV3 上链的 spendingAllowance,而不是此表——此表仅用于镜像展示,改善用户体验。
  • 005_autonomous_schedules.sql — 升级计划账单以支持真正的无人值守执行;由于 AbaPayV3 的链上额度限制了最坏情况下的损失,因此此功能安全可靠。
  • 006_agent_admin_controls.sql — 为 Agent 提供运营方紧急开关(agent_enabled、agent_autonomous_enabled、ai_chat_enabled,以及每笔交易和每日 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 结算)。
  • 011_one_off_schedules.sql — 为 scheduled_bills 添加 run_once_at 和 batch_id,因此,单次聊天请求即可创建一次性未来付款(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 Agent 成为可正式关联的渠道。
  • 020_mcp_oauth.sql — OAuth 2.1 表:动态注册的客户端、一次性授权码(含其 PKCE challenge),以及访问令牌/刷新令牌记录(经过哈希处理,并支持轮换与撤销)。

📱 使用 MiniPay 测试

AbaPay 针对移动 Web3 体验进行了高度优化。要在 Celo MiniPay 环境中测试此 dApp:

  1. 部署项目(例如部署到 Vercel)。
  2. 将 NEXT_PUBLIC_NETWORK 设置为 celo-sepolia(测试网)或 celo(主网),并相应地将 NEXT_PUBLIC_APP_MODE 设置为 sandbox 或 production。
  3. 在 Android 上打开 Opera Mini 浏览器,进入 MiniPay 选项卡,然后输入已部署的 URL。
作为 Farcaster Mini App 进行测试

应用内置 Farcaster frame 元数据(public/.well-known/farcaster.json 和 layout.tsx 中的 frame 配置)。将应用部署到公开 URL,然后在支持 Mini Apps 的 Farcaster 客户端中分享链接,即可直接启动应用。


🛡️ 安全架构

  • 无日志密钥: VTpass 密钥、Supabase service role key、Telegram 令牌及所有其他密钥都严格限制在服务端 API 路由中,绝不会暴露在客户端 bundle 中。
  • 重放保护: 每个区块链交易哈希都会记录在持久账本中(Supabase 表,对 tx hash 设置唯一约束),且一个哈希最多只能对应一笔交易。Web 应用的支付意图由服务器签发的 ID 创建,并以插入方式写入(绝不覆盖)。数据库触发器(supabase/migrations/026_transaction_state_guard.sql)会拒绝将已完成的交易恢复为待处理状态、修改真实 tx hash,或在交易离开待处理状态后更改其服务商引用,因此已结算或已退款的支付无法再次发放。⚠️ 在无服务器环境中,仅依赖内存跟踪并不安全:冷启动时状态会重置,且不同并发实例之间不会共享状态。
  • 链上验证: 在发放任何账单前,服务器会读取交易回执,并要求已配置的 AbaPay 金库针对支付开始时记录的代币、金额、账户、服务和付款方(而非请求中的值)发出 PaymentReceived(src/lib/paymentProof.ts)。小数位数取自事件中指定的代币,因此使用其他代币进行的支付无法冒充指定代币的支付。直接钱包调用、在 Base 上由赞助的 ERC-4337 UserOperation 发起的支付,或由 Agent 中继器发起的支付,都会以相同方式发出该事件。发放的账单从服务器存储的记录中读取,而非来自客户端。
  • 事件交叉验证: Webhook 会解码 PaymentReceived 事件,并要求其中的付款方、代币、金额和账号均与待处理记录相符,之后才会发放账单。这可以阻止这样一类攻击:用户先创建一笔金额较小的待处理意图,然后手动向合约转入不同(或更多/更少)的金额,企图将这笔转账错误地关联到该记录。
  • 过期意图清理: 预检意图(签名前创建的记录)如果始终未生成链上交易,会由计划清理任务自动过期(/api/cleanup,每 15 分钟运行一次),避免它们永远停留在 PENDING 状态。清理任务只会处理带有 preflight_ 前缀的行,因此真实的已广播交易绝不会被过期。
  • Webhook 确认: 请求通过签名验证后,即使没有找到匹配的交易记录(测试 ping、无关活动或尚未同步的支付意图都属于正常且预期的情况,并非投递失败),Webhook 始终会返回 2xx。如果此处返回非 2xx,Alchemy 可能会在多次“失败”后最终自动禁用该 Webhook,而这些“失败”其实并非真正的失败。
  • VTpass 延迟状态 Webhook: /api/webhook/vtpass 会立即回复 {"response": "success"}——这是 VTpass 能正确解析的确切确认消息——然后才开始实际处理(通过 after()),因为 VTpass 要求及时且轻量的回复,任何其他回复都会被视为未确认投递并触发重试。绝不会信任推送本身:处理程序会通过服务器到服务器的方式,使用我们的 API 密钥重新查询 VTpass,并且只根据已确认的状态采取操作。
  • 汇率验证: 发放账单前,服务器会根据平台实时汇率检查已支付的加密货币金额,即使客户端被篡改,也能防止少付攻击。
  • 智能合约金库: 用户的稳定币会直接存入不可变的 AbaPay.sol 智能合约金库。只有由合约所有者进行加密签名的交易才能提取资金,任何后端服务都不会直接托管用户资金。
  • 自动退款: 如果已验证的链上支付未能成功发放(服务商中断、详情无效等),系统会标记该交易并将款项退回用户钱包,同时将退款交易哈希记录到账本中。
  • 退款验证: /api/admin/refund 会在将交易标记为 REFUNDED 前,在链上验证退款(从交易的 ERC-20 Transfer 日志中解码代币、接收方和金额),因此管理员无法登记一笔实际上并未发生的退款。
  • RPC 故障转移: 链上读取通过 viem 的 fallback() 传输方式使用多个 RPC 端点(src/lib/chain.ts),因此单个服务商宕机不会中断支付验证。
  • 内容安全策略(Content-Security-Policy): 以 Content-Security-Policy-Report-Only 模式发布(next.config.ts),可显示违规情况,而不会影响钱包连接。确认与真实钱包流程兼容后,可切换到强制执行的 Content-Security-Policy。
  • 管理员身份验证: 仅管理员可访问的 API 路由和 /admin 控制面板,都由专用身份验证保护(src/utils/adminAuth.ts),与公开商店分离。获准列表中的运维钱包(绝非金库所有者)只需使用 Ethereum 签名登录消息登录一次(与域名绑定的一次性 nonce)。该钱包会获得一个 HttpOnly、Secure、SameSite=Strict 会话 Cookie,空闲 2 小时后过期,最长有效 8 小时,并可通过“退出登录”或“在所有设备上退出登录”撤销。系统只存储会话哈希。更改状态的管理员请求还必须提供同站点 Origin。链上金库操作仍需使用所有者钱包,因为合约会强制执行这一要求。
  • 仅限内部使用的 AI 路由: DeAI “大脑”(/api/deai/*)只能由应用自身的机器人 Webhook 通过签名的内部服务令牌访问(src/utils/internalAuth.ts)。这可防止公共互联网冒充任何用户的聊天 ID/电话号码/X ID,或消耗 Claude API 预算。
  • 机器人 Webhook 签名: WhatsApp 和 X Webhook 会验证每个传入负载的 Meta X-Hub-Signature-256/X x-twitter-webhooks-signature HMAC(配置了相应密钥时),Telegram 则会验证其密钥令牌,从而防止消息事件被伪造。
  • 哈希处理的交易 PIN: DeAI PIN 会以加盐的 scrypt 哈希形式存储(src/utils/pinSecurity.ts),绝不以明文存储;旧版明文值会在下次使用时自动升级,并设有 4 次尝试的锁定机制。
  • 受限的 Paymaster 代理: Gas 赞助代理(/api/paymaster)仅将 ERC-7677 paymaster JSON-RPC 方法列入允许列表,因此无法被滥用为使用你的 CDP 密钥运行的通用 RPC 中继。它还会解码每个 UserOperation(智能钱包的 execute/executeBatch),并且只赞助 AbaPay 自身的调用:受支持稳定币的 approve(vault, …),以及金库的 payBill/setSpendingAllowance;不允许携带原生代币价值,且仅限 Base。其他任何请求都会收到 -32602 拒绝响应,绝不会转发到 CDP。请求会按 IP 和智能钱包发送方进行速率限制(src/lib/paymasterPolicy.ts)。
速率限制

两个相互独立的层级,均在 src/lib/rateLimit.ts 中实现,且都属于纵深防御——真正的最后防线并非它们,而是链上额度、紧急停用开关和 PIN 锁定机制。它们只是用来减缓泄露凭据遭滥用的速度,让凭据所有者有时间看到带外警报并撤销凭据。

  • 第 1 层 — 路由级,按 IP 限制。 使用客户端 IP(getClientKey)作为键的固定窗口,通过 enforceRateLimit 按端点应用。IP 按信任顺序读取:先读 x-vercel-forwarded-for,再读 x-real-ip,最后读 x-forwarded-for 中最右侧的条目(绝不读最左侧的条目,因为调用方可以通过在请求头前置自己的值来伪造它)。
  • 第 2 层 — 仅针对支出操作,按身份限制。 pay_bill、schedule_bill 和 pay_bill_batch 还会按用于验证调用的凭据对应的 agent_links 行 ID 进行速率限制(src/lib/deai/mcpTools.ts 中的 checkSpendRateLimit),而不是按 IP 限制,因此泄露的 API 密钥或 OAuth 令牌无法通过轮换源地址来绕过限制。此检查在 PIN 检查之后运行,因为递增式 PIN 锁定只会在 PIN 错误时触发,无法减缓一连串正确调用。
范围限制窗口适用对象
mcp6060s每次 /api/mcp 工具调用,按 IP 限制
a2a6060s每次 /api/a2a 调用,按 IP 限制
mcp-pay_bill1060spay_bill,按凭据限制
mcp-schedule_bill560sschedule_bill,按凭据限制
mcp-pay_bill_batch560spay_bill_batch,按凭据限制
oauth-authorize40300sGET /api/oauth/authorize
oauth-authorize-submit15300s同意表单提交
oauth-token60300s令牌交换
oauth-register20300s动态客户端注册
agent-link-read6060s读取已关联 Agent 的配置
agent-link-write10300s创建/编辑/撤销 Agent 关联
pay3060sREST /api/pay
schedules-read / schedules-write60 / 2060s/api/schedules
deai-chat2060sTelegram/WhatsApp/X 上的一轮聊天
deai-resolve3060s意图解析
requery3060s/api/requery
verify / otp-request / otp-confirm20 / 5 / 1560s / 600s / 60s账户验证、发送 OTP、确认 OTP
monnify-verify / monnify-banks / monnify-resolve / monnify-requery20 / 30 / 8 / 3060sMonnify 银行转账查询
foreign6060s国际目录读取
support5300s提交支持工单

工作机制: 使用 Postgres 固定窗口(rate_limits 表),并通过 rate_limit_hit RPC(supabase/migrations/023_rate_limit_atomic_increment.sql)以原子方式递增,从而避免并发请求在任何请求写入前都读到“未超限”的竞态;若尚未部署该迁移,则回退到非原子的读-改-写。失败时放行,而非拒绝: 如果无法访问 rate_limits 表或 RPC,就允许请求通过,而不是让整个 API 停摆——速率限制是滥用控制机制,不是身份验证控制机制,绝不能成为单点故障(001_rate_limits.sql 用于创建该表;如果没有此表,速率限制会静默失效)。受限调用会返回 HTTP 429,并带有 Retry-After 请求头和 JSON 响应体:{"success": false, "error": "Too many requests..."}。

📖 面向用户的文档页面

有四个页面会向用户介绍 AbaPay 的功能。它们不会自动生成——除非主动更新,否则会悄然过时,因此应将其视为变更的一部分,而非事后才考虑的事项:

页面位置入口
文档与常见问题页面src/app/docs/page.tsxAppFooter 中的“文档与常见问题”链接
服务条款(完整版)src/app/terms/page.tsxAppFooter 中的“条款”链接
隐私政策(完整版)src/app/privacy/page.tsxAppFooter 中的“隐私”链接
本 READMEREADME.mdGitHub

⚠️ /terms 和 /privacy 由工程师而非律师撰写,且未经法律审查。在具备资质的律师审查之前,不应将这两份文件视为经过法律审核;/terms 底部有一条醒目的声明,明确说明了这一点,重写该页面时必须保留这条声明。

/docs 是唯一的文档与常见问题页面——应用内没有单独的条款、隐私或常见问题弹窗。


🏢 法律实体

AbaPay 由 Masonode Technologies Limited 运营,该公司在尼日利亚联邦共和国企业事务委员会(CAC)注册,注册号为 RC 9524980。


📄 许可证

本仓库中的代码采用 MIT 许可证——可免费使用、分叉和构建,包括供集成 AbaPay 的 MCP/x402 支付通道的独立 Agent/应用使用。本许可证仅适用于代码;不授予 AbaPay 名称或品牌的使用权,也不对使用线上 abapays.com 服务的法律条款作出任何声明;相关条款由 /terms 单独规定。


👨‍💻 维护者

由 Oluwafemi Olagoke (@investorphem) 构建并维护。

专注于 Web3、去中心化 AI 和可扩展的区块链应用。

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。