通过反向代理接入#

给任何 API 加上稳定币按调用计费——核心业务代码零改动。

把支付网关放在你现有的 HTTP 服务前面,无需修改它的代码。代理在买家与你的业务服务之间,完成 402 协商、注入上游凭证、转发已付费请求。

工作原理#

代理处理每一次请求的生命周期——校验、注入、转发。买家永远看不到上游凭证。

如果你的服务是自研项目、可以直接修改业务代码,通过 SDK 接入 是更轻量的方式—。

准备工作#

通用准备#

  • 收款钱包:任何 EVM 兼容钱包(如 Agentic Wallet)。你需要持有钱包私钥用于签上链交易。
  • API 密钥:通过 OKX 开发者管理平台 申请。如果你的收款钱包是 Agentic Wallet,则无需申请 API 密钥。
  • 业务后端:你自己已部署的 HTTP API 服务

代理特有准备#

代理需要一个本地 HMAC 密钥 MPPX_SECRET_KEY,用于对 HTTP 402 Challenge 做签名。由你自己生成,与 OKX 无关——泄露会导致 Challenge 可被伪造,轮换需重启代理。

bash
openssl rand -base64 32
# 或
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"

安装 SDK#

bash
npm install @okxweb3/mpp mppx viem
版本说明
@okxweb3/mpplatestOKX MPP 协议实现,提供 chargesession 方法
mppx>= 0.3.15提供 Proxy / Service 工厂(位于 mppx/proxy 子路径)
viem>= 2.21卖家签名工具
ProxyService 不在 @okxweb3/mpp 的导出表中,必须直接从 mppx/proxy 导入,因此 mppx 需要作为显式依赖安装。

接入服务#

1. 初始化 mppx 实例#

typescript
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 内自由混搭:

typescript
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 返回的实例同时暴露 fetchlistener 两种入口:

typescript
// 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/inferencehttps://api.weather.example.com/v1/inference


进阶配置#

多服务部署#

单个 Proxy.create 调用可注册任意数量的 Service,每个服务挂载在独立的路径前缀下:

typescript
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 钩子接管请求改写:

typescript
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}/...
baseUrlstring上游服务根 URL,不包含路径
titlestring用于发现端点的可读名称
descriptionstring服务摘要,出现在发现端点中
bearerstring注入 Authorization: Bearer <token> 至上游请求
headersRecord<string, string>注入任意自定义 header,与 bearer 互斥使用
routesRecord<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. 1
    通过 Onchain OS 访问代理端口
  2. 2
    代理返回 402 状态码和 PAYMENT-REQUIRED 响应头
  3. 3
    使用 Agentic Wallet 完成支付
  4. 4
    钱包自动重试请求
  5. 5
    代理验证支付通过后,注入上游凭证并转发请求,返回上游响应