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

# Sessions \[Low-cost high-throughput payments]

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

Sessions let a buyer pay per request, per token, or per byte without an on-chain transaction for each payment. The buyer locks up a deposit in a payment channel once, then signs off-chain vouchers as it uses your service. Per-request latency and fees are near-zero. Your server verifies vouchers, checks the live channel reserve, and settles on-chain in batches.

Use Sessions for metered usage: LLM tokens, repeated API calls, or streams. For single API calls or one-off purchases, use [charge](/payment-methods/tempo/charge).

## How it works

A Credential is the buyer's payment proof: an opening transaction or a signed voucher for an existing channel.

```mermaid
sequenceDiagram
  participant Client
  participant Server
  participant Tempo
  Client->>Tempo: (1) Open channel with deposit
  Client->>Server: (2) Open Credential
  Server-->>Client: 200 OK + Receipt
  loop Per request
      Client->>Server: (3) Request + voucher
      Note over Server: verify signature
      Server-->>Client: 200 OK + Receipt
  end
  Server->>Tempo: (4) Settle earned amount
  Client->>Server: (5) Close
  Server->>Tempo: close(channelId, voucher)
  Tempo-->>Client: Refund remaining deposit

```

:::steps
### Open

The buyer deposits stablecoins into a channel on the [TIP-1034 precompile](https://tips.sh/1034). The channel is between the buyer (payer) and you, the seller (payee), and a `channelId` identifies it.

### Pay with vouchers

The buyer signs a voucher authorizing a cumulative payment limit. Your server verifies it and records the cost of each billable item as spend. A voucher can authorize more than the buyer has used; that extra authorization is not earned revenue.

### Settle

Your server submits the largest accepted voucher at or below recorded spend to collect earned revenue. The channel stays open. Settlement runs on a schedule you choose; see [Settlement](/payment-methods/tempo/session#settlement).

### Top up or close

When the deposit runs low, the buyer tops up without closing the channel. When the buyer is done, it closes the channel: your server settles the final amount and the rest of the deposit goes back to the buyer.
:::

## Server

Use [`tempo.session`](/sdk/typescript/server/Method.tempo.session) to accept Sessions, and `mppx.session` in a request handler to charge for each request.

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

const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef')

const mppx = Mppx.create({
  methods: [
    tempo.session({
      account, // signs settlement and close transactions
      chainId: 42431, // Tempo testnet (Moderato)
      currencies: ['0x20c0000000000000000000006a37DA5C996874BE'], // OUSD on Tempo
      store: Store.memory(), // use a shared atomic store in production
    }),
  ],
})

export async function handler(request: Request) {
  // [!code hl:start]
  const result = await mppx.session({
    amount: '25',
    unitType: 'llm_token',
  })(request)
  // [!code hl:end]

  if (result.status === 402) return result.challenge

  return result.withReceipt(Response.json({ data: '...' }))
}
```

`Store.memory()` works for local development. In production, every server instance must share one store with an atomic `update(key, fn)`: use `Store.redis()`, `Store.upstash()`, or `Store.cloudflare()` over a backend that implements `update` atomically. These adapters don't add atomicity a backend lacks.

A production server also needs a [settlement schedule](/payment-methods/tempo/session#settlement) to collect spend and [close supervision](/payment-methods/tempo/session#close-supervision) to protect it.

:::info[Go live]
Before going live, set `chainId: 4217` (Tempo mainnet) and fund a mainnet account.
:::

### Settlement

Set `settlementSchedule` so your server settles in the background as spend accumulates. Buyers don't see or control the schedule. Settlement triggers when any threshold is crossed: `amount` (spend since the last settlement), `units` (metered units), or `intervalMs` (time since the last settlement).

```ts twoslash
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, // Tempo testnet (Moderato)
      currencies: ['0x20c0000000000000000000006a37DA5C996874BE'], // OUSD on Tempo
      settlementSchedule: { // [!code hl]
        amount: '10',
        intervalMs: 60_000,
        units: 10_000,
      },
      store: Store.memory(),
    }),
  ],
})
```

The schedule applies to HTTP responses, SSE streams, and WebSocket streams. `mppx` checks it after each charge.

To settle at a time you pick—for example, end-of-period reconciliation or before maintenance—call `tempo.session.settle` for one channel or `tempo.session.settleBatch` for several. Each submits the largest accepted voucher at or below recorded spend. If no voucher fits, these helpers throw; the [Cloudflare queue](/sdk/typescript/cloudflare#settle) instead performs a bounded close that captures recorded spend up to the signed voucher and deposit, then refunds the remainder.

```ts twoslash
import { Store, tempo } from 'mppx/server'
import { createWalletClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { tempoModerato } from 'viem/chains'

const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef')
const store = Store.memory()

const client = createWalletClient({
  account,
  chain: tempoModerato,
  transport: http('https://rpc.moderato.tempo.xyz'),
})

const channelIds = [
  '0x0000000000000000000000000000000000000000000000000000000000000000',
  '0x1111111111111111111111111111111111111111111111111111111111111111',
] as const

const txHashes = await tempo.session.settleBatch(store, client, channelIds, { account }) // [!code hl]
console.log(txHashes)
// @log: ['0x...', '0x...']
```

### Close supervision

A payer can request a close on-chain without going through your server, for example when its client crashes or stops paying. After Tempo's 900-second close grace period ends, the payer can withdraw the whole deposit, including spend your server accepted but hasn't settled. An idle channel gets no requests your server could react to, so run close supervision on every server that accepts Sessions.

Run [`tempo.session.superviseCloses`](/sdk/typescript/server/Method.tempo.session#temposessionsupervisecloses) on a schedule. Each run reads the on-chain reserve of the channels you pass it and answers every close request with a payee close that captures what the channel spent, refunding the rest to the payer.

* **Schedule:** run it every minute, as Tempo's own services do. Every 5 minutes is fine while you have few open channels; keep the interval plus a run's duration well inside the 900-second grace period, and shorten it as open channels grow.
* **Where:** a timer in your server process, or any cron (Cloudflare Workers cron triggers, Vercel Cron Jobs, a Kubernetes CronJob). It needs the same `store` and settlement `account` as `tempo.session()`.
* **Channel IDs:** the store can't list channels, so record each channel ID when it is first charged (a Session Receipt's `reference` is the channel ID) and drop it after a run reports it `closed`, `finalized`, `missing` or `unrecovered`.
* **Monitoring:** save `result.block` as `previousBlock` only when `result.complete` is true, and alert when the last complete run's `result.timestamp` grows older than a few intervals.

```ts twoslash
import { Mppx, Store, tempo } from 'mppx/server'
import { createWalletClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { tempoModerato } from 'viem/chains'

const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef')
const client = createWalletClient({
  account,
  chain: tempoModerato,
  transport: http('https://rpc.moderato.tempo.xyz'),
})
const store = Store.memory()

// Keep these in your database so they survive restarts.
const channelIds = new Set<`0x${string}`>()
let previousBlock: bigint | undefined

const mppx = Mppx.create({
  methods: [
    tempo.session({
      account,
      chainId: 42431,
      currencies: ['0x20c0000000000000000000006a37DA5C996874BE'], // OUSD on Tempo
      onPaymentSuccess({ receipt }) { // [!code hl:start]
        channelIds.add(receipt.reference as `0x${string}`)
      }, // [!code hl:end]
      store,
    }),
  ],
})

async function supervise() {
  const result = await tempo.session.superviseCloses({ // [!code hl:start]
    account,
    channelIds,
    client,
    previousBlock,
    store,
  }) // [!code hl:end]
  if (result.complete) previousBlock = result.block
  for (const { channelId, outcome } of result.channels)
    if (['closed', 'finalized', 'missing', 'unrecovered'].includes(outcome)) channelIds.delete(channelId)
}

// Schedule the next run after this one finishes, so runs never overlap and an RPC error doesn't crash the server.
async function loop() {
  try {
    await supervise()
  } catch (error) {
    console.error('close supervision failed', error)
  }
  setTimeout(loop, 60_000)
}
loop()
```

`superviseCloses` is a higher-level helper over the same on-chain reads and payee close that [`tempo.session.settle`](/payment-methods/tempo/session#settlement) uses; servers with their own channel bookkeeping can drive those directly instead. Close requests aren't pushed to servers, so polling on a schedule is the supported approach.

For unpaid channels whose signed voucher exceeds recorded spend, opt into `closeUnsettled: true` to collect spend with a bounded close even without a payer close request. It keeps channels open when a retained voucher can settle normally and waits for delivery of items charged in advance or another worker's settlement. See the [supervision reference](/sdk/typescript/server/Method.tempo.session#temposessionsupervisecloses) for lease, checkpoint, and retry rules.

Cloudflare's queue enables this recovery automatically. After upgrading an older scheduler, run [`backfillSettlement`](/sdk/typescript/cloudflare#backfillsettlement) over your registered channel IDs to wake dormant unpaid channels.

### Receipts

A Session Receipt's `reference` is the channel ID, not a transaction hash as in a charge Receipt. Closing the channel returns a Receipt whose `txHash` is the settlement transaction.

```ts
type SessionReceipt = {
  acceptedCumulative: string
  challengeId: string
  channelId: `0x${string}`
  intent: 'session'
  method: 'tempo'
  reference: string // channel ID
  spent: string
  status: 'success'
  timestamp: string
  txHash?: `0x${string}` // on close
  units?: number
}
```

### Production checklist

* **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).
* **Answer close requests**: run [`tempo.session.superviseCloses`](/sdk/typescript/server/Method.tempo.session#temposessionsupervisecloses) every minute (every 5 minutes with few open channels) so every buyer close request gets a payee close that captures spend and refunds the rest. It doesn't close idle channels nobody asked to close. 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. The charge atomically records the pending delivery, blocking automatic bounded closure until delivery or refund reconciliation completes. 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.

## Client

Use [`tempo`](/sdk/typescript/client/Method.tempo) with `Mppx.create`. It handles both one-time charges and Sessions: on a Session Challenge it opens a channel, signs vouchers, and reuses the channel across requests. `maxDeposit` caps how much it locks up per channel.

### Accounts SDK

```ts twoslash
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 { fetch: mppxFetch } = Mppx.create({
  methods: [tempo({
    account: provider.getAccount({ signable: true }),
    getClient: provider.getClient,
    maxDeposit: '1',
  })],
  polyfill: false,
})

const response = await mppxFetch('https://api.example.com/v1/chat/completions')
// Opens the channel and signs vouchers automatically
```

### viem

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

const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef')

const { fetch: mppxFetch } = Mppx.create({
  methods: [tempo({ account, maxDeposit: '1' })],
  polyfill: false,
})

const response = await mppxFetch('https://api.example.com/v1/chat/completions')
// Opens the channel and signs vouchers automatically
```

### Closing the channel

Channels don't expire: the deposit stays locked up until someone closes the channel. To close it and get the unspent deposit back, use `tempo.session.manager()` and call `close()`. The manager also exposes `.topUp()`, `.sse()`, and `.ws()`.

### Accounts SDK

```ts twoslash
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 response = await session.fetch('https://api.example.com/v1/chat/completions')
const receipt = await session.close() // [!code hl]
```

### viem

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

const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef')

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

const response = await session.fetch('https://api.example.com/v1/chat/completions')
const receipt = await session.close() // [!code hl]
```

See [`tempo.session.manager`](/sdk/typescript/client/Method.tempo.session-manager) for the full lifecycle API.

### Limit prices and bind the API origin

`currencies` and `expectedRecipients` restrict which Session offers your client accepts. `maxAmount` caps the Challenge price per unit; it requires `currencies` and explicit `decimals`. `maxAtomicAmount` is the raw-unit alternative. These are separate from `maxDeposit`, which bounds funds reserved per channel, not a cumulative budget across replacement channels. Explicit closes remain available outside the price and recipient policy.

Each stored channel belongs to one HTTP origin. Use a separate store per API origin and set `origin` when low-level `tempo.session()` needs to recover the server's balance snapshot for an existing channel. Managers pin their first request origin automatically and reject redirects and foreign response origins. Configure `trustedTempoProofRealms` when a manager's API uses a realm other than its hostname; this authorizes the realm alias, not another origin.

### Recovering channels after a restart

To reuse a channel after a browser reload or app restart, pass a [`channelStore`](/sdk/typescript/client/Method.tempo.session-manager#channelstore) to `tempo.session.manager()`. On the server, [`bootstrap: true`](/sdk/typescript/server/Method.tempo.session#with-same-route-bootstrap) lets a client find its previous channel before opening a new one.

Old persisted entries without `origin` are rejected. Verify the origin and descriptor independently before migrating them; don't relabel channels with the next request's origin. Keep funded entries for reconciliation. See [origin-bound recovery](/sdk/typescript/client/Method.tempo.session#origin-bound-recovery).

### Sessions only

To handle only Sessions and not one-time charges, register `tempo.session()` instead of `tempo()`. To handle both with separate options, register `tempo.charge()` and `tempo.session()` side by side.

### Accounts SDK

```ts twoslash
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: [
    // [!code hl:start]
    tempo.session({
      account: provider.getAccount({ signable: true }),
      getClient: provider.getClient,
      maxDeposit: '1',
    }),
    // [!code hl:end]
  ],
  polyfill: false,
})

