# Accept streaming payments

Use MPP sessions to charge for streamed output with usage settling to your Stripe balance.

A session lets an agent authorize a maximum amount up front, then pay for only what it uses. Your server charges for each unit of work as it happens, in increments as small as 0.000001 USDC, and periodically settles funds to your Stripe balance in whole cent increments.

Use sessions for services with variable pricing, such as:

- LLM inference billed per token
- Compute billed per second
- Data analysis billed per row

## Understand session behavior

A session-based payment flow involves four parties: the agent, your server, the Tempo blockchain, and Stripe.

A diagram showing the MPP session payment flow between agent, server, Tempo blockchain, and Stripe (See full diagram at https://docs.stripe.com/payments/machine/mpp/sessions)

```text
[Agent] -- Deposit USDC into session (one-time on-chain) --> [Tempo]
[Tempo] -- Session opened, funds locked --> [Agent]
[Agent] -- Submit a request with a voucher --> [Server]
[Server] -- Stream results, charging for each unit of work --> [Agent]
[Server] -- Repeat until usage reaches the settlement amount --> [Server]
[Server] -- Settle accumulated funds --> [Tempo]
[Tempo] -- Settlement confirmed --> [Server]
[Server] -- Create PaymentIntent with transaction_verification --> [Stripe]
```

The agent deposits funds into escrow on-chain when it opens the session. For each unit of work, the server charges the agent against the authorized deposit amount. When accrued usage reaches your configured settlement amount, the server settles funds on-chain and records them to your Stripe Balance by creating a PaymentIntent.

> Stripe verifies each PaymentIntent amount against the on-chain settlement amount rounded down to the cent, so any sub-cent remainder, such as when a client closes a session, is forfeited.

## Before you begin

Make sure you’ve satisfied all [MPP prerequisites](https://docs.stripe.com/payments/machine/mpp.md).

## Use a coding agent

In this example, you monetize a CSV validation service with streaming payments. Accrue usage in increments as small as 0.000001 USD, the smallest unit of USDC on Tempo. Settle to your Stripe balance in increments of 0.01 USD or more, which is the smallest amount a PaymentIntent can record.

Use this prompt with your coding agent:

```bash
Read https://docs.stripe.com/payments/machine/mpp/sessions.md?lang=node, and build a payment-gated POST /validate-csv endpoint that checks CSV rows for empty fields and charges 0.001 USD per data row using MPP sessions on the Tempo network. Settle every 0.01 USD and stream a JSON validation result for each row.
```

You can also follow the manual steps in the following sections.

## Create a deposit address

Create the deposit address that you use to accumulate funds. Later, you’ll recognize funds sent to this address using standard `PaymentIntents`.

```bash
curl https://api.stripe.com/v1/crypto/deposit_addresses \
  -u "<<YOUR_SECRET_KEY>>:" \
  -H "Stripe-Version: 2026-07-29.preview" \
  -d network=tempo
```

Save the returned `address` value. You’ll use it as `TEMPO_DEPOSIT_ADDRESS` in your server environment.

## Generate an operator key

Generate a private key for your server’s operator account and store it as `MPP_SESSION_OPERATOR_KEY` in your server environment. The operator signs on-chain settlements, but doesn’t need to hold any funds.

```bash
openssl rand -hex 32 | sed 's/^/0x/'
```

## Install dependencies

Install the packages required for the server, payment flow, and Stripe API calls.

```bash
npm install mppx stripe hono @hono/node-server viem csv-parse
```

## Configure Stripe and mppx

Import your dependencies and configure the Stripe client with [gasless transactions](https://docs.stripe.com/payments/machine/mpp.md#enable-gasless-transactions).

Set `STRIPE_PROFILE_ID` to your [Stripe profile](https://docs.stripe.com/get-started/account/profile.md) ID.

```typescript
import crypto from "node:crypto";
import { Readable } from "node:stream";
import { parse } from "csv-parse";
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { streamSSE } from "hono/streaming";
import { Mppx } from "mppx/hono";
import { Store, stripe } from "mppx/server";
import Stripe from "stripe";
import { privateKeyToAccount } from "viem/accounts";

const mppSecretKey = crypto
  .createHmac("sha256", process.env.STRIPE_SECRET_KEY!)
  .update("mpp-challenge-signing")
  .digest("base64");

const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: "2026-07-29.preview" as Stripe.LatestApiVersion,
});

const recipient = process.env.TEMPO_DEPOSIT_ADDRESS! as
  stripe.DepositAddress<"tempo">;
const operator = privateKeyToAccount(
  process.env.MPP_SESSION_OPERATOR_KEY! as `0x${string}`,
);

const stripeMachinePayments = stripe.create({
  client: stripeClient,
  networkId: process.env.STRIPE_PROFILE_ID!,
  livemode: !process.env.STRIPE_SECRET_KEY!.includes("_test_"),
  hostedFeePayer: true,
});

const store = Store.memory();
```

`mppx` binds each challenge to `mppSecretKey` so that it can verify credentials it issued. `Store.memory()` works for a single process. Use a shared `AtomicStore` before production.

The configuration detects your sandbox API key, sets `livemode` to `false`, and uses Tempo testnet. Gasless transactions require Tempo deposit addresses. The deposit address and Stripe API key must belong to the same account. You can’t use `hostedFeePayer` with Connect integrations.

## Create the MPP Session

Create the session and gate your route with the `mppx.session` middleware. This example charges 0.001 USD per data row and settles every 0.01 USD of usage. Choose a settlement amount that’s a whole number of cents and a multiple of your unit price, so that every scheduled settlement is a whole-cent amount. For example, a 0.001 USD unit price divides evenly into a 0.01 USD settlement amount. A 0.003 USD unit price doesn’t, so scheduled settlements leave sub-cent remainders that are forfeited.

```typescript
const session = stripeMachinePayments.tempo.session({
  recipient,
  account: operator,
  operator: operator.address,
  store,
  sse: true,
  settlementSchedule: { amount: "0.01" },
});

const mppx = Mppx.create({ secretKey: mppSecretKey, methods: [session] });

const app = new Hono();

app.post(
  "/validate-csv",
  mppx.session({ amount: "0.001", unitType: "row", suggestedDeposit: "0.05" }),
  (context) => {
    if (!context.req.raw.body) return context.text("CSV body is required", 400);
    const records = Readable.fromWeb(context.req.raw.body as never).pipe(
      parse({ columns: true, bom: true, skip_empty_lines: true }),
    ) as AsyncIterable<Record<string, string>>;

    return streamSSE(context, async (stream) => {
      let row = 0;
      for await (const record of records) {
        const valid = Object.values(record).every(Boolean);
        await stream.writeSSE({
          data: JSON.stringify({ row: ++row, valid }),
        });
      }
    });
  },
);

serve({ fetch: app.fetch, port: 4242 });
```

The middleware handles the HTTP `402` challenge, session management, and [MPP receipts](https://mpp.dev/protocol/receipts). MPP receipts confirm payment to the client and are separate from Stripe receipts. When your handler returns a `text/event-stream` response, `mppx` charges the client for each event.

Test in a sandbox with `mppx validate` using the first 35 rows of the 2018 NYC Central Park squirrel census:

```bash
npx mppx validate http://localhost:4242 --endpoint POST:/validate-csv --body "$(curl -fsSL 'https://data.cityofnewyork.us/resource/vfnx-vebw.csv?$limit=35')"
```

## See also

- [MPP payments](https://docs.stripe.com/payments/machine/mpp.md)
