Accept pay-as-you-go payments
Use pay-as-you-go sessions when a customer should pay a small amount each time they use an API or service, without sending an on-chain transaction for every request.
This guide uses a simple photo API as the example: the client opens one mppx session, then pays $0.01 each time it asks for a photo. mppx is the library that manages the MPP session; Picsum only supplies sample images for the demo.
How pay-as-you-go sessions work
- Open — Client deposits funds into an on-chain reserve contract, creating a payment channel
- Session — Client signs EIP-712 vouchers with increasing cumulative amounts as service is consumed
- Top up — If the channel runs low, the client deposits additional tokens without closing the channel
- Close — Either party closes the channel, settling the final balance on-chain and refunding unused deposit
The channel-reserve gas snapshot for the credited reopen path is:
| Channel reserve snapshot | T7 gas |
|---|---|
open_new_channel_with_storage_credit | 60,225 |
This is a call-level gas number and excludes separate approval gas. It matters most for session services where the same payer opens, closes or withdraws, and later opens channels again.
Pay-as-you-go server setup
Set up an Mppx session instance
Set up an Mppx instance with the tempo method.
recipientis the address where you receive payments.currencyis the token address for payments (in this case,pathUSD).
import { Mppx, tempo } from 'mppx/server'
const mppx = Mppx.create({
methods: [tempo({
currency: '0x20c0000000000000000000000000000000000000',
recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8',
})],
})Add a pay-as-you-go route
Add payment verification using mppx.session as route middleware. The handler only runs after payment is verified.
import { Mppx, tempo } from 'mppx/nextjs'
const mppx = Mppx.create({
methods: [tempo({
currency: '0x20c0000000000000000000000000000000000000',
recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8',
})],
})
export const GET =
mppx.session({ amount: '0.01', unitType: 'photo' })
(async () => {
const res = await fetch('https://picsum.photos/200/200')
return Response.json({ url: res.url })
})import { Hono } from 'hono'
import { Mppx, tempo } from 'mppx/hono'
const app = new Hono()
const mppx = Mppx.create({
methods: [tempo({
currency: '0x20c0000000000000000000000000000000000000',
recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8',
})],
})
app.get(
'/api/sessions/photo',
mppx.session({ amount: '0.01', unitType: 'photo' }),
async (c) => {
const res = await fetch('https://picsum.photos/200/200')
return c.json({ url: res.url })
},
)import express from 'express'
import { Mppx, tempo } from 'mppx/express'
const app = express()
const mppx = Mppx.create({
methods: [tempo({
currency: '0x20c0000000000000000000000000000000000000',
recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8',
})],
})
app.get(
'/api/sessions/photo',
mppx.session({ amount: '0.01', unitType: 'photo' }),
async (req, res) => {
const response = await fetch('https://picsum.photos/200/200')
res.json({ url: response.url })
},
)import { Mppx, tempo } from 'mppx/server'
const mppx = Mppx.create({
methods: [tempo({
currency: '0x20c0000000000000000000000000000000000000',
recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8',
})],
})
Bun.serve({
async fetch(request) {
const result = await mppx.session({
amount: '0.01',
unitType: 'photo',
})(request)
if (result.status === 402) return result.challenge
const res = await fetch('https://picsum.photos/200/200')
return result.withReceipt(Response.json({ url: res.url }))
},
})Pay-as-you-go client setup
When using sessions from a client, set maxDeposit to enable automatic channel management. This is the maximum amount of tokens the client locks into the payment channel's reserve contract. Any unspent deposit is refunded when the channel closes.
import { Mppx, tempo } from 'mppx/client'
import { privateKeyToAccount } from 'viem/accounts'
const mppx = Mppx.create({
methods: [tempo({
account: privateKeyToAccount('0x...'),
maxDeposit: '1', // Lock up to 1 pathUSD per channel
})],
})
// Each fetch automatically manages the session lifecycle:
// 1st request: opens channel on-chain, sends initial voucher
// 2nd+ requests: sends off-chain vouchers (no on-chain tx)
const res = await fetch('http://localhost:3000/api/sessions/photo')maxDeposit: '1'— Locks up to 1 pathUSD into the payment channel. At $0.01/photo, this covers up to 100 requests before the channel runs out.- The client handles the full session lifecycle automatically: channel open, voucher signing, and retry after
402responses. - If the server sets
suggestedDeposit, the client usesmin(suggestedDeposit, maxDeposit).
Next steps for pay-as-you-go payments
Per-token billing over Server-Sent Events
Framework middleware reference
Complete tempo.session API documentation
Was this helpful?