Skip to content

docs: add production patterns for x402 and MPP#14

Open
Eras256 wants to merge 2 commits intostellar:mainfrom
Eras256:feat/production-patterns-x402-mpp
Open

docs: add production patterns for x402 and MPP#14
Eras256 wants to merge 2 commits intostellar:mainfrom
Eras256:feat/production-patterns-x402-mpp

Conversation

@Eras256
Copy link
Copy Markdown

@Eras256 Eras256 commented Apr 11, 2026

Summary

Adds production-ready patterns to x402.md and mpp.md based 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:

  • Multi-route price table: per-endpoint pricing with paymentMiddlewareFromConfig
  • Graceful degradation: safe fallback when STELLAR_RECIPIENT is not configured (prevents crashes in CI/dev)
  • Payment receipt logging: async, non-blocking pattern for tracking revenue

mpp.md additions:

  • Dual-mode server: combining charge + channel in the same Express app with route-level intent selection
  • Per-route pricing: middleware factory pattern for different price tiers per endpoint
  • Recipient key auto-resolution: detects and derives public key when a secret key is accidentally set in container env vars (common Railway/Docker misconfiguration)
  • Discovery endpoint: /info pattern so clients can negotiate available intents

README.md:

  • Added x402/MPP example prompts to the "Example Prompts" section
  • Updated Skill Structure list to include x402.md and mpp.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

  • Verify markdown renders correctly on GitHub
  • Code examples are syntactically valid JS/TS
  • Patterns are consistent with existing x402.md and mpp.md style

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings April 11, 2026 00:46
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.md and skill/mpp.md with 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.md to 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.

Comment thread skill/x402.md
const payTo = process.env.STELLAR_RECIPIENT;

const facilitator = new HTTPFacilitatorClient({
url: process.env.FACILITATOR_URL ?? "https://channels.openzeppelin.com/x402/testnet",
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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"),

Copilot uses AI. Check for mistakes.
Comment thread skill/x402.md
Comment on lines +262 to +265
if (!process.env.STELLAR_RECIPIENT) {
console.warn("[x402] STELLAR_RECIPIENT not set — premium routes open (no payment required)");
x402Middleware = (_req, _res, next) => next();
} else {
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread skill/x402.md
Comment on lines +283 to +286
app.get("/api/premium/signals", (req, res) => {
// Non-blocking payment log
logPayment({
from: req.headers["x-payment-from"] || "unknown",
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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,

Copilot uses AI. Check for mistakes.
Comment thread skill/mpp.md
Comment on lines +339 to +342
if (!chargeMppx) {
res.setHeader("X-MPP-Warning", "MPP not configured");
return next();
}
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
if (!chargeMppx) {
res.setHeader("X-MPP-Warning", "MPP not configured");
return next();
}

Copilot uses AI. Check for mistakes.
Comment thread skill/mpp.md
// Use Mppx.toNodeListener to bridge Web API ↔ Express
const nodeHandler = Mppx.toNodeListener(handler);
await nodeHandler(req, res);
if (res.statusCode !== 402) next();
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
if (res.statusCode !== 402) next();
const isPaymentChallenge = res.statusCode === 402;
if (isPaymentChallenge) return;
if (!res.headersSent && !res.writableEnded) return next();

Copilot uses AI. Check for mistakes.
Comment thread skill/mpp.md
channel: process.env.CHANNEL_CONTRACT,
commitmentKey: process.env.COMMITMENT_PUBKEY,
network: NETWORK,
store: Store.memory(), // use persistent store in production
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants