> **Can't find what you're looking for?** Use `search_docs` on the docs MCP server at `https://mpp.dev/api/mcp` to find what you need.

# Bill usage with sessions \[Pay per request or per token with off-chain vouchers]

Use sessions when a buyer makes many requests or streams tokens: the buyer reserves funds once, then pays for each request or token with an off-chain voucher, with near-zero latency and per-request fees.

For a single paid request, use [Charge](/guides/one-time-payments). To bill on a schedule, use [Subscription](/guides/subscription-payments).

* **[Bill per request](/guides/usage-based#per-request)**: charge a fixed amount for each API call.
* **[Bill per token (streaming)](/guides/usage-based#streaming)**: charge per token as content streams over Server-Sent Events (SSE).

## Choose a signing account

### Direct

Create a server-only wallet.ts module, then import account wherever an example creates a local signing account.

```ts
import { privateKeyToAccount } from 'viem/accounts'

export const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)
```

### Privy

Create an EVM wallet in Privy, fund it with the required currency on this page's network, and keep PRIVY\_APP\_SECRET server-side.

```bash
pnpm add @privy-io/node
```

Create a server-only privy.ts module, then import its account wherever an example configures account or feePayer.

```ts
import { PrivyClient } from '@privy-io/node'
import { createViemAccount } from '@privy-io/node/viem'

const privy = new PrivyClient({
  appId: process.env.PRIVY_APP_ID!,
  appSecret: process.env.PRIVY_APP_SECRET!,
})

export const account = createViemAccount(privy, {
  address: process.env.PRIVY_WALLET_ADDRESS as `0x${string}`,
  walletId: process.env.PRIVY_WALLET_ID!,
})
```

createViemAccount delegates signatures to the Privy wallet, so it replaces any local viem account in the examples on this page.

## Per request

Build a payment-gated photo gallery API that charges $0.01 per photo.
The server returns random photos from [Picsum](https://picsum.photos) behind a paywall, but you could generate images with an AI model instead, such as [OpenAI Image Generation](https://developers.openai.com/api/reference/resources/images).

### Try it

Pick a demo, then click **Run demo** to create a wallet, fund it, and pay per photo or per streamed word.

```text
GET /api/sessions/photo

402 Payment Required

Open a Tempo payment session and pay $0.01 for each photo.

GET /api/sessions/poem

402 Payment Required

Open a Tempo payment session and pay for streamed output.
```

### Per-request prompt mode

Paste this into your coding agent to build the photo gallery in one prompt:

```text
Use https://mpp.dev/guides/usage-based.md as reference.
Add mppx to my app with a payment-gated gallery endpoint
that charges $0.01 per photo using the Tempo session payment method with
OUSD. When payment is verified, fetch a random photo from
https://picsum.photos/200/200 and return the URL as JSON.
```

### Per-request manual mode

Select your framework to follow a step-by-step guide. If your framework isn't listed, choose **Other** for a generic [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) approach compatible with most TypeScript server frameworks.

### Next.js

::::steps
#### Install `mppx`

:::code-group
```bash [npm]
$ npm install mppx viem
```

```bash [pnpm]
$ pnpm add mppx viem
```

```bash [bun]
$ bun add mppx viem
```
:::

#### Set up `Mppx` instance

Set up an `Mppx` instance with the current `tempo.session` method.

* `account` signs server-side session settlement and close transactions.
* `currencies: [address]` accepts only OUSD when configuring a Tempo factory. Per-route overrides use singular `currency`.
* `store` keeps v2 Session channel state; use a durable atomic store in production.

```ts [app/api/sessions/photo/route.ts]
import { Store } from 'mppx'
import { Mppx, tempo } from 'mppx/nextjs'
import { privateKeyToAccount } from 'viem/accounts'

const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef')

const mppx = Mppx.create({
  methods: [
    tempo.session({
      account,
      chainId: 42431,
      currencies: ['0x20c0000000000000000000006a37DA5C996874BE'], // OUSD on Tempo
      store: Store.memory(),
    }),
  ],
})
```

#### Create the `/api/sessions/photo` route

Create the gallery route. This route is **currently unpaid**.

```ts [app/api/sessions/photo/route.ts]
import { Store } from 'mppx'
import { Mppx, tempo } from 'mppx/nextjs'
import { privateKeyToAccount } from 'viem/accounts'

const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef')

const mppx = Mppx.create({
  methods: [
    tempo.session({
      account,
      chainId: 42431,
      currencies: ['0x20c0000000000000000000006a37DA5C996874BE'], // OUSD on Tempo
      store: Store.memory(),
    }),
  ],
})

// [!code focus:start]
export const GET = async () => {
  const res = await fetch('https://picsum.photos/200/200')
  return Response.json({ url: res.url })
}
// [!code focus:end]
```

#### Add `.session` to the route handler

Add payment verification using `mppx.session` as route middleware.
The handler runs only after payment is verified.

```ts [app/api/sessions/photo/route.ts]
import { Store } from 'mppx'
import { Mppx, tempo } from 'mppx/nextjs'
import { privateKeyToAccount } from 'viem/accounts'

const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef')

const mppx = Mppx.create({
  methods: [
    tempo.session({
      account,
      chainId: 42431,
      currencies: ['0x20c0000000000000000000006a37DA5C996874BE'], // OUSD on Tempo
      store: Store.memory(),
    }),
  ],
})

// [!code focus:start]
export const GET =
  mppx.session({ amount: '0.01', unitType: 'photo' }) // [!code ++]
  (async () => {
    const res = await fetch('https://picsum.photos/200/200')
    return Response.json({ url: res.url })
  })
// [!code focus:end]
```

#### Test via the `mppx` CLI

```bash [test.sh]
# Create a testnet account and fund it with test OUSD
$ npx mppx account create --network testnet
$ npx mppx account fund --network testnet

# Make a paid request
$ npx mppx http://localhost:3000/api/sessions/photo 
```
::::

### Hono

::::steps
#### Install `mppx` and `hono`

:::code-group
```bash [npm]
$ npm install mppx hono viem
```

```bash [pnpm]
$ pnpm add mppx hono viem
```

```bash [bun]
$ bun add mppx hono viem
```
:::

#### Set up `Mppx` instance

Set up an `Mppx` instance with the current `tempo.session` method.

* `account` signs server-side session settlement and close transactions.
* `currencies: [address]` accepts only OUSD when configuring a Tempo factory. Per-route overrides use singular `currency`.
* `store` keeps v2 Session channel state; use a durable atomic store in production.

```ts [server.ts]
import { Store } from 'mppx'
import { Hono } from 'hono'
import { Mppx, tempo } from 'mppx/hono'
import { privateKeyToAccount } from 'viem/accounts'

const app = new Hono()

const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef')

const mppx = Mppx.create({
  methods: [
    tempo.session({
      account,
      chainId: 42431,
      currencies: ['0x20c0000000000000000000006a37DA5C996874BE'], // OUSD on Tempo
      store: Store.memory(),
    }),
  ],
})
```

#### Create the `/api/sessions/photo` route

Create the gallery route. This route is **currently unpaid**.

```ts [server.ts]
import { Store } from 'mppx'
import { Hono } from 'hono'
import { Mppx, tempo } from 'mppx/hono'
import { privateKeyToAccount } from 'viem/accounts'

const app = new Hono()

const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef')

const mppx = Mppx.create({
  methods: [
    tempo.session({
      account,
      chainId: 42431,
      currencies: ['0x20c0000000000000000000006a37DA5C996874BE'], // OUSD on Tempo
      store: Store.memory(),
    }),
  ],
})

// [!code focus:start]
app.get('/api/sessions/photo', async (c) => {
  const res = await fetch('https://picsum.photos/200/200')
  return c.json({ url: res.url })
})
// [!code focus:end]
```

#### Add `.session` to the route handler

Add payment verification using `mppx.session` as route middleware.
The handler runs only after payment is verified.

```ts [server.ts]
import { Store } from 'mppx'
import { Hono } from 'hono'
import { Mppx, tempo } from 'mppx/hono'
import { privateKeyToAccount } from 'viem/accounts'

const app = new Hono()

const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef')

const mppx = Mppx.create({
  methods: [
    tempo.session({
      account,
      chainId: 42431,
      currencies: ['0x20c0000000000000000000006a37DA5C996874BE'], // OUSD on Tempo
      store: Store.memory(),
    }),
  ],
})

// [!code focus:start]
app.get(
  '/api/sessions/photo',
  mppx.session({ amount: '0.01', unitType: 'photo' }), // [!code ++]
  async (c) => {
    const res = await fetch('https://picsum.photos/200/200')
    return c.json({ url: res.url })
  },
)
// [!code focus:end]
```

#### Test via the `mppx` CLI

```bash [test.sh]
# Create a testnet account and fund it with test OUSD
$ npx mppx account create --network testnet
$ npx mppx account fund --network testnet

# Make a paid request
$ npx mppx http://localhost:3000/api/sessions/photo 
```
::::

### Workers

::::steps
#### Install `mppx`

:::code-group
```bash [npm]
$ npm install mppx viem
```

```bash [pnpm]
$ pnpm add mppx viem
```

```bash [bun]
$ bun add mppx viem
```
:::

#### Set up `Mppx` instance

Set up an `Mppx` instance with the current `tempo.session` method.

* `account` signs server-side session settlement and close transactions.
* `currencies: [address]` accepts only OUSD when configuring a Tempo factory. Per-route overrides use singular `currency`.
* `store` keeps v2 Session channel state; use a durable atomic store in production.

```ts [src/index.ts]
import { Mppx, Store, tempo } from 'mppx/server'
import { privateKeyToAccount } from 'viem/accounts'

const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef')

const mppx = Mppx.create({
  methods: [
    tempo.session({
      account,
      chainId: 42431,
      currencies: ['0x20c0000000000000000000006a37DA5C996874BE'], // OUSD on Tempo
      store: Store.memory(),
    }),
  ],
})
```

#### Create the gallery route

Create the gallery route. This route is **currently unpaid**.

```ts [src/index.ts]
import { Mppx, Store, tempo } from 'mppx/server'
import { privateKeyToAccount } from 'viem/accounts'

const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef')

const mppx = Mppx.create({
  methods: [
    tempo.session({
      account,
      chainId: 42431,
      currencies: ['0x20c0000000000000000000006a37DA5C996874BE'], // OUSD on Tempo
      store: Store.memory(),
    }),
  ],
})

// [!code focus:start]
export default {
  async fetch(request: Request) {
    const res = await fetch('https://picsum.photos/200/200')
    return Response.json({ url: res.url })
  },
}
// [!code focus:end]
```

#### Add `.session` to the route handler

Add payment verification using `mppx.session`. If the status is `402`, return the Challenge. Otherwise, fetch the photo and attach a Receipt to the response.

```ts [src/index.ts]
import { Mppx, Store, tempo } from 'mppx/server'
import { privateKeyToAccount } from 'viem/accounts'

const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef')

const mppx = Mppx.create({
  methods: [
    tempo.session({
      account,
      chainId: 42431,
      currencies: ['0x20c0000000000000000000006a37DA5C996874BE'], // OUSD on Tempo
      store: Store.memory(),
    }),
  ],
})

// [!code focus:start]
export default {
  async fetch(request: Request) {
    const result = await mppx.session({ // [!code ++]
      amount: '0.01', // [!code ++]
      unitType: 'photo', // [!code ++]
    })(request) // [!code ++]

    if (result.status === 402) return result.challenge // [!code ++]

    const res = await fetch('https://picsum.photos/200/200')
    return result.withReceipt(Response.json({ url: res.url })) // [!code ++]
  },
}
// [!code focus:end]
```

#### Test via the `mppx` CLI

```bash [test.sh]
# Create a testnet account and fund it with test OUSD
$ npx mppx account create --network testnet
$ npx mppx account fund --network testnet

# Make a paid request
$ npx mppx http://localhost:8787 
```
::::

### Express

::::steps
#### Install `mppx` and `express`

:::code-group
```bash [npm]
$ npm install mppx express viem
```

```bash [pnpm]
$ pnpm add mppx express viem
```

```bash [bun]
$ bun add mppx express viem
```
:::

#### Set up `Mppx` instance

Set up an `Mppx` instance with the current `tempo.session` method.

* `account` signs server-side session settlement and close transactions.
* `currencies: [address]` accepts only OUSD when configuring a Tempo factory. Per-route overrides use singular `currency`.
* `store` keeps v2 Session channel state; use a durable atomic store in production.

```ts [server.ts]
import { Store } from 'mppx'
import express from 'express'
import { Mppx, tempo } from 'mppx/express'
import { privateKeyToAccount } from 'viem/accounts'

const app = express()

const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef')

const mppx = Mppx.create({
  methods: [
    tempo.session({
      account,
      chainId: 42431,
      currencies: ['0x20c0000000000000000000006a37DA5C996874BE'], // OUSD on Tempo
      store: Store.memory(),
    }),
  ],
})
```

#### Create the `/api/sessions/photo` route

Create the gallery route. This route is **currently unpaid**.

```ts [server.ts]
import { Store } from 'mppx'
import express from 'express'
import { Mppx, tempo } from 'mppx/express'
import { privateKeyToAccount } from 'viem/accounts'

const app = express()

const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef')

const mppx = Mppx.create({
  methods: [
    tempo.session({
      account,
      chainId: 42431,
      currencies: ['0x20c0000000000000000000006a37DA5C996874BE'], // OUSD on Tempo
      store: Store.memory(),
    }),
  ],
})

// [!code focus:start]
app.get('/api/sessions/photo', async (req, res) => {
  const response = await fetch('https://picsum.photos/200/200')
  res.json({ url: response.url })
})
// [!code focus:end]
```

#### Add `.session` to the route handler

Add payment verification using `mppx.session` as route middleware.
The handler runs only after payment is verified.

```ts [server.ts]
import { Store } from 'mppx'
import express from 'express'
import { Mppx, tempo } from 'mppx/express'
import { privateKeyToAccount } from 'viem/accounts'

const app = express()

const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef')

const mppx = Mppx.create({
  methods: [
    tempo.session({
      account,
      chainId: 42431,
      currencies: ['0x20c0000000000000000000006a37DA5C996874BE'], // OUSD on Tempo
      store: Store.memory(),
    }),
  ],
})

// [!code focus:start]
app.get(
  '/api/sessions/photo',
  mppx.session({ amount: '0.01', unitType: 'photo' }), // [!code ++]
  async (req, res) => {
    const response = await fetch('https://picsum.photos/200/200')
    res.json({ url: response.url })
  },
)
// [!code focus:end]
```

#### Test via the `mppx` CLI

```bash [test.sh]
# Create a testnet account and fund it with test OUSD
$ npx mppx account create --network testnet
$ npx mppx account fund --network testnet

# Make a paid request
$ npx mppx http://localhost:3000/api/sessions/photo 
```
::::

### Other

This guide walks through using `mppx/server` directly with any [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)-compatible framework: [Bun](https://bun.sh), [Deno](https://deno.com), [Cloudflare Workers](https://workers.dev), and others.

::::steps
#### Install `mppx`

:::code-group
```bash [npm]
$ npm install mppx viem
```

```bash [pnpm]
$ pnpm add mppx viem
```

```bash [bun]
$ bun add mppx viem
```
:::

#### Set up `Mppx` instance

Set up an `Mppx` instance with the current `tempo.session` method.

* `account` signs server-side session settlement and close transactions.
* `currencies: [address]` accepts only OUSD when configuring a Tempo factory. Per-route overrides use singular `currency`.
* `store` keeps v2 Session channel state; use a durable atomic store in production.

```ts [server.ts]
import { Mppx, Store, tempo } from 'mppx/server'
import { privateKeyToAccount } from 'viem/accounts'

const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef')

const mppx = Mppx.create({
  methods: [
    tempo.session({
      account,
      chainId: 42431,
      currencies: ['0x20c0000000000000000000006a37DA5C996874BE'], // OUSD on Tempo
      store: Store.memory(),
    }),
  ],
})
```

#### Create the `/api/sessions/photo` route

Create the gallery route. This route is **currently unpaid**.

```ts [server.ts]
import { Mppx, Store, tempo } from 'mppx/server'
import { privateKeyToAccount } from 'viem/accounts'

const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef')

const mppx = Mppx.create({
  methods: [
    tempo.session({
      account,
      chainId: 42431,
      currencies: ['0x20c0000000000000000000006a37DA5C996874BE'], // OUSD on Tempo
      store: Store.memory(),
    }),
  ],
})

// [!code focus:start]
Bun.serve({
  async fetch(request) {
    const res = await fetch('https://picsum.photos/200/200')
    return Response.json({ url: res.url })
  },
})
// [!code focus:end]
```

#### Add `.session` to the route handler

Add payment verification using `mppx.session`. If the status is `402`, return the Challenge. Otherwise, fetch the photo and attach a Receipt to the response.

```ts [server.ts]
import { Mppx, Store, tempo } from 'mppx/server'
import { privateKeyToAccount } from 'viem/accounts'

const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef')

const mppx = Mppx.create({
  methods: [
    tempo.session({
      account,
      chainId: 42431,
      currencies: ['0x20c0000000000000000000006a37DA5C996874BE'], // OUSD on Tempo
      store: Store.memory(),
    }),
  ],
})

// [!code focus:start]
Bun.serve({
  async fetch(request) {
    const result = await mppx.session({ // [!code ++]
      amount: '0.01', // [!code ++]
      unitType: 'photo', // [!code ++]
    })(request) // [!code ++]

    if (result.status === 402) return result.challenge // [!code ++]

    const res = await fetch('https://picsum.photos/200/200')
    return result.withReceipt(Response.json({ url: res.url })) // [!code ++]
  },
})
// [!code focus:end]
```

#### Test via the `mppx` CLI

```bash [test.sh]
# Create a testnet account and fund it with test OUSD
$ npx mppx account create --network testnet
$ npx mppx account fund --network testnet

# Make a paid request
$ npx mppx http://localhost:3000
```
::::

### Per-request client setup

For the full buyer walkthrough (limits, streaming, closing), see [Pay for sessions](/guides/pay-for-sessions).

Set `maxDeposit` to bound the tokens reserved per channel. Automatic channel management doesn't require this option, but an unattended client needs a limit. Unspent funds return when the channel closes. This is not a total budget across replacement channels; use an [access-key spending limit](/guides/managing-agent-spend) for that.

For an API you don't control, also pin `currencies` and `expectedRecipients`, and set `maxAmount` with explicit `decimals` to cap each unit price. Use one channel store per HTTP origin. Set a trusted `origin` to recover unknown server snapshots; see [origin-bound recovery](/sdk/typescript/client/Method.tempo.session#origin-bound-recovery).

### Accounts SDK

```ts [client.ts]
import { Mppx, tempo } from 'mppx/client'
import { Provider } from 'accounts'

const provider = Provider.create({ mpp: false }) // Avoid double 402 handling; mppx is configured below.
await provider.request({ method: 'wallet_connect' })

const mppx = Mppx.create({
  methods: [tempo({
    account: provider.getAccount({ signable: true }),
    getClient: provider.getClient,
    maxDeposit: '1', // Reserve up to 1 OUSD 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')
```

### viem

```ts [client.ts]
import { Mppx, tempo } from 'mppx/client'
import { privateKeyToAccount } from 'viem/accounts'

const mppx = Mppx.create({
  methods: [tempo({
    account: privateKeyToAccount('0x...'),
    maxDeposit: '1', // Reserve up to 1 OUSD 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'`**: Caps reserved funds at 1 OUSD per channel. At $0.01/photo, 1 OUSD covers 100 requests.
* The client handles the full session lifecycle automatically: channel open, voucher signing, and retry after `402` responses.
* If the server sets `suggestedDeposit`, the client uses `min(suggestedDeposit, maxDeposit)`.

#### Close the channel

After you're done making requests, close the channel to settle on-chain and reclaim unspent deposit:

### Accounts SDK

```ts twoslash [client.ts]
import { tempo } from 'mppx/client'
import { Provider } from 'accounts'

const provider = Provider.create({ mpp: false }) // Avoid double 402 handling; mppx is configured below.
await provider.request({ method: 'wallet_connect' })

const session = tempo.session.manager({
  account: provider.getAccount({ signable: true }),
  getClient: provider.getClient,
  maxDeposit: '1',
})

const res = await session.fetch('http://localhost:3000/api/sessions/photo')

// Settle on-chain and reclaim unspent deposit
const receipt = await session.close()
```

### viem

```ts twoslash [client.ts]
import { tempo } from 'mppx/client'
import { privateKeyToAccount } from 'viem/accounts'

const session = tempo.session.manager({
  account: privateKeyToAccount('0x...'),
  maxDeposit: '1',
})

const res = await session.fetch('http://localhost:3000/api/sessions/photo')

// Settle on-chain and reclaim unspent deposit
const receipt = await session.close()
```

:::info
Channels remain open for reuse. Closing is not required between individual requests—only when you're done with the session entirely.
:::

## Streaming

Build a payment-gated poetry API that streams poems word-by-word and charges $0.001 per word over SSE.

The server charges per token as content streams. If the channel balance runs out mid-stream, the client automatically sends a new voucher and the stream resumes.

Pick **Streaming** in the [demo above](/guides/usage-based#try-it) to stream a paid poem.

### Streaming prompt mode

Paste this into your coding agent to build the poetry API in one prompt:

```text
Use https://mpp.dev/guides/usage-based.md as reference.
Add mppx to my app with a payment-gated SSE endpoint
that streams text word-by-word and charges $0.001 per word using the
Tempo session payment method with OUSD and sse: true.
```

### Streaming manual mode

Select your framework to follow a step-by-step guide. If your framework isn't listed, choose **Other** for a generic [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) approach compatible with most TypeScript server frameworks.

### Next.js

::::steps
#### Install `mppx`

:::code-group
```bash [npm]
$ npm install mppx viem
```

```bash [pnpm]
$ pnpm add mppx viem
```

```bash [bun]
$ bun add mppx viem
```
:::

#### Set up `Mppx` instance with streaming

Set up an `Mppx` instance with the current `tempo.session` method and `sse: true`.

```ts [app/api/sessions/poem/route.ts]
import { Store } from 'mppx'
import { Mppx, tempo } from "mppx/nextjs";
import { privateKeyToAccount } from 'viem/accounts'

const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef')

const mppx = Mppx.create({
  methods: [
    tempo.session({
      account,
      chainId: 42431,
      currencies: ["0x20c0000000000000000000006a37DA5C996874BE"], // OUSD on Tempo
      sse: true,
      store: Store.memory(),
    }),
  ],
});
```

#### Create the `/api/sessions/poem` route

Return an async generator from the poem route. Charge each word with `stream.charge()` before yielding it; metadata and newlines are free.

```ts [app/api/sessions/poem/route.ts]
import { Store } from 'mppx'
import { Mppx, tempo } from "mppx/nextjs";
import { privateKeyToAccount } from 'viem/accounts'

const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef')

const mppx = Mppx.create({
  methods: [
    tempo.session({
      account,
      chainId: 42431,
      currencies: ["0x20c0000000000000000000006a37DA5C996874BE"], // OUSD on Tempo
      sse: true,
      store: Store.memory(),
    }),
  ],
});

// [!code focus:start]
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;
      }
    };
  },
);
// [!code focus:end]
```

#### Test via the `mppx` CLI

```bash [test.sh]
# Create a testnet account and fund it with test OUSD
$ npx mppx account create --network testnet
$ npx mppx account fund --network testnet

# Stream a paid poem
$ npx mppx http://localhost:3000/api/sessions/poem
```
::::

### Hono

::::steps
#### Install `mppx` and `hono`

:::code-group
```bash [npm]
$ npm install mppx hono viem
```

```bash [pnpm]
$ pnpm add mppx hono viem
```

```bash [bun]
$ bun add mppx hono viem
```
:::

#### Set up `Mppx` instance with streaming

Set up an `Mppx` instance with the current `tempo.session` method and `sse: true`.

```ts [server.ts]
import { Store } from 'mppx'
import { Hono } from "hono";
import { Mppx, tempo } from "mppx/hono";
import { privateKeyToAccount } from 'viem/accounts'

const app = new Hono();

const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef')

const mppx = Mppx.create({
  methods: [
    tempo.session({
      account,
      chainId: 42431,
      currencies: ["0x20c0000000000000000000006a37DA5C996874BE"], // OUSD on Tempo
      sse: true,
      store: Store.memory(),
    }),
  ],
});
```

#### Create the `/api/sessions/poem` route

Return an async generator from the session middleware. Charge each word with `stream.charge()` before yielding it; metadata and newlines are free.

```ts [server.ts]
import { Store } from 'mppx'
import { Hono } from "hono";
import { Mppx, tempo } from "mppx/hono";
import { privateKeyToAccount } from 'viem/accounts'

const app = new Hono();

const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef')

const mppx = Mppx.create({
  methods: [
    tempo.session({
      account,
      chainId: 42431,
      currencies: ["0x20c0000000000000000000006a37DA5C996874BE"], // OUSD on Tempo
      sse: true,
      store: Store.memory(),
    }),
  ],
});

// [!code focus:start]
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;
      }
    };
  },
);
// [!code focus:end]
```

#### Test via the `mppx` CLI

```bash [test.sh]
# Create a testnet account and fund it with test OUSD
$ npx mppx account create --network testnet
$ npx mppx account fund --network testnet

# Stream a paid poem
$ npx mppx http://localhost:3000/api/sessions/poem
```
::::

### Workers

::::steps
#### Install `mppx`

:::code-group
```bash [npm]
$ npm install mppx viem
```

```bash [pnpm]
$ pnpm add mppx viem
```

```bash [bun]
$ bun add mppx viem
```
:::

#### Set up `Mppx` instance with streaming

Set `sse: { poll: true }` on Workers: vouchers arrive in other request contexts, which cannot resolve the stream's pending promises.

```ts [src/index.ts]
import { Mppx, Store, tempo } from "mppx/server";
import { privateKeyToAccount } from 'viem/accounts'

const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef')

const mppx = Mppx.create({
  methods: [
    tempo.session({
      account,
      chainId: 42431,
      currencies: ["0x20c0000000000000000000006a37DA5C996874BE"], // OUSD on Tempo
      sse: { poll: true }, // [!code hl]
      store: Store.memory(),
    }),
  ],
});
```

#### Create the `/api/sessions/poem` route

Return an async generator from the poem route. Charge each word with `stream.charge()` before yielding it; metadata and newlines are free.

```ts [src/index.ts]
import { Mppx, Store, tempo } from "mppx/server";
import { privateKeyToAccount } from 'viem/accounts'

const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef')

const mppx = Mppx.create({
  methods: [
    tempo.session({
      account,
      chainId: 42431,
      currencies: ["0x20c0000000000000000000006a37DA5C996874BE"], // OUSD on Tempo
      sse: { poll: true }, // [!code hl]
      store: Store.memory(),
    }),
  ],
});

// [!code focus:start]
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 default {
  async fetch(request: 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;
      }
    });
  },
};
// [!code focus:end]
```

#### Test via the `mppx` CLI

```bash [test.sh]
# Create a testnet account and fund it with test OUSD
$ npx mppx account create --network testnet
$ npx mppx account fund --network testnet

# Stream a paid poem
$ npx mppx http://localhost:8787
```
::::

### Express

::::steps
#### Install `mppx` and `express`

:::code-group
```bash [npm]
$ npm install mppx express viem
```

```bash [pnpm]
$ pnpm add mppx express viem
```

```bash [bun]
$ bun add mppx express viem
```
:::

#### Set up `Mppx` instance with streaming

Set up an `Mppx` instance with the current `tempo.session` method and `sse: true`.

```ts [server.ts]
import { Store } from 'mppx'
import express from "express";
import { Mppx, tempo } from "mppx/express";
import { privateKeyToAccount } from 'viem/accounts'

const app = express();

const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef')

const mppx = Mppx.create({
  methods: [
    tempo.session({
      account,
      chainId: 42431,
      currencies: ["0x20c0000000000000000000006a37DA5C996874BE"], // OUSD on Tempo
      sse: true,
      store: Store.memory(),
    }),
  ],
});
```

#### Create the `/api/sessions/poem` route

Return an async generator from the session middleware. Charge each word with `stream.charge()` before yielding it; metadata and newlines are free.

```ts [server.ts]
import { Store } from 'mppx'
import express from "express";
import { Mppx, tempo } from "mppx/express";
import { privateKeyToAccount } from 'viem/accounts'

const app = express();

const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef')

const mppx = Mppx.create({
  methods: [
    tempo.session({
      account,
      chainId: 42431,
      currencies: ["0x20c0000000000000000000006a37DA5C996874BE"], // OUSD on Tempo
      sse: true,
      store: Store.memory(),
    }),
  ],
});

// [!code focus:start]
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;
      }
    };
  },
);
// [!code focus:end]
```

#### Test via the `mppx` CLI

```bash [test.sh]
# Create a testnet account and fund it with test OUSD
$ npx mppx account create --network testnet
$ npx mppx account fund --network testnet

# Stream a paid poem
$ npx mppx http://localhost:3000/api/sessions/poem
```
::::

### Other

This guide walks through using `mppx/server` directly with any [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)-compatible framework: [Bun](https://bun.sh), [Deno](https://deno.com), [Cloudflare Workers](https://workers.dev), and others.

::::steps
#### Install `mppx`

:::code-group
```bash [npm]
$ npm install mppx viem
```

```bash [pnpm]
$ pnpm add mppx viem
```

```bash [bun]
$ bun add mppx viem
```
:::

#### Set up `Mppx` instance with streaming

Set up an `Mppx` instance with the current `tempo.session` method and `sse: true`.

```ts [server.ts]
import { Mppx, Store, tempo } from "mppx/server";
import { privateKeyToAccount } from 'viem/accounts'

const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef')

const mppx = Mppx.create({
  methods: [
    tempo.session({
      account,
      chainId: 42431,
      currencies: ["0x20c0000000000000000000006a37DA5C996874BE"], // OUSD on Tempo
      sse: true,
      store: Store.memory(),
    }),
  ],
});
```

#### Create the streaming poem route

Pass an async generator to `withReceipt`. Charge each word with `stream.charge()` before yielding it; metadata and newlines are free. If the balance runs out, the server emits `event: payment-need-voucher` and waits for a new voucher.

```ts [server.ts]
import { Mppx, Store, tempo } from "mppx/server";
import { privateKeyToAccount } from 'viem/accounts'

const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef')

const mppx = Mppx.create({
  methods: [
    tempo.session({
      account,
      chainId: 42431,
      currencies: ["0x20c0000000000000000000006a37DA5C996874BE"], // OUSD on Tempo
      sse: true,
      store: Store.memory(),
    }),
  ],
});

// [!code focus:start]
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;
      }
    });
  },
});
// [!code focus:end]
```

#### Test via the `mppx` CLI

```bash [test.sh]
# Create a testnet account and fund it with test OUSD
$ npx mppx account create --network testnet
$ npx mppx account fund --network testnet

# Stream a paid poem
$ npx mppx http://localhost:3000
```
::::

### Streaming client setup

For the full buyer walkthrough (limits, streaming, closing), see [Pay for sessions](/guides/pay-for-sessions).

Use `tempo.session.manager()` from `mppx/client` to create a session manager. Use one manager and channel store per API origin: its first HTTP or HTTPS request pins that origin, and later requests or redirected responses from other origins are rejected. 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.

### Accounts SDK

```ts [client.ts]
import { tempo } from "mppx/client";
import { Provider } from "accounts";

const provider = Provider.create({ mpp: false }) // Avoid double 402 handling; mppx is configured below.
await provider.request({ method: "wallet_connect" })

const session = tempo.session.manager({
  account: provider.getAccount({ signable: true }),
  getClient: provider.getClient,
  maxDeposit: "1", // Reserve up to 1 OUSD 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 + " ");
}
```

### viem

```ts [client.ts]
import { tempo } from "mppx/client";
import { privateKeyToAccount } from "viem/accounts";

const session = tempo.session.manager({
  account: privateKeyToAccount("0x..."),
  maxDeposit: "1", // Reserve up to 1 OUSD 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.manager()`** — Creates a session manager that handles the full Sessions lifecycle.
* **`.sse()`** — Connects to an SSE endpoint. Automatically sends new vouchers when the server emits `payment-need-voucher` events.
* **`maxDeposit: '1'`** — Reserves up to 1 OUSD. At $0.001/word, this covers ~1,000 words before the channel needs a top-up.

#### Close the channel after streaming

After streaming completes, close the channel to settle and reclaim unspent deposit:

### Accounts SDK

```ts twoslash [client.ts]
import { tempo } from 'mppx/client'
import { Provider } from 'accounts'

const provider = Provider.create({ mpp: false }) // Avoid double 402 handling; mppx is configured below.
await provider.request({ method: 'wallet_connect' })

const session = tempo.session.manager({
  account: provider.getAccount({ signable: true }),
  getClient: provider.getClient,
  maxDeposit: '1',
})

const stream = await session.sse('http://localhost:3000/api/sessions/poem')

for await (const word of stream) {
  process.stdout.write(word + ' ')
}

// Settle on-chain and reclaim unspent deposit
const receipt = await session.close()
```

### viem

```ts twoslash [client.ts]
import { tempo } from 'mppx/client'
import { privateKeyToAccount } from 'viem/accounts'

const session = tempo.session.manager({
  account: privateKeyToAccount('0x...'),
  maxDeposit: '1',
})

const stream = await session.sse('http://localhost:3000/api/sessions/poem')

for await (const word of stream) {
  process.stdout.write(word + ' ')
}

// Settle on-chain and reclaim unspent deposit
const receipt = await session.close()
```

## Go to production

* **Use a persistent store**: replace `Store.memory()` with a durable, atomic store so channel state survives restarts and scales across instances. See [`tempo.session`](/sdk/typescript/server/Method.tempo.session).
* **Close idle channels**: run [`tempo.session.superviseCloses`](/sdk/typescript/server/Method.tempo.session#temposessionsupervisecloses) every minute (every 5 minutes with few open channels) so idle channels close and buyers get unspent funds back. See [Close supervision](/payment-methods/tempo/session#close-supervision).
* **Settle streams on a schedule**: set `settlementSchedule` on `tempo.session()` so SSE and WebSocket charges settle without waiting for close.
* **Monitor settlement failures**: use [`onSessionSettlementFailure`](/sdk/typescript/server/Method.tempo.session#onsessionsettlementfailure-optional) to alert on failures. Retryable failures leave the charge due for a later settlement.
* **Credit undelivered prepaid items**: with `sse: true` and `unitType: 'request'`, buyers pay before your handler runs. Set `sse: { autoCreditPrepaid: true, onRefundRequired }` to credit items lost to SSE failures, and persist refund jobs in `onRefundRequired`. See [automatic prepaid credits](/sdk/typescript/server/Method.tempo.session#automatic-prepaid-credits).
* **Charge dynamic amounts**: pass a `bigint` in base units, such as `stream.charge(2_000n)`, to price one event differently from the route's tick cost.
* **Use WebSocket for bidirectional streams**: serve with [`mppx.session.serveWebSocket()`](/sdk/typescript/server/Ws.serve) and connect with [`session.ws()`](/sdk/typescript/client/Method.tempo.session-manager#with-websocket-streaming) instead of `session.sse()`.
* **Go live**: before going live, set `chainId: 4217` and fund a mainnet account.

## Next steps

[Charge per request](/guides/one-time-payments) — One payment per request, settled on-chain

[Server quickstart](/quickstart/server) — Charge for your API as a seller

[Tempo Wallet CLI](https://wallet.tempo.xyz) — Managed MPP client with built in spend controls and service discovery
