Integrate via Reverse Proxy#

Add stablecoin per-call payments to any API — no changes to your core business logic.

Put a payment gate in front of your existing HTTP service without modifying its code. The proxy sits between buyers and your origin service, handles the 402 negotiation, injects upstream credentials, and forwards paid requests.

How it works#

The proxy handles the lifecycle of every request — verify, inject, forward. The buyer never sees your upstream credentials.

If you're building a new service and can freely modify its code, Integrate via SDK is lighter.

Prerequisites#

General setup#

  • Receiving wallet: Any EVM-compatible wallet (e.g. Agentic Wallet). You'll need its private key to sign on-chain transactions.
  • API credentials: Create them on the OKX Developer Portal. If you're using Agentic Wallet as your receiving wallet, no API credentials are required.
  • Backend service: Your existing HTTP API service, already deployed.

Proxy-specific setup#

The proxy needs a local HMAC key MPPX_SECRET_KEY to sign HTTP 402 Challenges. You generate it yourself — it's unrelated to OKX. A leak lets attackers forge Challenges, and rotation requires a proxy restart.

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

Install the SDK#

bash
npm install @okxweb3/mpp mppx viem
PackageVersionPurpose
@okxweb3/mpplatestOKX protocol implementation; exposes the charge and session methods
mppx>= 0.3.15Provides the Proxy / Service factories (under mppx/proxy)
viem>= 2.21Seller signing utilities
Proxy and Service are not re-exported from @okxweb3/mpp — you must import them from mppx/proxy directly. That's why mppx is declared as an explicit dependency.

Build the proxy#

1. Initialize the mppx instance#

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!,
});

Register only the methods you need: if you use charge only, omit session(...) (you won't even need sellerSigner); if you use session only, omit charge(...).

2. Define service routes#

Each Service.from describes an upstream service and the payment requirements of its routes. A route can take one of three forms:

FormBehavior
mppx.charge({...})One-time payment. Each request renegotiates 402 and verifies a fresh Credential.
mppx.session({...})Pay-as-you-go. The client opens an on-chain channel once, then signs offline vouchers for subsequent requests.
trueFree passthrough. No payment required; upstream credentials are still injected.

All three forms can be freely mixed within the same 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: {
        // Free passthrough (upstream Bearer credentials still injected)
        "GET /v1/status": true,

        // One-time payment: 0.01 USDT0 per request
        "GET /v1/forecast": mppx.charge({
          amount: "10000",
          currency: CURRENCY,
          recipient: RECIPIENT,
          description: "Single forecast lookup",
          methodDetails: { chainId: CHAIN_ID, feePayer: true },
        }),

        // Pay-as-you-go: channel-based cumulative billing
        "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,
          },
        }),
      },
    }),
  ],
});

Route patterns support :param named parameters and * wildcards. Requests that don't match any route return 404 — they're never forwarded upstream.

MPP_ESCROW in the example refers to the official Escrow contract OKX deploys on X Layer, used by the pay-as-you-go (session) mode. See pay-as-you-go for details.

3. Start the proxy#

Proxy.create returns an instance that exposes both a fetch handler and a Node-style listener:

typescript
// Node.js
import { createServer } from "node:http";
createServer(proxy.listener).listen(3000);

// Bun / Deno / Cloudflare Workers
export default { fetch: proxy.fetch };

Incoming requests follow the pattern /<serviceId>/<upstreamPath>. The proxy strips <serviceId> and forwards <upstreamPath> appended to the service's baseUrl.

For example: POST /weather/v1/inferencehttps://api.weather.example.com/v1/inference


Advanced configuration#

Multiple upstream services#

A single Proxy.create call can register any number of Services, each mounted under its own path prefix:

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 },
        }),
      },
    }),
  ],
});

Dynamic request rewriting#

When your upstream expects more than a static credential — for example body-based HMAC signing, SigV4, or dynamic nonces — use the rewriteRequest hook to take over request construction:

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 });
  },
});

Once rewriteRequest is defined, bearer and headers are ignored — the hook is fully responsible for constructing the upstream request. ctx provides request, service, upstreamPath, and the endpoint's options.

Service.from full reference#

FieldTypeRequiredDescription
id (first argument)stringYesService identifier; used as the URL prefix (/{id}/...)
baseUrlstringYesUpstream service root URL, excluding path
titlestringNoHuman-readable name shown in discovery endpoints
descriptionstringNoService summary, shown in discovery endpoints
bearerstringNoInjects Authorization: Bearer <token> on the upstream request
headersRecord<string, string>NoInjects arbitrary custom headers; mutually exclusive with bearer
routesRecord<string, Endpoint>YesMap of route patterns to payment definitions
rewriteRequest(req, ctx) => RequestNoFull upstream request rewrite; takes precedence over bearer / headers
rewriteResponse(res, ctx) => ResponseNoModifies the upstream response before it reaches the client

Discovery endpoints#

Proxy.create automatically exposes three read-only discovery endpoints — no configuration needed:

EndpointContent
GET /llms.txtLLM-friendly Markdown listing of available services
GET /discoverJSON description of all services
GET /discover/<serviceId>Detailed information for a single service: routes, prices, documentation

Discovery endpoints are themselves free to access. Upstream credentials (bearer / headers) are never exposed — only the id / title / description / routes payment metadata appears in the output.


Test the proxy#

  1. 1
    Send a request to the proxy endpoint via Onchain OS
  2. 2
    The proxy returns 402 with a PAYMENT-REQUIRED response header
  3. 3
    Complete payment via Agentic Wallet
  4. 4
    The wallet automatically replays the request
  5. 5
    After verifying the payment, the proxy injects upstream credentials, forwards the request, and returns the upstream response