在Stable上构建一个MPP端点
本指南将指导您在Stable上为USDT0编写一个自定义的MPP支付方法,并提供一个MPP门控端点。买方签署一个ERC-3009 transferWithAuthorization,服务器通过mppx的verify()钩子验证它,结算在您控制的单独步骤中进行。
您将构建什么
一个HTTP端点,它返回 402 Payment Required 带有 MPP WWW-Authenticate 挑战,在 Authorization 头中接受签名凭据,验证它,在 USDT0 上结算 transferWithAuthorization,并返回带有 Payment-Receipt 头的响应。
步骤 1. 客户端:GET /weather (无Authorization头)
服务器:402 Payment Required
WWW-Authenticate: Payment realm="...", challenges="[...usdt0-stable charge for $0.001...]"
步骤 2. 客户端使用他们的viem账户签署一个ERC-3009授权
步骤 3. 客户端:GET /weather + 包含序列化凭据的Authorization头
服务器:verify()验证EIP-712签名
服务器:settle()提交Stable上的transferWithAuthorization
(~700ms 区块确认)
服务器:200 OK { weather: "sunny" }
Payment-Receipt: reference="0x8f3a...", status="success"
步骤 4. 在Stablescan上验证结算
https://stablescan.xyz/tx/0x8f3a...先决条件
- Stable上资金充足的USDT0钱包。请参阅使用水龙头或移动USDT0。
- Node 20+,并安装了
mppx、viem和zod。 - Stable上的卖家账户(一个EOA)。对于默认的结算路径,卖家以USDT0支付gas;Gas豁免部分展示了零gas变体。
npm install mppx viem zod express1. 定义共享Dapp模式
Method.from() 声明了意图以及请求(挑战)和凭据负载的 Dapp 模式。客户端和服务器都导入此定义。
// src/method.ts
import { Method } from "mppx";
import { z } from "zod";
import { parseUnits } from "viem";
export const USDT0_STABLE = "0x779Ded0c9e1022225f8E0630b35a9b54bE713736";
export const CHAIN_ID = 988;
// Request: The Challenge payload the server sends to the client.
const zRequest = z.pipe(
z.object({
chainId: z.literal(CHAIN_ID),
asset: z.literal(USDT0_STABLE),
amount: z.string(), // human-readable, e.g. "0.001"
decimals: z.literal(6),
payTo: z.string().regex(/^0x[a-fA-F0-9]{40}$/),
validAfter: z.number().int().nonnegative(),
validBefore: z.number().int().positive(),
nonce: z.string().regex(/^0x[a-fA-F0-9]{64}$/),
}),
z.transform(({ amount, decimals, ...rest }) => ({
...rest,
amount: parseUnits(amount, decimals).toString(), // atomic units
})),
);
// Credential payload: what the client returns after signing.
const zPayload = z.object({
from: z.string().regex(/^0x[a-fA-F0-9]{40}$/),
signature: z.string().regex(/^0x[a-fA-F0-9]{130}$/), // 65-byte hex
});
export const usdt0Stable = Method.from({
intent: "charge",
name: "usdt0-stable",
schema: { request: zRequest, credential: { payload: zPayload } },
});
// EIP-712 domain + type, used by both client and server.
export const EIP712_DOMAIN = {
name: "USDT0",
version: "1",
chainId: CHAIN_ID,
verifyingContract: USDT0_STABLE,
} as const;
export const TRANSFER_WITH_AUTHORIZATION_TYPES = {
TransferWithAuthorization: [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "validAfter", type: "uint256" },
{ name: "validBefore", type: "uint256" },
{ name: "nonce", type: "bytes32" },
],
} as const;usdt0Stable.name === "usdt0-stable"
usdt0Stable.intent === "charge"2. 服务器:验证凭据
Method.toServer 将 verify() 接入到 mppx。该函数接收反序列化的凭据(挑战 + 负载),并且必须在无效证明时抛出错误或返回 Receipt。
// src/server-method.ts
import { Method, Receipt } from "mppx";
import { verifyTypedData } from "viem";
import {
usdt0Stable,
EIP712_DOMAIN,
TRANSFER_WITH_AUTHORIZATION_TYPES,
} from "./method";
export const usdt0StableServer = Method.toServer(usdt0Stable, {
async verify({ credential }) {
const { request } = credential.challenge;
const { from, signature } = credential.payload;
const valid = await verifyTypedData({
address: from as `0x${string}`,
domain: EIP712_DOMAIN,
types: TRANSFER_WITH_AUTHORIZATION_TYPES,
primaryType: "TransferWithAuthorization",
message: {
from: from as `0x${string}`,
to: request.payTo as `0x${string}`,
value: BigInt(request.amount),
validAfter: BigInt(request.validAfter),
validBefore: BigInt(request.validBefore),
nonce: request.nonce as `0x${string}`,
},
signature: signature as `0x${string}`,
});
if (!valid) throw new Error("无效的ERC-3009签名");
// Recipt的reference字段将在settle()后被交易哈希填充。
return Receipt.from({
method: usdt0Stable.name,
reference: "pending",
status: "success",
timestamp: new Date().toISOString(),
});
},
});{
method: "usdt0-stable",
reference: "pending",
status: "success",
timestamp: "2026-06-01T12:34:56.000Z"
}3. 结算:提交 transferWithAuthorization
结算有意地与 verify() 分离。在 verify() 返回后,您可以通过适合您运营模型的任何路径提交链上授权。三种选项,按推荐顺序列出。
默认:服务器直接提交
卖家的 EOA 使用签名的授权将 transferWithAuthorization 提交给 USDT0。卖家以 USDT0(Stable 的原生 gas token)支付 gas,因此无需管理单独的 gas token 余额。
// src/settle.ts
import { createWalletClient, http, parseSignature } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { stable } from "viem/chains";
import { USDT0_STABLE } from "./method";
const USDT0_ABI = [
{
name: "transferWithAuthorization",
type: "function",
stateMutability: "nonpayable",
inputs: [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "validAfter", type: "uint256" },
{ name: "validBefore", type: "uint256" },
{ name: "nonce", type: "bytes32" },
{ name: "v", type: "uint8" },
{ name: "r", type: "bytes32" },
{ name: "s", type: "bytes32" },
],
outputs: [],
},
] as const;
const seller = privateKeyToAccount(process.env.SELLER_KEY as `0x${string}`);
const wallet = createWalletClient({
account: seller,
chain: stable,
transport: http("https://rpc.stable.xyz"),
});
export async function settleDirect(credential: {
challenge: { request: any };
payload: { from: string; signature: string };
}): Promise<{ txHash: `0x${string}` }> {
const { request } = credential.challenge;
const { v, r, s } = parseSignature(credential.payload.signature as `0x${string}`);
const txHash = await wallet.writeContract({
address: USDT0_STABLE,
abi: USDT0_ABI,
functionName: "transferWithAuthorization",
args: [
credential.payload.from as `0x${string}`,
request.payTo as `0x${string}`,
BigInt(request.amount),
BigInt(request.validAfter),
BigInt(request.validBefore),
request.nonce as `0x${string}`,
Number(v),
r as `0x${string}`,
s as `0x${string}`,
],
});
return { txHash };
}{ txHash: "0x8f3a1b2c..." }替代方案:通过 Gas 豁免进行结算
使用 Stable 的Gas 豁免以 gasPrice = 0 提交内部交易。卖家仍然签署包装交易,但无需支付 gas 费。需要豁免服务器 API 密钥。
// src/settle-waiver.ts
import { encodeFunctionData } from "viem";
import { USDT0_STABLE } from "./method";
import { USDT0_ABI } from "./settle";
const WAIVER_SERVER = "https://waiver.stable.xyz"; // mainnet endpoint
export async function settleViaWaiver(
credential: { challenge: { request: any }; payload: { from: string; signature: string } },
signedInnerTxHex: `0x${string}`,
): Promise<{ txHash: `0x${string}` }> {
const res = await fetch(`${WAIVER_SERVER}/v1/submit`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.WAIVER_API_KEY}`,
},
body: JSON.stringify({ transactions: [signedInnerTxHex] }),
});
const lines = (await res.text()).trim().split("\n");
const result = JSON.parse(lines[0]);
if (!result.success) throw new Error(`结算失败: ${result.error?.message}`);
return { txHash: result.txHash };
}{ txHash: "0x8f3a1b2c..." }有关如何构建已签名的内部交易(gasPrice: 0,编码的 transferWithAuthorization 调用)然后再发布的信息,请参阅Gas 豁免协议。
替代方案:移交给x402协调器
如果您已经运行了x402协调器集成(Semantic Pay或Heurist),您可以将其复用为结算目标。向 /settle POST 一个 paymentPayload;协调器会提交链上调用。
paymentPayload 的确切形状是 x402 中间件内部的,未在协议层面指定。最简单的路径是使用协调器自己的 SDK 来构建负载,或者坚持使用上述的直接提交路径。协调器不需要理解 MPP;它只看到 transferWithAuthorization 字段。
4. 客户端:签署凭据
Method.toClient 将 createCredential() 接入 mppx。客户端读取挑战,使用代理的 viem 账户签署 EIP-712 授权,并序列化凭据。
// src/client-method.ts
import { Credential, Method } from "mppx";
import { hexToSignature, parseSignature } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import {
usdt0Stable,
EIP712_DOMAIN,
TRANSFER_WITH_AUTHORIZATION_TYPES,
} from "./method";
export function createUsdt0StableClient(privateKey: `0x${string}`) {
const account = privateKeyToAccount(privateKey);
return Method.toClient(usdt0Stable, {
async createCredential({ challenge }) {
const { request } = challenge;
const signature = await account.signTypedData({
domain: EIP712_DOMAIN,
types: TRANSFER_WITH_AUTHORIZATION_TYPES,
primaryType: "TransferWithAuthorization",
message: {
from: account.address,
to: request.payTo as `0x${string}`,
value: BigInt(request.amount),
validAfter: BigInt(request.validAfter),
validBefore: BigInt(request.validBefore),
nonce: request.nonce as `0x${string}`,
},
});
return Credential.serialize({
challenge,
payload: { from: account.address, signature },
});
},
});
}"eyJjaGFsbGVuZ2UiOnsi..." // base64-serialized credential, ~600 bytes5. 将服务器连接起来
使用 mppx 的 Express 中间件来发布挑战、解析传入的 Authorization 头、运行 verify()、调用您的结算函数并发出 Payment-Receipt 头。
// src/server.ts
import express from "express";
import { Mppx } from "mppx/express";
import { randomBytes } from "node:crypto";
import { usdt0StableServer } from "./server-method";
import { settleDirect } from "./settle";
const PAY_TO = process.env.PAY_TO_ADDRESS as `0x${string}`;
const PORT = Number(process.env.PORT ?? 4022);
const mppx = Mppx.create({
secretKey: process.env.MPP_SECRET_KEY!,
methods: [usdt0StableServer],
onVerified: async ({ credential, receipt }) => {
const { txHash } = await settleDirect(credential);
return { ...receipt, reference: txHash };
},
});
const app = express();
app.get(
"/weather",
mppx.charge({
amount: "0.001",
method: "usdt0-stable",
request: {
chainId: 988,
asset: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736",
decimals: 6,
payTo: PAY_TO,
validAfter: 0,
validBefore: Math.floor(Date.now() / 1000) + 300,
nonce: `0x${randomBytes(32).toString("hex")}`,
},
})((_req, res) => {
res.json({ weather: "sunny", temperature: 70 });
}),
);
app.listen(PORT, () => {
console.log(`MPP服务器正在监听 http://localhost:${PORT}`);
});MPP服务器正在监听 http://localhost:40226. 端到端运行流程
启动服务器,确认挑战,运行客户端,并确认结算。
确认挑战
curl -i http://localhost:4022/weatherHTTP/1.1 402 Payment Required
WWW-Authenticate: Payment realm="...", challenges="[{\"method\":\"usdt0-stable\",\"request\":{...}}]"
Content-Type: application/json
{"error":"Payment required"}发送付费请求
// src/client.ts
import { Mppx } from "mppx/client";
import { createUsdt0StableClient } from "./client-method";
const client = Mppx.create({
methods: [createUsdt0StableClient(process.env.BUYER_KEY as `0x${string}`)],
});
const res = await fetch("http://localhost:4022/weather", {
// mppx wraps fetch with the 402 retry loop:
...client.fetchOptions(),
});
console.log(res.status, await res.json());
console.log("Payment-Receipt:", res.headers.get("Payment-Receipt"));npx tsx src/client.ts200 { weather: "sunny", temperature: 70 }
Payment-Receipt: reference="0x8f3a1b2c...", status="success", timestamp="2026-06-01T12:34:56.000Z"在Stablescan上验证
打开 https://stablescan.xyz/tx/0x8f3a1b2c... 并确认 transferWithAuthorization 已结算到您的 PAY_TO 地址。
您做了什么
- 以美元计价的 USDT0 支付,买方无需管理 gas token 余额。
- 在客户端-服务器跳跃中使用了 MPP 的
WWW-Authenticate/Authorization/Payment-Receipt线路格式。 - 在同一 HTTP 请求生命周期内(~700 毫秒区块时间)通过 Stable 上的
transferWithAuthorization进行结算。

