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.
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.
openssl rand -base64 32
# or
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
Install the SDK#
npm install @okxweb3/mpp mppx viem
| Package | Version | Purpose |
|---|---|---|
@okxweb3/mpp | latest | OKX protocol implementation; exposes the charge and session methods |
mppx | >= 0.3.15 | Provides the Proxy / Service factories (under mppx/proxy) |
viem | >= 2.21 | Seller 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#
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:
| Form | Behavior |
|---|---|
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. |
true | Free passthrough. No payment required; upstream credentials are still injected. |
All three forms can be freely mixed within the same 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: {
// 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:
// 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/inference → https://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:
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:
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#
| Field | Type | Required | Description |
|---|---|---|---|
id (first argument) | string | Yes | Service identifier; used as the URL prefix (/{id}/...) |
baseUrl | string | Yes | Upstream service root URL, excluding path |
title | string | No | Human-readable name shown in discovery endpoints |
description | string | No | Service summary, shown in discovery endpoints |
bearer | string | No | Injects Authorization: Bearer <token> on the upstream request |
headers | Record<string, string> | No | Injects arbitrary custom headers; mutually exclusive with bearer |
routes | Record<string, Endpoint> | Yes | Map of route patterns to payment definitions |
rewriteRequest | (req, ctx) => Request | No | Full upstream request rewrite; takes precedence over bearer / headers |
rewriteResponse | (res, ctx) => Response | No | Modifies the upstream response before it reaches the client |
Discovery endpoints#
Proxy.create automatically exposes three read-only discovery endpoints — no configuration needed:
| Endpoint | Content |
|---|---|
GET /llms.txt | LLM-friendly Markdown listing of available services |
GET /discover | JSON 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#
- 1Send a request to the proxy endpoint via Onchain OS
- 2The proxy returns 402 with a PAYMENT-REQUIRED response header
- 3Complete payment via Agentic Wallet
- 4The wallet automatically replays the request
- 5After verifying the payment, the proxy injects upstream credentials, forwards the request, and returns the upstream response
