Integration Guide
Connect an mppx server to Naven MPP Relay.
Naven MPP Relay is called by your backend, not by a browser or the paying client. Your backend owns challenge issuance and uses the relay only for validation and settlement. The public relay does not require a Naven account or API key.
Prerequisites
- A receiving wallet on Robinhood Chain
- USDG payment terms
- A server using
mppxwith a private challenge secret
Install the SDK in your server project:
bun add mppx@0.9.2Configure customer-owned server variables. Never expose your challenge secret to client code:
NAVEN_MPP_RELAY_URL=https://mpp.naven.network
MPP_SECRET_KEY=at-least-32-random-bytes
MERCHANT_RECIPIENT=0xYourReceivingWalletMPP_SECRET_KEY belongs only to your application and is not shared with Naven.
Configure the payment method
Create an evm/charge server method whose lifecycle hooks call the relay:
import { Method } from "mppx";
import { Methods } from "mppx/evm";
import { Mppx } from "mppx/server";
const relayUrl = process.env.NAVEN_MPP_RELAY_URL!;
async function callRelay(
operation: "validate" | "broadcast",
credential: any,
request: any,
) {
const response = await fetch(`${relayUrl}/v1/mpp/${operation}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(operation === "broadcast"
? {
"Idempotency-Key":
`mpp_${credential.payload.from}_${credential.payload.nonce}`,
}
: {}),
},
body: JSON.stringify({ credential, request }),
});
const result = await response.json();
if (!response.ok || result.success !== true) {
throw new Error(result.error?.message ?? "MPP relay rejected the payment");
}
return result;
}
const defaults = {
chainId: 4663,
currency: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168",
credentialTypes: ["authorization"] as ["authorization"],
decimals: 6,
recipient: process.env.MERCHANT_RECIPIENT as `0x${string}`,
};
const evmCharge = Method.toServer<
typeof Methods.charge,
typeof defaults
>(Methods.charge, {
defaults,
async validate({ credential }) {
const result = await callRelay(
"validate",
credential,
credential.challenge.request,
);
return {
challenge: credential.challenge,
credential,
details: result.details,
intent: "charge",
method: "evm",
request: credential.challenge.request,
...(credential.source ? { source: credential.source } : {}),
};
},
async broadcast({ credential }) {
return (
await callRelay("broadcast", credential, credential.challenge.request)
).receipt;
},
});
export const mppx = Mppx.create({
methods: [evmCharge],
secretKey: process.env.MPP_SECRET_KEY!,
});The deterministic idempotency key makes a network retry refer to the same payment. Never reuse one key for a different credential.
Protect a route
Use the configured method in your HTTP handler:
export async function handle(request: Request) {
const payment = await mppx.evm.charge({
amount: "0.01",
description: "Generate one report",
})(request);
if (payment.status === 402) return payment.challenge;
const report = await generateReport();
return payment.withReceipt(Response.json({ report }));
}The route price uses display units, so 0.01 USDG becomes the atomic amount
10000 in the challenge. mppx verifies your challenge HMAC and route binding
before it calls the relay. The paid result is returned only after validation and
broadcast succeed.
Make the purchased operation idempotent where possible. Settlement occurs before your application produces the response, so your application remains responsible for retry and fulfillment behavior if its business operation fails afterward.
Production checklist
- Keep
MPP_SECRET_KEYon your server. - Generate a unique challenge for the protected route and exact payment terms.
- Use a stable, credential-specific
Idempotency-Keyfor broadcast retries. - Call broadcast before releasing paid content or recording the order as paid.
- Attach the returned receipt with
payment.withReceipt(...). - Continue using the Naven x402 facilitator unchanged for any existing x402 routes.
See the API Reference for request and response fields.