> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vibestrap.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# NOWPayments

> 纯加密货币的托管发票网关 —— 支持 BTC/ETH/USDC 等 300+ 币种，商户端无 KYC。

NOWPayments 是纯加密货币的托管发票网关。当你想接 Bitcoin / Ethereum /
USDC / USDT 等 300+ 主流币、又不想跑 Coinbase 那种 KYC 流程时用它。商户端
邮箱注册即用，买家在 NOWPayments 托管页面挑币种，crypto 直接打到你的钱包。

适用场景：独立开发者想在卡支付（Stripe / Creem）旁边加一条 crypto
通道；或者整个项目就只接 crypto，绕开传统支付的 KYC / 公司主体要求。

<Note>
  **不支持订阅。** NOWPayments 有 "recurring billing" 功能，但实质是每期重新
  邮件发新发票让用户**手动支付**，不是自动扣款（因为链上钱包没有"预授权扣
  款"能力）。Vibestrap 的 provider 在 `type: 'subscription'` 时**直接抛错**，
  宁可显式失败也不糊弄。NOWPayments 只用于 one-time / lifetime / credit
  pack。需要真订阅 → Stripe（USDC）或 Helio。

  **没有 customer portal。** `createPortalLink` 抛错 —— NOWPayments 没有
  Stripe billing portal 那样的用户自服务页面。
</Note>

## 前置条件

* 一个 NOWPayments 账号 [nowpayments.io](https://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`：

```ts theme={null}
payment: {
  provider: 'nowpayments' as 'stripe' | 'creem' | 'nowpayments',
  currency: 'usd',
},
```

`currency` 保持 `'usd'` —— 你这边按美元定价，NOWPayments 在 checkout 时按
实时汇率折成对应 crypto 数量。

## 2. 配环境变量

`.env.local`（变量名严格对齐 `src/env.ts`）：

```bash theme={null}
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
```

<Note>
  NOWPayments 没有"product"或"price ID"概念。其他 provider 的 `PRICE_*` 环境
  变量存的是 Stripe `price_xxx` / Creem 商品 id 之类；NOWPayments 这里**直接
  存美元金额字符串**。provider 内部 parse 成数字后作为 `price_amount` POST
  出去。
</Note>

## 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: 49`、`price_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 **sandbox**：[sandbox.nowpayments.io](https://sandbox.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_status` 从 `waiting` 一路走到
   `finished`。
6. `finished` 后查 Postgres：
   ```sql theme={null}
   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` 表，互不冲突。

## 官方文档

* NOWPayments: [nowpayments.io](https://nowpayments.io)
* API reference: [documenter.getpostman.com/view/7907941/S1a32n38](https://documenter.getpostman.com/view/7907941/S1a32n38)
* IPN 设置指南: [nowpayments.io/help/ipn-callbacks](https://nowpayments.io/help/ipn-callbacks)
* Sandbox: [sandbox.nowpayments.io](https://sandbox.nowpayments.io)
* 官方 Node SDK: [github.com/NowPaymentsIO/nowpayments-api-js](https://github.com/NowPaymentsIO/nowpayments-api-js)
