Add payments to your API
Charge for access to protected resources
Overview
This quickstart demonstrates how to plug MPP into any server framework to accept payments for protected resources. Pick the path that suits you:
- Prompt mode: paste a prompt into your coding agent and build in one prompt
- Framework mode: use
mppxmiddleware for Next.js, Hono, Elysia, or Express - Manual mode: call
mppx/serverdirectly with the Fetch API
Prompt mode
Paste this into your coding agent to set up a server with mppx in one prompt:
Framework mode
Use the framework-specific middleware from mppx to integrate payment into your server. Each middleware handles the 402 Challenge/Credential flow and attaches receipts automatically.
import { Mppx, tempo } from 'mppx/nextjs'
const mppx = Mppx.create({
methods: [tempo({
currency: '0x20c0000000000000000000000000000000000000', // pathUSD on Tempo
recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
})],
})
export const GET =
mppx.charge({ amount: '0.1' })
(() => Response.json({ data: '...' }))import { Hono } from 'hono'
import { Mppx, tempo } from 'mppx/hono'
const app = new Hono()
const mppx = Mppx.create({
methods: [tempo({
currency: '0x20c0000000000000000000000000000000000000',
recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
})],
})
app.get(
'/resource',
mppx.charge({ amount: '0.1' }),
(c) => c.json({ data: '...' }),
)import { Elysia } from 'elysia'
import { Mppx, tempo } from 'mppx/elysia'
const mppx = Mppx.create({
methods: [tempo({
currency: '0x20c0000000000000000000000000000000000000',
recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
})],
})
const app = new Elysia()
.guard(
{ beforeHandle: mppx.charge({ amount: '0.1' }) },
(app) => app.get('/resource', () => ({ data: '...' })),
)import express from 'express'
import { Mppx, tempo } from 'mppx/express'
const app = express()
const mppx = Mppx.create({
methods: [tempo({
currency: '0x20c0000000000000000000000000000000000000',
recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
})],
})
app.get(
'/resource',
mppx.charge({ amount: '0.1' }),
(req, res) => res.json({ data: '...' }))Advanced: manual mode
If you prefer full control over the payment flow, use mppx/server directly with the Fetch API.
import { Mppx, tempo } from 'mppx/server'
const mppx = Mppx.create({
methods: [tempo({
currency: '0x20c0000000000000000000000000000000000000',
recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
})],
})
export async function handler(request: Request) {
const response = await mppx.charge({ amount: '0.1' })(request)
// Payment required: send 402 response with challenge
if (response.status === 402) return response.challenge
// Payment verified: attach receipt and return resource
return response.withReceipt(Response.json({ data: '...' }))
} The intent handler accepts a Fetch API-compatible request object, and returns a Response object.
The Fetch API is compatible with most server frameworks, including: Hono, Deno, Cloudflare Workers, Next.js, Bun, and other Fetch API-compatible frameworks.
Node.js & Express compatibility
If your framework doesn't support the Fetch API (for example, Express or Node.js), you're likely interfacing with the Node.js Request Listener API.
Use the Mppx.toNodeListener helper to transform the handler into a Node.js-compatible listener.
export async function (: , : ) {
const = await .(
.({ : '0.1' })
)(, )
// Payment required: send 402 response with challenge
if (. === 402) return .
// Payment verified: attach receipt and return resource
return .(.({ : '...' }))
} Push & pull modes
Non-zero Tempo charges support two transaction submission modes, determined by the client. Zero-amount charges skip transaction submission entirely and use a proof Credential payload instead.
pullmode (default): the client signs the transaction and sends the serialized transaction to the server. The server broadcasts it and verifies on-chain. This enables the server to sponsor gas fees via afeePayer.pushmode: the client builds, signs, and broadcasts the transaction itself (for example, via a browser wallet). It sends the transaction hash to the server, which verifies the payment by fetching the receipt.
Your server handles all three payload types automatically—no configuration required. The server inspects the credential payload type (proof for zero-amount Challenges, transaction for pull, hash for push) and verifies accordingly.
If you would like to force a specific mode, you can set the mode parameter to 'pull' or 'push'.
import { Mppx, tempo } from 'mppx/server'
const mppx = Mppx.create({
methods: [tempo({
currency: '0x20c0000000000000000000000000000000000000',
mode: 'push',
recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
})],
})The mode parameter only affects non-zero charges. When amount is 0, the client always returns a proof payload and feePayer is irrelevant.
Fee sponsorship
To sponsor gas fees for pull-mode clients, pass a feePayer account to tempo():
import { Mppx, tempo } from 'mppx/server'
import { privateKeyToAccount } from 'viem/accounts'
const mppx = Mppx.create({
methods: [tempo({
currency: '0x20c0000000000000000000000000000000000000',
feePayer: privateKeyToAccount('0x…'),
recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
})],
})It is possible to pass a fee payer service URL instead:
import { Mppx, tempo } from 'mppx/server'
const mppx = Mppx.create({
methods: [tempo({
currency: '0x20c0000000000000000000000000000000000000',
feePayer: 'https://sponsor.example.com',
recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
})],
})When a pull-mode client submits a signed transaction, the server co-signs with the fee payer account (or delegates to the relay) before broadcasting. Push-mode clients pay their own gas, so feePayer is ignored for those requests. Zero-amount proof flows do not create a transaction at all.
Optimistic verification
By default, the server waits for onchain confirmation before returning a Receipt. For lower latency, set waitForConfirmation: false to return immediately after simulation:
const mppx = Mppx.create({
methods: [tempo({
currency: '0x20c0000000000000000000000000000000000000',
recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
waitForConfirmation: false,
})],
})Discovery
After your server is running, add discovery so agents can find your API and its payment terms automatically. The discovery() helper generates a GET /openapi.json endpoint from your route configuration:
import { Hono } from 'hono'
import { Mppx, discovery } from 'mppx/hono'
import { tempo } from 'mppx/server'
const app = new Hono()
const mppx = Mppx.create({
methods: [tempo({
currency: '0x20c0000000000000000000000000000000000000',
recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
})],
})
app.get('/resource', mppx.charge({ amount: '0.1' }), (c) => c.json({ data: '...' }))
discovery(app, mppx, {
auto: true,
info: { title: 'My API', version: '1.0.0' },
})
The generated document advertises each paid route with canonical x-payment-info.offers[] entries. See Discovery for the full document shape, multi-offer examples, and flat-shorthand compatibility notes.
Register your service on MPPScan or the MPP Services directory so agents and registries can discover it.
Testing your server
After your server is running, test it with the mppx CLI:
# Create an account funded with testnet tokens
$ npx mppx account create
# Make a paid request
$ npx mppx <your-server>/resource