docs: add production patterns for x402 and MPP#14
docs: add production patterns for x402 and MPP#14Eras256 wants to merge 2 commits intostellar:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds production-focused documentation patterns for x402 and MPP, plus README discoverability updates, to help developers move from toy examples to real deployments.
Changes:
- Added “Production patterns” sections to
skill/x402.mdandskill/mpp.mdwith multi-route pricing, graceful degradation, and operational tips. - Added new code patterns for dual-mode (charge + channel) MPP servers and per-route middleware factories.
- Updated
README.mdto surface x402/MPP skill docs and example prompts.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| skill/x402.md | Adds route-based pricing, env-safe middleware initialization, and payment logging patterns for x402. |
| skill/mpp.md | Adds dual-mode server pattern, per-route pricing middleware factory, discovery endpoint, and recipient auto-resolution helper. |
| README.md | Includes x402/MPP docs in the skill structure and adds user-facing example prompts. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const payTo = process.env.STELLAR_RECIPIENT; | ||
|
|
||
| const facilitator = new HTTPFacilitatorClient({ | ||
| url: process.env.FACILITATOR_URL ?? "https://channels.openzeppelin.com/x402/testnet", |
There was a problem hiding this comment.
The example can silently misconfigure mainnet: when STELLAR_NETWORK === "mainnet" but FACILITATOR_URL is unset, it still defaults to the testnet facilitator URL. Consider selecting the default facilitator URL based on network (pubnet vs testnet) to avoid a guaranteed network/URL mismatch.
| url: process.env.FACILITATOR_URL ?? "https://channels.openzeppelin.com/x402/testnet", | |
| url: | |
| process.env.FACILITATOR_URL ?? | |
| (network === "stellar:pubnet" | |
| ? "https://channels.openzeppelin.com/x402" | |
| : "https://channels.openzeppelin.com/x402/testnet"), |
| if (!process.env.STELLAR_RECIPIENT) { | ||
| console.warn("[x402] STELLAR_RECIPIENT not set — premium routes open (no payment required)"); | ||
| x402Middleware = (_req, _res, next) => next(); | ||
| } else { |
There was a problem hiding this comment.
This “graceful degradation” pattern can unintentionally disable payments in production if STELLAR_RECIPIENT is missing/misnamed, opening premium routes without auth/payment. Recommend requiring an explicit opt-in flag for the passthrough behavior (e.g., ALLOW_UNPAID_PREMIUM=true) and/or restricting passthrough to non-production environments to reduce the risk of accidentally running unpaid.
| app.get("/api/premium/signals", (req, res) => { | ||
| // Non-blocking payment log | ||
| logPayment({ | ||
| from: req.headers["x-payment-from"] || "unknown", |
There was a problem hiding this comment.
The text says the signed proof is in the X-PAYMENT header, but the example logs x-payment-from and never references X-PAYMENT. To avoid confusion, either update the prose to match the headers actually used, or update the example to log (or hash/store) the X-PAYMENT proof alongside the payer identity.
| app.get("/api/premium/signals", (req, res) => { | |
| // Non-blocking payment log | |
| logPayment({ | |
| from: req.headers["x-payment-from"] || "unknown", | |
| app.get("/api/premium/signals", (req, res) => { | |
| const paymentProof = req.headers["x-payment"]; | |
| const paymentProofHash = paymentProof | |
| ? require("crypto").createHash("sha256").update(paymentProof).digest("hex") | |
| : null; | |
| // Non-blocking payment log | |
| logPayment({ | |
| from: req.headers["x-payment-from"] || "unknown", | |
| paymentProofHash, |
| if (!chargeMppx) { | ||
| res.setHeader("X-MPP-Warning", "MPP not configured"); | ||
| return next(); | ||
| } |
There was a problem hiding this comment.
In this snippet, chargeMppx is always created earlier (const chargeMppx = Mppx.create(...)), so the if (!chargeMppx) branch is effectively dead code and may mislead readers about configuration behavior. Either make chargeMppx conditional on required env vars (so the branch can trigger) or remove this branch from the example.
| if (!chargeMppx) { | |
| res.setHeader("X-MPP-Warning", "MPP not configured"); | |
| return next(); | |
| } |
| // Use Mppx.toNodeListener to bridge Web API ↔ Express | ||
| const nodeHandler = Mppx.toNodeListener(handler); | ||
| await nodeHandler(req, res); | ||
| if (res.statusCode !== 402) next(); |
There was a problem hiding this comment.
Calling next() after nodeHandler(req, res) purely based on statusCode can cause double-handling if the MPP handler already wrote/ended the response (common symptom: “Cannot set headers after they are sent”). Prefer guarding with if (!res.headersSent && !res.writableEnded) (or equivalent) before calling next(), and keep the decision based on whether the handler produced a 402 challenge vs a pass-through.
| if (res.statusCode !== 402) next(); | |
| const isPaymentChallenge = res.statusCode === 402; | |
| if (isPaymentChallenge) return; | |
| if (!res.headersSent && !res.writableEnded) return next(); |
| channel: process.env.CHANNEL_CONTRACT, | ||
| commitmentKey: process.env.COMMITMENT_PUBKEY, | ||
| network: NETWORK, | ||
| store: Store.memory(), // use persistent store in production |
There was a problem hiding this comment.
This appears under “Production patterns” but uses Store.memory(), which is explicitly unsafe in production (you already note that above). To prevent copy/paste footguns, consider swapping the example to a persistent store placeholder (or a minimal example of one), and keep Store.memory() only in clearly labeled dev/test snippets.
Summary
Adds production-ready patterns to
x402.mdandmpp.mdbased on running both protocols in production on Stellar. These patterns address gaps in the current documentation that developers encounter when moving from examples to real deployments.x402.md additions:
paymentMiddlewareFromConfigSTELLAR_RECIPIENTis not configured (prevents crashes in CI/dev)mpp.md additions:
/infopattern so clients can negotiate available intentsREADME.md:
Context
These patterns come from operating x402 + MPP in production on Stellar since before the x402 Foundation launch (April 2, 2026). The auto-resolution pattern in particular addresses a real-world issue we hit in containerized deployments on Railway.
Test plan
🤖 Generated with Claude Code