# BotPay 商户接入

外部商户的 Bot 可以自己注册、拿到 API key，然后在 Base Sepolia 上用 USDC 报价和付款。默认演示金额是 **1 USDC**。没有主网。

线上地址：`https://botpay.polyos.ai`

机器可读说明：

- `GET /v1/onboarding`
- `GET /.well-known/botpay.json`
- `GET /onboarding.md`

## 先记住这几条

- API key 和 webhook secret **只在注册或轮换时出现一次**。立刻放进密钥管理器，不要写进仓库、日志或对话。
- 服务端只存 API key 的 SHA-256。丢了就轮换，没有找回接口。
- 收款地址由收款商户自己设置。付款请求里不要传 `pay_to` 或 `to_address`。
- 会动钱的 POST 必须带 `Idempotency-Key`（用 UUID）。相同 key 和相同请求体会重放；请求体不同会返回 `IDEMPOTENCY_KEY_CONFLICT`。
- 单笔超过 50 USDC 要管理员审批，超过 100 USDC 直接拒绝。
- 付款从**平台共享的测试网钱包**转出。自助注册的商户默认每天最多花 **5 USDC**（UTC 日切），全体自助商户每天合计默认 **50 USDC**。演示商户 `mer_a` / `mer_b` 不走这本账。管理员可以把单个商户的上限调到 0–100 USDC/日。
- 注册接口不强制 Turnstile，方便 Bot 直接调用，但按 IP 和全局限流。配了 Turnstile 时，带上无效 token 会被拒绝；不带 token 仍然可以注册。

## 1. 注册

付款方和收款方各注册一次。`scope_preset` 省略时是 `full`，同一把 key 既能付也能收。只要付款用 `payer`，只要收款用 `payee`。

```bash
curl -sS https://botpay.polyos.ai/v1/merchants \
  -H 'content-type: application/json' \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"name":"Acme Bot","scope_preset":"full"}'
```

响应里把下面三个值存成 secret，不要打印到对话里：

- `data.api_key`（形如 `bp_live_...`）
- `data.webhook_secret`（形如 `whsec_...`）
- `data.merchant_id`

15 分钟内用同一个 `Idempotency-Key` 重试，会拿回同一次注册结果（含密钥）。这个重放缓存在 KV，过期后数据库里只剩哈希。

## 2. 接上 MCP

服务是无状态的 Streamable HTTP，只接受 `POST /mcp`。用 Bearer token。`register_merchant` 不需要 token，其余工具需要。

只支持 stdio 的客户端可以用 `mcp-remote`：

```json
{
  "mcpServers": {
    "botpay": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://botpay.polyos.ai/mcp",
        "--header",
        "Authorization: Bearer ${BOTPAY_API_KEY}"
      ],
      "env": {
        "BOTPAY_API_KEY": "把密钥放在环境变量里，不要写进这个文件"
      }
    }
  }
}
```

Cursor 也可以直接配远程 MCP：

```json
{
  "mcpServers": {
    "botpay": {
      "url": "https://botpay.polyos.ai/mcp",
      "headers": {
        "Authorization": "Bearer ${BOTPAY_API_KEY}"
      }
    }
  }
}
```

协议版本 `2025-03-26` 或 `2025-06-18`。先 `initialize`，再 `notifications/initialized`，然后 `tools/list` / `tools/call`。服务器不提供 GET SSE 会话。

还没有 key 时，可以不带 Authorization 调用工具 `register_merchant`（参数 `name`、`idempotency_key`）。拿到 key 之后重新连接。

## 3. 收款方设置地址

地址必须是 EIP-55 校验和形式。全零、`0x1111…` 这类占位地址、以及平台出款钱包会被拒绝。

下面只是格式示例，请换成你自己的 Base Sepolia 地址：

```bash
curl -sS -X PUT https://botpay.polyos.ai/v1/merchants/me/wallet \
  -H "authorization: Bearer $BOTPAY_API_KEY" \
  -H 'content-type: application/json' \
  -d '{"address":"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"}'
```

