> **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 streamed payments \[Per-token billing over Server-Sent Events]

## 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 poetry API that streams poems word-by-word and charges $0.001 per word using `mppx` sessions with Server-Sent Events (SSE).

:::info
Streamed payments extend [pay-as-you-go sessions](/guides/pay-as-you-go) with 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.
:::

## Demo

Try the payment-gated poetry API. Click **Run demo** to create a wallet, fund it, and stream a paid poem.

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

  402 Payment Required

  Open a Tempo payment session and pay for streamed output.
  ```
</div>

## Prompt mode

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

```text
Use https://mpp.dev/guides/streamed-payments.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 PathUSD and sse: true.
```

## 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: 4217,
      currency: "0x20c0000000000000000000000000000000000000", // pathUSD on Tempo
      sse: true,
      store: Store.memory(),
    }),
  ],
});
```

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

Create the poem route. The `withReceipt` method accepts an async generator—each yielded value is one SSE event and one charged word.

```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: 4217,
      currency: "0x20c0000000000000000000000000000000000000", // pathUSD 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 [terminal]
# Create account funded with testnet tokens
$ npx mppx account create

# 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: 4217,
      currency: "0x20c0000000000000000000000000000000000000", // pathUSD on Tempo
      sse: true,
      store: Store.memory(),
    }),
  ],
});
```

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

Create the poem route with the session middleware. The handler returns an async generator—each yielded value is one SSE event and one charged word.

```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
      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 [terminal]
# Create account funded with testnet tokens
$ npx mppx account create

# 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 up an `Mppx` instance with the current `tempo.session` method and `sse: true`.

```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
      sse: true,
      store: Store.memory(),
    }),
  ],
});
```

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

Create the poem route. The `withReceipt` method accepts an async generator—each yielded value is one SSE event and one charged word.

```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
      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 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 [terminal]
# Create account funded with testnet tokens
$ npx mppx account create

# 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: 4217,
      currency: "0x20c0000000000000000000000000000000000000", // pathUSD on Tempo
      sse: true,
      store: Store.memory(),
    }),
  ],
});
```

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

Create the poem route with the session middleware. The handler returns an async generator—each yielded value is one SSE event and one charged word.

```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
      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 [terminal]
# Create account funded with testnet tokens
$ npx mppx account create

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

<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 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: 4217,
      currency: "0x20c0000000000000000000000000000000000000", // pathUSD on Tempo
      sse: true,
      store: Store.memory(),
    }),
  ],
});
```

### Create the streaming poem route

Create the route handler. `withReceipt` accepts an async generator—each yielded value becomes one SSE `event: message` 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.

```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
      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 [terminal]
# Create account funded with testnet tokens
$ npx mppx account create

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

## Client setup

Use `tempo.session.manager()` 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.

### 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 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 + " ");
}
```

### 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 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.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 pathUSD. At $0.001/word, this covers ~1,000 words before the channel needs a top-up.

### Closing the channel

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()
```

## WebSocket alternative

The examples above use Server-Sent Events (SSE) for streaming. If your use case benefits from a persistent bidirectional connection — for example, interactive chat or real-time sessions — you can stream over WebSocket instead. The session payment flow is the same (channel open, voucher signing, close), but vouchers and content travel over a single socket rather than separate HTTP requests.

On the server, use [`tempo.Ws.serve()`](/sdk/typescript/server/Ws.serve) to bridge a WebSocket to the session payment flow. On the client, use [`session.ws()`](/sdk/typescript/client/Method.tempo.session-manager#sessionwsinput-init) instead of `session.sse()`.

## Next steps
