> **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.

# Accept pay-as-you-go payments \[Session-based billing with payment channels]

## 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.

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

:::info
Unlike [one-time payments](/guides/one-time-payments), sessions open a payment channel once and
use off-chain vouchers for each subsequent request—vouchers are **not bottlenecked by blockchain throughput**, they are processed in pure CPU-bound signature checks.
:::

## Demo

Try the payment-gated photo gallery API. Click **Run demo** to create a wallet, fund it, and generate a gallery of paid photos.

<div style={{ height: 480 }}>
  ```text
  GET /api/sessions/photo

  402 Payment Required

  Open a Tempo payment session and pay $0.01 for each photo.
  ```
</div>

## Prompt mode

Paste this into your coding agent to build the entire guide in one prompt:

```text
Use https://mpp.dev/guides/pay-as-you-go.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
PathUSD. When payment is verified, fetch a random photo from
https://picsum.photos/200/200 and return the URL as JSON.
```

## 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.
* `currency` is the token address for payments, in this case `pathUSD`.
* `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: 4217,
      currency: '0x20c0000000000000000000000000000000000000', // pathUSD 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: 4217,
      currency: '0x20c0000000000000000000000000000000000000', // pathUSD 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: 4217,
      currency: '0x20c0000000000000000000000000000000000000', // pathUSD 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 [terminal]
# Create account funded with testnet tokens
$ npx mppx account create

# 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.
* `currency` is the token address for payments, in this case `pathUSD`.
* `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: 4217,
      currency: '0x20c0000000000000000000000000000000000000', // pathUSD 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: 4217,
      currency: '0x20c0000000000000000000000000000000000000', // pathUSD 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: 4217,
      currency: '0x20c0000000000000000000000000000000000000', // pathUSD 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 [terminal]
# Create account funded with testnet tokens
$ npx mppx account create

# 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.
* `currency` is the token address for payments, in this case `pathUSD`.
* `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: 4217,
      currency: '0x20c0000000000000000000000000000000000000', // pathUSD 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: 4217,
      currency: '0x20c0000000000000000000000000000000000000', // pathUSD 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: 4217,
      currency: '0x20c0000000000000000000000000000000000000', // pathUSD 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 [terminal]
# Create account funded with testnet tokens
$ npx mppx account create

# 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.
* `currency` is the token address for payments, in this case `pathUSD`.
* `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: 4217,
      currency: '0x20c0000000000000000000000000000000000000', // pathUSD 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: 4217,
      currency: '0x20c0000000000000000000000000000000000000', // pathUSD 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: 4217,
      currency: '0x20c0000000000000000000000000000000000000', // pathUSD 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 [terminal]
# Create account funded with testnet tokens
$ npx mppx account create

# 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.

<div className="h-6" />

::::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.
* `currency` is the token address for payments, in this case `pathUSD`.
* `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: 4217,
      currency: '0x20c0000000000000000000000000000000000000', // pathUSD 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: 4217,
      currency: '0x20c0000000000000000000000000000000000000', // pathUSD 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: 4217,
      currency: '0x20c0000000000000000000000000000000000000', // pathUSD 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 [terminal]
# Create account funded with testnet tokens
$ npx mppx account create

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

## Client setup

When using sessions from a client, set `maxDeposit` to enable automatic channel management. This is the maximum amount of tokens the client reserves in the payment channel. Any unspent deposit is refunded when the channel closes.

### 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 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')
```

### 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 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'`**: Reserves up to 1 pathUSD in 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 `402` responses.
* If the server sets `suggestedDeposit`, the client uses `min(suggestedDeposit, maxDeposit)`.

### Closing 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.
:::

## Next steps