MCP 工具：`set_payee_wallet`。

## 4. 跑一笔 1 USDC

假设收款方商户 id 是 `$PAYEE`，付款方是 `$PAYER`，两边都用自己的 key。

收款方报价：

```bash
curl -sS https://botpay.polyos.ai/v1/quotes \
  -H "authorization: Bearer $PAYEE_KEY" \
  -H 'content-type: application/json' \
  -H "Idempotency-Key: $(uuidgen)" \
  -d "{\"payer_merchant_id\":\"$PAYER\",\"service_ref\":\"demo.1usdc\",\"amount\":{\"value\":\"1.00\",\"currency\":\"USDC\"},\"network\":\"eip155:84532\"}"
```

付款方接受并执行（不要加 `pay_to`）：

```bash
curl -sS https://botpay.polyos.ai/v1/payment-intents \
  -H "authorization: Bearer $PAYER_KEY" \
  -H 'content-type: application/json' \
  -H "Idempotency-Key: $(uuidgen)" \
  -d "{\"quote_id\":\"$QUOTE_ID\"}"

curl -sS -X POST "https://botpay.polyos.ai/v1/payment-intents/$PAYMENT_ID/execute" \
  -H "authorization: Bearer $PAYER_KEY" \
  -H 'content-type: application/json' \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{}'
```

然后 `GET /v1/payment-intents/$PAYMENT_ID`，等到 `SETTLED`。MCP 上对应的工具是 `create_quote`、`create_payment_intent`、`execute_payment`、`get_payment`。`create_quote` 的金额字段是 `amount_value`，演示填 `"1.00"`。

没有白名单记录的付款商户可以向任意有效收款商户付款。预置的 `mer_a` 仍然只能付给白名单里的 `mer_b`。

## 5. Webhook 和密钥

```bash
curl -sS https://botpay.polyos.ai/v1/webhooks/subscriptions \
  -H "authorization: Bearer $PAYEE_KEY" \
  -H 'content-type: application/json' \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"url":"https://example.com/botpay","event_types":["payment.settled"]}'
```

验签用注册时拿到的 `webhook_secret`。头是 `X-Webhook-Timestamp` 和 `X-Webhook-Signature`（`v1=` + HMAC-SHA256，消息是 `{timestamp}.{body}`）。

轮换 API key：`POST /v1/api-keys/{id}/rotate`（需要 `Idempotency-Key`）。旧 key 立即失效，新 key 只出现一次。`POST /v1/api-keys/{id}/revoke` 可以吊销其中一把，但不能吊销最后一把。`GET /v1/api-keys` 只返回前缀。

轮换 webhook secret：`POST /v1/webhooks/secret/rotate`。

看自己是谁：`GET /v1/merchants/me`。这里有今日剩余额度，没有密钥。

## 排障

| 现象 | 原因 |
|---|---|
| `401` | key 不存在。已吊销的 key 是 `403`。演示 key 在 `ALLOW_DEMO_KEYS=false` 时也是 `401`。`bp_live_` 不受演示开关影响 |
| `403` 缺少权限 | scope 不够。`payer` 不能报价，`payee` 不能执行付款，`read` 不能轮换 key |
| `400` 不允许由客户端指定收款地址 | 请求里带了 `pay_to` 或 `to_address` |
| `400` 收款地址必须是 EIP-55 | 用了全小写，或校验和错了 |
| `422 PAYEE_WALLET_MISSING` | 收款方还没设置地址 |
| `422 SPEND_ALLOWANCE_EXCEEDED` | 超过该商户或全平台的当日额度。换新的 Idempotency-Key，或等 UTC 日切，或请管理员调整 |
| `409 APPROVAL_REQUIRED` | 金额大于 50 USDC，要管理员批准后才能 execute |
| `422 AMOUNT_OVER_LIMIT` | 单笔超过 100 USDC |
| `429 RATE_LIMITED` | 这个 IP 每小时注册太多。默认 5 次，全平台默认 120 次 |
| `409` 不能吊销最后一把密钥 | 改用 rotate |
