mppx-xpr-network
An MPP plugin that brings HTTP 402 machine payments to XPR Network. Zero gas fees. Sub-second finality. Human-readable account names.
⚡ Quick Start
npm install mppx mppx-xpr-networkMinimal server (charge)
import { Mppx } from 'mppx/server'
import { xpr } from 'mppx-xpr-network'
const mppx = Mppx.create({
methods: [xpr.charge({ recipient: 'yourname' })],
secretKey: process.env.MPP_SECRET_KEY,
})
// In your route handler:
const result = await mppx.xpr.charge({ amount: '1.0000 XPR' })(request)
if (result.status === 402) return result.challenge
return result.withReceipt(Response.json({ hello: 'world' }))Minimal client
import { MppxClient } from 'mppx/client'
import { xprClientMethod } from 'mppx-xpr-network/client'
const client = new MppxClient({ methods: [xprClientMethod()] })
// Call a paid endpoint — client handles 402 automatically
const response = await client.fetch('https://your-api.com/api/joke', {
onChallenge: async (challenge) => {
// Sign with WebAuth or any XPR wallet
const txId = await wallet.transfer({
to: challenge.request.methodDetails?.recipient,
amount: challenge.request.amount,
})
return { txId }
},
})⚡ Charges — xpr.charge()
One-time payments verified on-chain via Hyperion history nodes. The client sends XPR, attaches the transaction ID as a credential, and the server verifies the transfer happened before returning content. Simple, instant, non-refundable.
Flow
Server setup
import { Mppx } from 'mppx/server'
import { xpr } from 'mppx-xpr-network'
const mppx = Mppx.create({
methods: [
xpr.charge({
methodDetails: { recipient: 'charliebot' }, // XPR account name in methodDetails
hyperion: 'https://proton.eosusa.io', // optional: preferred node
// amount is set per-request (see below)
}),
],
secretKey: process.env.MPP_SECRET_KEY!, // signs challenges
})
export async function GET(request: Request) {
const result = await mppx.xpr.charge({
amount: '1.0000 XPR', // per-request amount
})(request)
if (result.status === 402) return result.challenge
return result.withReceipt(
Response.json({ joke: 'Why did the blockchain...' })
)
}Configuration options
| Option | Type | Description |
|---|---|---|
| recipient | string | XPR account name to receive payments (e.g. "charliebot") |
| hyperion | string? | Preferred Hyperion endpoint. Falls back to built-in pool if unreachable. |
| amount | string | Per-request amount in XPR format: "1.0000 XPR" |
Hyperion verification — multi-node fallback
The plugin ships with 5 built-in Hyperion endpoints and tries them in order. If the preferred node is unreachable or the transaction isn't found yet, it automatically falls back to the next node. This makes verification resilient to node downtime without any configuration.
// Bundled fallback pool — no config needed
const HYPERION_ENDPOINTS = [
'https://proton.eosusa.io',
'https://proton.cryptolions.io',
'https://hyperion.protonchain.com',
'https://api.protonnz.com',
'https://proton.eosphere.io',
]📡 Sessions — xpr.session()
Streaming payments via XPR Network's vest contract. The client deposits a maximum amount upfront. The server claims what it uses. When the session ends (or the client stops early), the unclaimed balance is refunded automatically. Zero gas fees on open, every claim, and close.
Flow
Server setup
import { Mppx } from 'mppx/server'
import { xpr } from 'mppx-xpr-network'
const mppx = Mppx.create({
methods: [
xpr.session({
recipient: 'charliebot',
rpc: 'https://api.protonnz.com',
// duration and maxAmount set per-request
}),
],
secretKey: process.env.MPP_SECRET_KEY!,
})
export async function GET(request: Request) {
const result = await mppx.xpr.session({
maxAmount: '10.0000 XPR', // max deposit (refund remainder on stop)
duration: 120, // seconds the vest runs for
recipient: 'charliebot',
})(request)
if (result.status === 402) return result.challenge
// Stream content — each chunk claims from the vest
const stream = new ReadableStream({
async start(controller) {
for (const fact of facts) {
controller.enqueue(encoder.encode(fact))
await result.claim('1.0000 XPR') // claim per unit delivered
await sleep(1000)
}
await result.stop() // refund unused balance
controller.close()
},
})
return result.withReceipt(
new Response(stream, { headers: { 'Content-Type': 'text/event-stream' } })
)
}Configuration options
| Option | Type | Description |
|---|---|---|
| recipient | string | XPR account to receive streamed payments |
| rpc | string | XPR Network RPC endpoint for vest contract calls |
| maxAmount | string | Maximum deposit: "10.0000 XPR". Unused portion is refunded on stop. |
| duration | number | Vest duration in seconds before auto-expiry |
Vest contract actions
depositclientClient transfers XPR to the vest contract as collateral for the session.
claimserverServer claims earned XPR as content is delivered. Each claim is a separate on-chain action.
stopserver or clientEnds the vest early. Unclaimed balance is returned to the depositor immediately.
📖 API Reference
Mppx.create(options)
Creates the main MPP server instance.
| Option | Type | Description |
|---|---|---|
| methods | PluginMethod[] | Array of payment method plugins (e.g. xpr.charge(), xpr.session()) |
| secretKey | string | Secret used to sign and verify challenge tokens. Keep server-side only. |
| realm | string? | Optional realm for the WWW-Authenticate header. Defaults to request origin. |
mppx.xpr.charge(options)
Returns a request handler factory. Call it with the incoming request to get back either a 402 challenge or a verified result object.
// Returns a handler function
const handler = mppx.xpr.charge({ amount: '1.0000 XPR' })
// Call with the request
const result = await handler(request)
// result.status === 402 — needs payment
if (result.status === 402) return result.challenge // Response with WWW-Authenticate
// result.status === 200 — payment verified
return result.withReceipt(Response.json({ data: '...' })) // adds Payment-Receipt headermppx.xpr.session(options)
Like charge, but returns a streaming session handle with claim/stop methods.
const result = await mppx.xpr.session({
maxAmount: '10.0000 XPR',
duration: 120,
recipient: 'charliebot',
})(request)
if (result.status === 402) return result.challenge
await result.claim('1.0000 XPR') // claim earned amount mid-stream
await result.stop() // stop vest, refund remainder
return result.withReceipt(response)handlePaidRequest(mppx, handler)(request)
Convenience wrapper that automatically returns the 402 challenge if payment is needed, otherwise calls your handler with the verified result. Reduces boilerplate.
import { handlePaidRequest } from 'mppx/server'
export const GET = handlePaidRequest(
mppx.xpr.charge({ amount: '1.0000 XPR' }),
async (result) => {
return result.withReceipt(Response.json({ data: 'premium content' }))
}
)enrichChallengeResponse(response, challenge)
Adds payment metadata to an existing Response object. Useful when you want to construct the response yourself and attach the challenge or receipt headers manually.
import { enrichChallengeResponse } from 'mppx/server'
const base = new Response(null, { status: 402 })
return enrichChallengeResponse(base, challenge)
// → adds WWW-Authenticate: Payment ... header📡 Headers — IETF Spec
MPP uses the IETF Payment Authentication draft headers. Three headers carry the full challenge-response-receipt lifecycle.
WWW-Authenticate: Payment402 responseServer sends this on a 402 response. Contains a base64url-encoded challenge with the payment requirements: method, amount, recipient, expiry, and a server-signed token.
HTTP/1.1 402 Payment Required
WWW-Authenticate: Payment challenge="eyJpZCI6ImFiYzEyMyIsIm1ldGhvZCI6InhwciIsImludGVudCI6ImNoYXJnZSIsInJlcXVlc3QiOnsiYW1vdW50IjoiMS4wMDAwIFhQUiIsInJlY2lwaWVudCI6ImNoYXJsaWVib3QifX0"id, method, intent, realm, request, expiresAuthorization: Paymentretry requestClient sends this when retrying after payment. Contains the original challenge plus a payload with the transaction proof (tx ID from the XPR chain).
GET /api/joke HTTP/1.1
Authorization: Payment credential="eyJjaGFsbGVuZ2UiOnsuLi59LCJwYXlsb2FkIjp7InR4SWQiOiJhYmMxMjMuLi4ifX0"challenge (original) + payload.txIdPayment-Receipt200 responseServer attaches this to the successful 200 response. Contains a server-signed receipt that the client can use as proof of payment. Added automatically via result.withReceipt().
HTTP/1.1 200 OK
Payment-Receipt: receipt="eyJpZCI6ImFiYzEyMyIsInR4SWQiOiIuLi4iLCJhbW91bnQiOiIxLjAwMDAgWFBSIiwic2lnbmF0dXJlIjoiLi4uIn0"🔗 Links
The XPR Network plugin — source code, issues, contributions welcome
https://github.com/charliebot87/mpp-xpr
Source code for this playground app
https://github.com/charliebot87/x402-demo
npm package — install and version history
https://www.npmjs.com/package/mppx-xpr-network
Machine Payment Protocol — chain-agnostic open standard
https://mpp.dev
The original HTTP 402 payment spec by Coinbase + Cloudflare
https://x402.org
Zero-fee blockchain with WebAuth login and sub-second finality
https://xprnetwork.org