const response = await mppx.fetch('https://api.example.com/v1/chat/completions')
```

### viem

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

const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef')

const mppx = Mppx.create({
  methods: [
    // [!code hl:start]
    tempo.session({
      account,
      maxDeposit: '1',
    }),
    // [!code hl:end]
  ],
  polyfill: false,
})

const response = await mppx.fetch('https://api.example.com/v1/chat/completions')
```

### Private payments

Sessions don't accept payments from a [Zone](https://docs.tempo.xyz/docs/zones) balance. A channel deposit comes from the payer's Tempo account, so the payer's address is public on Tempo. For per-request payments that keep the payer private, use [charge with `zones`](/guides/private-payments).

## Why Sessions

Usage-based services—LLM APIs, cloud infrastructure, metered data—don't know the final cost when a request starts. Billing after the fact adds credit risk and reconciliation; prepaid credits make the buyer guess and strand unused funds.

With Sessions, payment is part of each request. For an LLM API priced at 0.000025 OUSD per token, a buyer opens a channel with 10 OUSD, signs a voucher for each chunk it receives, and gets the unused deposit back when it closes. Managed streams check the live reserve before charging and delivering content; settlement runs separately on your configured schedule.

:::info[Why Tempo]
* **Fast, cheap channel operations**—Opening, topping up, and closing are on-chain. Tempo's ~500ms finality and sub-cent fees keep them out of the way.
* **Payment lanes**—Channel transactions use dedicated nonce lanes, so they don't block other account activity, and several server processes can share one signing account.
* **Fee sponsorship**—Your server can pay channel fees for buyers, so after the deposit the buyer side is entirely off-chain.
* **Native precompile**—[TIP-1034](https://tips.sh/1034) makes Sessions a Tempo precompile at `0x4D5050…` ("MPP"). It needs no token approval and stays in the payment lane under congestion.
:::

## Advanced options

### Custom reserve contracts

Servers use the canonical TIP-1034 precompile by default. To use a custom deployment, set `escrowContract` on the server; `mppx` advertises it in every Session Challenge.

```ts
const method = tempo.session({
  account,
  currencies: [OUSD],
  escrowContract: customReserve,
  store,
})
```

Clients reject noncanonical addresses by default. Set `allowCustomEscrow: true` only when you trust the server to pick the contract, or set `escrow` to pin the exact address you trust. A pin takes precedence, and an existing channel stays bound to its original address.

```ts
const method = tempo.session({
  account,
  allowCustomEscrow: true,
})
```

### Custom clients

`tempo.session.manager()` signs close Credentials for you. A custom client signs the close authorization with `Session.Precompile.Voucher.signCloseAuthorization()` from `mppx/tempo`; servers verify it with `verifyCloseAuthorization()`.

## Reserve precompile

Sessions use the [TIP-1034 precompile](https://tips.sh/1034) for deposits, settlement, top-ups, and closes. The IETF Specification documents the voucher format and HTTP flow.

| Network | Chain ID | Precompile address |
|---|---|---|
| Mainnet | 4217 | [`0x4d50500000000000000000000000000000000000`](https://explore.mainnet.tempo.xyz/address/0x4d50500000000000000000000000000000000000) |
| Testnet (Moderato) | 42431 | [`0x4d50500000000000000000000000000000000000`](https://explore.testnet.tempo.xyz/address/0x4d50500000000000000000000000000000000000) |

## Specification

[IETF Specification](https://paymentauth.org/draft-tempo-session-00) — Read the full specification
