不支持订阅。 NOWPayments 有 “recurring billing” 功能,但实质是每期重新
邮件发新发票让用户手动支付,不是自动扣款(因为链上钱包没有”预授权扣
款”能力)。vibestrap 的 provider 在
type: 'subscription' 时直接抛错,
宁可显式失败也不糊弄。NOWPayments 只用于 one-time / lifetime / credit
pack。需要真订阅 → Stripe(USDC)或 Helio。没有 customer portal。 createPortalLink 抛错 —— NOWPayments 没有
Stripe billing portal 那样的用户自服务页面。前置条件
- 一个 NOWPayments 账号 nowpayments.io。基础注册 不需要 KYC,邮箱注册即用。
- 在 dashboard 配收款钱包地址(Settings → Payment settings → Outcome wallet)。crypto 最终打到这里。建议用自托管钱包(Ledger / Trezor)或 你信任的交易所充值地址。
- 在 Settings → Store settings → IPN Secret 生成 IPN 密钥。
- Postgres 跑起来,schema 已 push。
1. 选定当前 Provider
src/config/site.ts:
currency 保持 'usd' —— 你这边按美元定价,NOWPayments 在 checkout 时按
实时汇率折成对应 crypto 数量。
2. 配环境变量
.env.local(变量名严格对齐 src/env.ts):
NOWPayments 没有”product”或”price ID”概念。其他 provider 的
PRICE_* 环境
变量存的是 Stripe price_xxx / Creem 商品 id 之类;NOWPayments 这里直接
存美元金额字符串。provider 内部 parse 成数字后作为 price_amount POST
出去。3. 配置 IPN webhook
NOWPayments 把 webhook 叫 IPN(Instant Payment Notification)。在 Settings → IPN settings:- IPN callback URL:
https://your-domain.com/api/webhooks/nowpayments
ipn_callback_url,所以即使
dashboard 全局没配也能跑 —— 但两边都设是无害的,且能让失败发票重试时回到
正确的 host。
流程拆解
- 用户点 “用 crypto 支付” → 服务端 action 调
paymentManager.createCheckout({ priceId: '49', type: 'one_time', ... })。 - provider POST 到
https://api.nowpayments.io/v1/invoice,带price_amount: 49、price_currency: 'usd',order_id编码成vbs|<userId>|<scene>|<type>(NOWPayments 没有 metadata 字段,所以靠 order_id 反推上下文),并显式带上ipn_callback_url。 - NOWPayments 返回托管发票 URL,形如
https://nowpayments.io/payment?iid=4514933743。把用户重定向过去。 - 用户在托管页面选币种(BTC / ETH / USDC / …)并打款。
- NOWPayments 在支付生命周期里发多次 IPN:
waiting → confirming → confirmed → sending → finished。provider 只把finished视为已支付;其他状态归一化为unknown走 no-op。终态如failed/expired/partially_paid/refunded也是unknown—— 需 要在 NOWPayments dashboard 手动对账。 finished命中后,复用processNormalizedEvent:插入 payment 行(基于payment.invoiceId唯一索引幂等,invoiceId 用 NOWPayments 的payment_id)、发 credits、签发 license(如适用)。
Webhook 签名验证
NOWPayments 用 HMAC-SHA512 对 IPN body 的字典序排序后 JSON 串化结 果做签名,密钥是 IPN secret。签名通过x-nowpayments-sig header 传过来。
verifyNowpaymentsWebhook 实现严格对齐官方 Node 示例
(JSON.stringify(params, Object.keys(params).sort()))和官方 PHP
WooCommerce 插件的 check_ipn_request_is_valid()(ksort + hash_hmac)。
单测覆盖了 round-trip、篡改检测、JSON 解析失败、签名长度异常等场景。
验收
- 用 NOWPayments sandbox:sandbox.nowpayments.io 做测试支付(独立的 API key 和 IPN secret)。
- 用 cloudflared / ngrok 把
localhost:3000暴露出来,把生成的 URL 贴到 sandbox dashboard 的 IPN callback。 pnpm dev→ 打开/pricing→ 点 checkout → 跳转到 NOWPayments 托管 发票页。- 用 sandbox 提供的测试网水龙头打款。
- 看 IPN 进来 —— 会有好几条,
payment_status从waiting一路走到finished。 finished后查 Postgres:
常见坑
is_fee_paid_by_user: false—— vibestrap 默认设这个,让你吃掉网络 手续费而不是把发票金额抬高吓买家。要让用户出可以在src/payment/provider/nowpayments.ts改。- Sandbox 和生产 key 完全独立,IPN secret 也是。混用会签名验证失败但 不会有清晰报错。
partially_paid需要手动对账 —— 用户打款金额对不上(少打、或者 确认窗口里币价波动),NOWPayments 会发partially_paid并把钱托管住。 provider 直接忽略;你要在 dashboard 手动退款或补单。- 退款是 out-of-band 的 —— 没有 Stripe 那种 “create refund” API,要在
NOWPayments dashboard 操作。handler 不会自动回滚 credits。要自动化的话
自己监听
refundedIPN status 并调refundCredits()。 - Order ID 格式是关键路径 ——
vbs|<userId>|<scene>|<type>编码方式 是 IPN handler 反推上下文的唯一通道(NOWPayments 没 metadata)。改前缀 或分隔符必须同步改parseOrderId。 - 价格在创建发票时锁定 —— 如果买家拿到发票后 BTC 涨跌 5%, NOWPayments 锁原价,买家偶尔会反映”多付/少付了几分钱”,这是正常的 crypto 支付行为,不是 vibestrap 的 bug。
- 没有 customer portal —— 买家无法自助退款 / 看账单 / 改支付方式。
要前端展示就自己做
/settings/billing页,读payment表渲染。
从 Stripe / Creem 切过来
已经在跑别的 provider?三步:siteConfig.payment.provider改成'nowpayments'。- 配上面四个
NOWPAYMENTS_*环境变量。 - NOWPayments dashboard 配 IPN URL。
event.provider 分发 webhook,共享 handler 幂等,老 provider 的事件依然
能正常入 payment 表,互不冲突。
官方文档
- NOWPayments: nowpayments.io
- API reference: documenter.getpostman.com/view/7907941/S1a32n38
- IPN 设置指南: nowpayments.io/help/ipn-callbacks
- Sandbox: sandbox.nowpayments.io
- 官方 Node SDK: github.com/NowPaymentsIO/nowpayments-api-js