← Back
XPR Networkmppx-xpr-networkdocs
HTTP 402 — Machine Payments for XPR Network

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.

no gas fees<1s finality@account namesstreaming paymentsIETF 402 spec

⚡ Quick Start

Install
npm install mppx mppx-xpr-network

Minimal server (charge)

Server — 6 lines
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

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

1. Request
client → GET /api
2. 402
← WWW-Authenticate: Payment
3. Pay
transfer XPR on-chain
4. Credential
→ Authorization: Payment
5. Verify
Hyperion confirms tx
6. 200
← content + Payment-Receipt

Server setup

Next.js route handler
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

OptionTypeDescription
recipientstringXPR account name to receive payments (e.g. "charliebot")
hyperionstring?Preferred Hyperion endpoint. Falls back to built-in pool if unreachable.
amountstringPer-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.

Built-in Hyperion endpoints (tried in order)
// 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

1. Request
client → GET /api/stream
2. 402
← Payment challenge
3. Deposit
transfer XPR to vest
4. startvest
open vest stream
5. Stream
server claims per-unit
6. stopvest
close + refund remainder

Server setup

Next.js streaming route handler
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

OptionTypeDescription
recipientstringXPR account to receive streamed payments
rpcstringXPR Network RPC endpoint for vest contract calls
maxAmountstringMaximum deposit: "10.0000 XPR". Unused portion is refunded on stop.
durationnumberVest duration in seconds before auto-expiry

Vest contract actions

depositclient

Client transfers XPR to the vest contract as collateral for the session.

claimserver

Server claims earned XPR as content is delivered. Each claim is a separate on-chain action.

stopserver or client

Ends the vest early. Unclaimed balance is returned to the depositor immediately.

📖 API Reference

Mppx.create(options)

Creates the main MPP server instance.

OptionTypeDescription
methodsPluginMethod[]Array of payment method plugins (e.g. xpr.charge(), xpr.session())
secretKeystringSecret used to sign and verify challenge tokens. Keep server-side only.
realmstring?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 header

mppx.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 response

Server 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"
Decoded challenge payload contains: id, method, intent, realm, request, expires
Authorization: Paymentretry request

Client 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"
Decoded credential contains: challenge (original) + payload.txId
Payment-Receipt200 response

Server 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"