通过反向代理接入#
给任何 API 加上稳定币按调用计费——核心业务代码零改动。
把支付网关放在你现有的 HTTP 服务前面,无需修改它的代码。代理在买家与你的业务服务之间,完成 402 协商、注入上游凭证、转发已付费请求。
工作原理#
代理处理每一次请求的生命周期——校验、注入、转发。买家永远看不到上游凭证。
准备工作#
通用准备#
- 收款钱包:任何 EVM 兼容钱包(如 Agentic Wallet)。你需要持有钱包私钥用于签上链交易。
- API 密钥:通过 OKX 开发者管理平台 申请。如果你的收款钱包是 Agentic Wallet,则无需申请 API 密钥。
- 业务后端:你自己已部署的 HTTP API 服务
代理特有准备#
代理需要一个本地 HMAC 密钥 MPPX_SECRET_KEY,用于对 HTTP 402 Challenge 做签名。由你自己生成,与 OKX 无关——泄露会导致 Challenge 可被伪造,轮换需重启代理。
openssl rand -base64 32
# 或
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
安装 SDK#
npm install @okxweb3/mpp mppx viem
| 包 | 版本 | 说明 |
|---|---|---|
@okxweb3/mpp | latest | OKX MPP 协议实现,提供 charge 与 session 方法 |
mppx | >= 0.3.15 | 提供 Proxy / Service 工厂(位于 mppx/proxy 子路径) |
viem | >= 2.21 | 卖家签名工具 |
Proxy 与 Service 不在 @okxweb3/mpp 的导出表中,必须直接从 mppx/proxy 导入,因此 mppx 需要作为显式依赖安装。接入服务#
1. 初始化 mppx 实例#
import { Mppx } from "@okxweb3/mpp";
import { charge, session } from "@okxweb3/mpp/evm/server";
import { SaApiClient } from "@okxweb3/mpp/evm";
import { privateKeyToAccount } from "viem/accounts";
const saClient = new SaApiClient({
apiKey: process.env.OKX_API_KEY!,
secretKey: process.env.OKX_SECRET_KEY!,
passphrase: process.env.OKX_PASSPHRASE!,
baseUrl: "https://web3.okx.com",
});
const sellerSigner = privateKeyToAccount(
process.env.SELLER_PRIVATE_KEY! as `0x${string}`,
);
const mppx = Mppx.create({
methods: [
charge({ saClient }),
session({ saClient, signer: sellerSigner }),
],
realm: "api-proxy.example.com",
secretKey: process.env.MPPX_SECRET_KEY!,
});
methods 数组按需注册:只用 charge 可省略 session(...)(连 sellerSigner 都不需要);只用 session 可省略 charge(...)。
2. 定义服务路由#
每个 Service.from 描述一个上游服务及其路由的支付要求。路由值有三种形式:
| 形式 | 行为 |
|---|---|
mppx.charge({...}) | 单次扣费。每个请求都需要重新发起 402 并校验新的 Credential |
mppx.session({...}) | 通道扣费。客户端先 open 一次链上 channel,后续请求只签离线 voucher |
true | 免费透传,无需付费;上游凭证仍会被注入 |
三种形式可以在同一个 Service 内自由混搭:
import { Proxy, Service } from "mppx/proxy";
const CURRENCY = "0x779ded0c9e1022225f8e0630b35a9b54be713736"; // X Layer USDT0
const RECIPIENT = process.env.SELLER_ADDRESS!;
const CHAIN_ID = 196;
const proxy = Proxy.create({
title: "API Proxy",
description: "Payment-gated proxy with charge and session routes",
services: [
Service.from("weather", {
title: "Weather + Inference API",
description: "Upstream API protected by MPP payments",
baseUrl: "https://api.weather.example.com",
bearer: process.env.UPSTREAM_API_KEY!,
routes: {
// 免费透传(仍注入上游 Bearer 凭证)
"GET /v1/status": true,
// 单次扣费:每次请求 0.01 USDT0
"GET /v1/forecast": mppx.charge({
amount: "10000",
currency: CURRENCY,
recipient: RECIPIENT,
description: "Single forecast lookup",
methodDetails: { chainId: CHAIN_ID, feePayer: true },
}),
// 按量计费:基于通道累计扣费
"POST /v1/inference": mppx.session({
amount: "500",
currency: CURRENCY,
recipient: RECIPIENT,
description: "Per-call inference",
unitType: "request",
suggestedDeposit: "100000",
methodDetails: {
chainId: CHAIN_ID,
escrowContract: process.env.MPP_ESCROW!,
feePayer: true,
},
}),
},
}),
],
});
路径模式支持 :param 命名参数与 * 通配符。未匹配 routes 的请求统一返回 404,不会回落到上游。
MPP_ESCROW 是 OKX 在 X Layer 上部署的官方 Escrow 合约地址,用于通道扣费模式。详见按量支付。3. 启动代理#
Proxy.create 返回的实例同时暴露 fetch 与 listener 两种入口:
// Node.js
import { createServer } from "node:http";
createServer(proxy.listener).listen(3000);
// Bun / Deno / Cloudflare Workers
export default { fetch: proxy.fetch };
请求路径形式为 /<serviceId>/<upstreamPath>,代理将 <upstreamPath> 拼接到 baseUrl 后转发。
例如:POST /weather/v1/inference → https://api.weather.example.com/v1/inference
进阶配置#
多服务部署#
单个 Proxy.create 调用可注册任意数量的 Service,每个服务挂载在独立的路径前缀下:
const proxy = Proxy.create({
title: "Multi-Service Proxy",
services: [
Service.from("serviceA", {
baseUrl: "https://api.a.example.com",
bearer: process.env.A_API_KEY!,
routes: {
"GET /v1/models": true,
"POST /v1/query": mppx.charge({
amount: "50000",
currency: CURRENCY,
recipient: RECIPIENT,
methodDetails: { chainId: CHAIN_ID, feePayer: true },
}),
},
}),
Service.from("serviceB", {
baseUrl: "https://api.b.example.com",
headers: { "X-API-Key": process.env.B_API_KEY! },
routes: {
"POST /v1/analyze": mppx.charge({
amount: "100000",
currency: CURRENCY,
recipient: RECIPIENT,
methodDetails: { chainId: CHAIN_ID, feePayer: true },
}),
},
}),
],
});
动态请求改写#
当上游需要的不仅是静态凭证(例如基于请求体的 HMAC、SigV4、动态 nonce),使用 rewriteRequest 钩子接管请求改写:
Service.from("custom", {
baseUrl: "https://api.custom.example.com",
routes: {
"POST /v1/op": mppx.charge({
amount: "10000",
currency: CURRENCY,
recipient: RECIPIENT,
methodDetails: { chainId: CHAIN_ID, feePayer: true },
}),
},
rewriteRequest: async (req, ctx) => {
const body = await req.clone().text();
const timestamp = Date.now().toString();
const signature = await signHmac(
process.env.CUSTOM_SECRET!,
timestamp + req.method + ctx.upstreamPath + body,
);
const headers = new Headers(req.headers);
headers.set("X-Timestamp", timestamp);
headers.set("X-Signature", signature);
return new Request(req.url, { method: req.method, headers, body });
},
});
提供 rewriteRequest 后,bearer / headers 不再生效,钩子函数全权负责生成发送到上游的请求。ctx 提供 request / service / upstreamPath 与当前 endpoint 的 options 字段。
Service.from 完整字段#
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
id(首参数) | string | 是 | 服务标识,作为 URL 前缀(/{id}/...) |
baseUrl | string | 是 | 上游服务根 URL,不包含路径 |
title | string | 否 | 用于发现端点的可读名称 |
description | string | 否 | 服务摘要,出现在发现端点中 |
bearer | string | 否 | 注入 Authorization: Bearer <token> 至上游请求 |
headers | Record<string, string> | 否 | 注入任意自定义 header,与 bearer 互斥使用 |
routes | Record<string, Endpoint> | 是 | 路由模式到支付定义的映射 |
rewriteRequest | (req, ctx) => Request | 否 | 全量改写上游请求;优先级高于 bearer / headers |
rewriteResponse | (res, ctx) => Response | 否 | 在响应返回客户端前进行修改 |
自动发现端点#
Proxy.create 会自动暴露三个只读发现端点,无需手动配置:
| 端点 | 内容 |
|---|---|
GET /llms.txt | 服务清单的 LLM 友好 Markdown 表示 |
GET /discover | 全部服务的 JSON 描述 |
GET /discover/<serviceId> | 单个服务的详情,包括路由、价格、文档链接 |
发现端点本身免费访问。bearer / headers 等上游凭证不会出现在输出中——仅暴露 id / title / description / routes 的支付元数据。
测试服务#
- 1通过 Onchain OS 访问代理端口
- 2代理返回 402 状态码和 PAYMENT-REQUIRED 响应头
- 3使用 Agentic Wallet 完成支付
- 4钱包自动重试请求
- 5代理验证支付通过后,注入上游凭证并转发请求,返回上游响应
