跳转到主要内容
NOWPayments 是纯加密货币的托管发票网关。当你想接 Bitcoin / Ethereum / USDC / USDT 等 300+ 主流币、又不想跑 Coinbase 那种 KYC 流程时用它。商户端 邮箱注册即用,买家在 NOWPayments 托管页面挑币种,crypto 直接打到你的钱包。 适用场景:独立开发者想在卡支付(Stripe / Lemon Squeezy)旁边加一条 crypto 通道;或者整个项目就只接 crypto,绕开传统支付的 KYC / 公司主体要求。
不支持订阅。 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
payment: {
  provider: 'nowpayments' as 'stripe' | 'paddle' | 'lemonsqueezy' | 'creem' | 'nowpayments',
  currency: 'usd',
},
currency 保持 'usd' —— 你这边按美元定价,NOWPayments 在 checkout 时按 实时汇率折成对应 crypto 数量。

2. 配环境变量

.env.local(变量名严格对齐 src/env.ts):
NOWPAYMENTS_API_KEY=YOUR_API_KEY              # Settings → Store settings → API key
NOWPAYMENTS_IPN_SECRET=YOUR_IPN_SECRET        # Settings → Store settings → IPN Secret

# vibestrap 商品的美元定价(字符串形式 —— "49" = $49)
NOWPAYMENTS_PRICE_VIBESTRAP_PROMO=49
NOWPAYMENTS_PRICE_VIBESTRAP_STANDARD=99
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
vibestrap 也会在每次创建发票时单独传 ipn_callback_url,所以即使 dashboard 全局没配也能跑 —— 但两边都设是无害的,且能让失败发票重试时回到 正确的 host。

流程拆解

  1. 用户点 “用 crypto 支付” → 服务端 action 调 paymentManager.createCheckout({ priceId: '49', type: 'one_time', ... })
  2. provider POST 到 https://api.nowpayments.io/v1/invoice,带 price_amount: 49price_currency: 'usd'order_id 编码成 vbs|<userId>|<scene>|<type>(NOWPayments 没有 metadata 字段,所以靠 order_id 反推上下文),并显式带上 ipn_callback_url
  3. NOWPayments 返回托管发票 URL,形如 https://nowpayments.io/payment?iid=4514933743。把用户重定向过去。
  4. 用户在托管页面选币种(BTC / ETH / USDC / …)并打款。
  5. NOWPayments 在支付生命周期里发多次 IPN: waiting → confirming → confirmed → sending → finished。provider 只把 finished 视为已支付;其他状态归一化为 unknown 走 no-op。终态如 failed / expired / partially_paid / refunded 也是 unknown —— 需 要在 NOWPayments dashboard 手动对账。
  6. 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 解析失败、签名长度异常等场景。

验收

  1. 用 NOWPayments sandboxsandbox.nowpayments.io 做测试支付(独立的 API key 和 IPN secret)。
  2. 用 cloudflared / ngrok 把 localhost:3000 暴露出来,把生成的 URL 贴到 sandbox dashboard 的 IPN callback。
  3. pnpm dev → 打开 /pricing → 点 checkout → 跳转到 NOWPayments 托管 发票页。
  4. 用 sandbox 提供的测试网水龙头打款。
  5. 看 IPN 进来 —— 会有好几条,payment_statuswaiting 一路走到 finished
  6. finished 后查 Postgres:
    select id, provider, scene, status, amount, currency, invoice_id
    from payment
    where provider = 'nowpayments'
    order by created_at desc limit 1;
    

常见坑

  • 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。要自动化的话 自己监听 refunded IPN 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?三步:
  1. siteConfig.payment.provider 改成 'nowpayments'
  2. 配上面四个 NOWPAYMENTS_* 环境变量。
  3. NOWPayments dashboard 配 IPN URL。
正在跑的 Stripe / Creem 订阅会按自己的节奏走完 —— vibestrap 按 event.provider 分发 webhook,共享 handler 幂等,老 provider 的事件依然 能正常入 payment 表,互不冲突。

官方文档