Use this file to discover all available pages before exploring further.
This guide walks through writing a custom MPP payment method for USDT0 on Stable and serving an MPP-gated endpoint. The buyer signs an ERC-3009transferWithAuthorization, the server validates it through mppx’s verify() hook, and settlement happens in a separate step you control.
An HTTP endpoint that returns 402 Payment Required with an MPP WWW-Authenticate challenge, accepts a signed credential in the Authorization header, verifies it, settles transferWithAuthorization on USDT0, and returns the response with a Payment-Receipt header.
step 1. Client: GET /weather (no Authorization header) Server: 402 Payment Required WWW-Authenticate: Payment realm="...", challenges="[...usdt0-stable charge for $0.001...]"step 2. Client signs an ERC-3009 authorization with their viem accountstep 3. Client: GET /weather + Authorization header containing the serialized credential Server: verify() validates the EIP-712 signature Server: settle() submits transferWithAuthorization on Stable (~700ms block confirmation) Server: 200 OK { weather: "sunny" } Payment-Receipt: reference="0x8f3a...", status="success"step 4. Verify settlement on Stablescan https://stablescan.xyz/tx/0x8f3a...
Method.from() declares the intent and the schemas for the request (Challenge) and the credential payload. Both client and server import this definition.
// src/method.tsimport { 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;
Method.toServer wires verify() into mppx. The function receives the deserialized credential (challenge + payload) and must throw on invalid proofs or return a Receipt.
// src/server-method.tsimport { 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("Invalid ERC-3009 signature"); // The Receipt's reference is filled in with the tx hash after settle(). return Receipt.from({ method: usdt0Stable.name, reference: "pending", status: "success", timestamp: new Date().toISOString(), }); },});
verify() checks the signature but does not check nonce uniqueness or whether the authorization has already been spent. The chain enforces both at submission time: transferWithAuthorization reverts on a used nonce. The settle step turns those reverts into errors the server can surface to the client.
Settlement is intentionally separate from verify(). After verify() returns, you submit the authorization on-chain through whichever path fits your operational model. Three options, in order of recommendation.
The seller’s EOA submits transferWithAuthorization to USDT0 with the signed authorization. The seller pays gas in USDT0 (Stable’s native gas token), so there is no separate gas-token balance to manage.
Use Stable’s Gas Waiver to submit the inner transaction at gasPrice = 0. The seller still signs the wrapping transaction, but pays no gas. Requires a Waiver Server API key.
If you already operate an x402 facilitator integration (Semantic Pay or Heurist), you can reuse it as a settlement target. POST a paymentPayload to /settle; the facilitator submits the on-chain call.The exact paymentPayload shape is x402-middleware-internal and not specified at the wire level. The simplest path is to use the facilitator’s own SDK to build the payload, or stick with the direct-submission path above. The facilitator does not need to speak MPP; it sees only the transferWithAuthorization fields.
Method.toClient wires createCredential() into mppx. The client reads the Challenge, signs the EIP-712 authorization with the agent’s viem account, and serializes the credential.
Use mppx’s Express middleware to issue Challenges, parse incoming Authorization headers, run verify(), call your settle function, and emit the Payment-Receipt header.