Accept streamed payments
Build a payment-gated API that streams content word-by-word and charges $0.001 per word using mppx sessions with Server-Sent Events (SSE).
How streamed payment sessions work
- Open — Client deposits funds into an on-chain reserve contract, creating a payment channel
- Stream — Server streams SSE events, calling
stream.charge()per token to increment the voucher amount - Top up — If the channel runs low mid-stream, the server emits a
payment-need-voucherevent and the client automatically signs a new voucher - Close — Either party closes the channel, settling the final balance on-chain and refunding unused deposit
Streamed payment server setup
Set up an Mppx streaming instance
Set up an Mppx instance with sse: true to enable SSE support on the session method.
import { Mppx, tempo } from 'mppx/server'
const mppx = Mppx.create({
methods: [tempo({
currency: '0x20c0000000000000000000000000000000000000',
recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8',
sse: true,
})],
})Add a streamed payment route
The handler returns an async generator — each yielded value becomes one SSE event and is charged one tick ($0.001). If the channel balance runs out mid-stream, the server emits event: payment-need-voucher and pauses until the client sends a new voucher.
import { Mppx, tempo } from 'mppx/nextjs'
const mppx = Mppx.create({
methods: [tempo({
currency: '0x20c0000000000000000000000000000000000000',
recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8',
sse: true,
})],
})
const poem = {
title: 'The Road Not Taken',
author: 'Robert Frost',
lines: [
'Two roads diverged in a yellow wood,',
'And sorry I could not travel both',
'And be one traveler, long I stood',
'And looked down one as far as I could',
'To where it bent in the undergrowth;',
],
}
export const GET =
mppx.session({ amount: '0.001', unitType: 'word' })
(async () => {
const words = poem.lines.flatMap((line) => [...line.split(' '), '\\n'])
return async function* (stream) {
yield JSON.stringify({ title: poem.title, author: poem.author })
for (const word of words) {
await stream.charge()
yield word
}
}
})import { Hono } from 'hono'
import { Mppx, tempo } from 'mppx/hono'
const app = new Hono()
const mppx = Mppx.create({
methods: [tempo({
currency: '0x20c0000000000000000000000000000000000000',
recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8',
sse: true,
})],
})
const poem = {
title: 'The Road Not Taken',
author: 'Robert Frost',
lines: [
'Two roads diverged in a yellow wood,',
'And sorry I could not travel both',
'And be one traveler, long I stood',
'And looked down one as far as I could',
'To where it bent in the undergrowth;',
],
}
app.get(
'/api/sessions/poem',
mppx.session({ amount: '0.001', unitType: 'word' }),
async (c) => {
const words = poem.lines.flatMap((line) => [...line.split(' '), '\\n'])
return async function* (stream) {
yield JSON.stringify({ title: poem.title, author: poem.author })
for (const word of words) {
await stream.charge()
yield word
}
}
},
)import express from 'express'
import { Mppx, tempo } from 'mppx/express'
const app = express()
const mppx = Mppx.create({
methods: [tempo({
currency: '0x20c0000000000000000000000000000000000000',
recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8',
sse: true,
})],
})
const poem = {
title: 'The Road Not Taken',
author: 'Robert Frost',
lines: [
'Two roads diverged in a yellow wood,',
'And sorry I could not travel both',
'And be one traveler, long I stood',
'And looked down one as far as I could',
'To where it bent in the undergrowth;',
],
}
app.get(
'/api/sessions/poem',
mppx.session({ amount: '0.001', unitType: 'word' }),
async (req, res) => {
const words = poem.lines.flatMap((line) => [...line.split(' '), '\\n'])
return async function* (stream) {
yield JSON.stringify({ title: poem.title, author: poem.author })
for (const word of words) {
await stream.charge()
yield word
}
}
},
)import { Mppx, tempo } from 'mppx/server'
const mppx = Mppx.create({
methods: [tempo({
currency: '0x20c0000000000000000000000000000000000000',
recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8',
sse: true,
})],
})
const poem = {
title: 'The Road Not Taken',
author: 'Robert Frost',
lines: [
'Two roads diverged in a yellow wood,',
'And sorry I could not travel both',
'And be one traveler, long I stood',
'And looked down one as far as I could',
'To where it bent in the undergrowth;',
],
}
Bun.serve({
async fetch(request) {
const result = await mppx.session({
amount: '0.001',
unitType: 'word',
})(request)
if (result.status === 402) return result.challenge
const words = poem.lines.flatMap((line) => [...line.split(' '), '\\n'])
return result.withReceipt(async function* (stream) {
yield JSON.stringify({ title: poem.title, author: poem.author })
for (const word of words) {
await stream.charge()
yield word
}
})
},
})Streamed payment client setup
Use tempo.session() from mppx/client to create a session manager. The .sse() method connects to the SSE endpoint and handles voucher renewal automatically — if the server requests a new voucher mid-stream, the client signs and sends one without interrupting the stream.
import { tempo } from 'mppx/client'
import { privateKeyToAccount } from 'viem/accounts'
const session = tempo.session({
account: privateKeyToAccount('0x...'),
maxDeposit: '1', // Lock up to 1 pathUSD per channel
})
// .sse() returns an async iterable of SSE data payloads
const stream = await session.sse('http://localhost:3000/api/sessions/poem')
for await (const word of stream) {
process.stdout.write(word + ' ')
}tempo.session()— Creates a session manager that handles the full channel lifecycle: open, voucher signing, and close..sse()— Connects to an SSE endpoint. Automatically sends new vouchers when the server emitspayment-need-voucherevents.maxDeposit: '1'— Locks up to 1 pathUSD. At $0.001/word, this covers ~1,000 words before the channel needs a top-up.
Next steps for streamed payments
Charge per request with on-chain settlement
Session-based billing without streaming
Complete tempo.session API documentation
Was this helpful?