Skip to content
LogoLogo

tempo.session.manager

Sessions manager

Choose a signing account

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

wallet.ts
import { privateKeyToAccount } from 'viem/accounts'

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

Creates a Sessions manager that handles channel open, paid fetches, streaming, top-ups, and close.

Use this when application code needs direct lifecycle control for one channel. The manager opens a TIP-1034 channel lazily after the first 402 Challenge, signs cumulative vouchers, tops up when needed, and closes the channel when you call .close().

Use tempo.session() instead when you only need to register the current Sessions method inside Mppx.create. Use tempo() when the same fetch wrapper should handle both one-time charges and current Sessions.

Usage

With SSE streaming

Stream server-sent events with automatic voucher handling. The manager signs incremental vouchers as the server requests more payment during the stream.

With WebSocket streaming

Open a paid WebSocket session. The manager handles the HTTP 402 probe, channel open, in-band voucher signing, and payment control frames.

With top-up

Add deposit to the active channel without closing it.

With session resumption

Pass channelStore to persist channel hints between manager instances. The next manager sends the stored channel ID as a hint and hydrates from server snapshots when the server supports them.

In browsers, scope the key to the authenticated user and API origin so one user's channel hint is not reused for another user.

import { createJsonChannelStore, tempo } from 'mppx/client'
 
const storageKey = 'mppx-session:api.example.com:user-123'
 
const channelStore = createJsonChannelStore({
  delete(key) {
    localStorage.removeItem(`${storageKey}:${key}`)
  },
  get(key) {
    return localStorage.getItem(`${storageKey}:${key}`) ?? undefined
  },
  set(key, value) {
    localStorage.setItem(`${storageKey}:${key}`, value)
  },
})
 
const session = tempo.session.manager({
  account,
  channelStore,
  client,
  bootstrap: true,
  maxDeposit: '1',
})

When bootstrap: true is enabled, the manager first sends a same-route HEAD request if it has no active channel and no stored channel. Servers that enable bootstrap can answer with a $0 identity Challenge and then return a Payment-Session-Snapshot header. The manager hydrates from that snapshot and persists it through channelStore.

Migrate from Legacy Sessions

Legacy Sessions clients used tempo.sessionLegacy() or tempo.sessionLegacy.method(). Use tempo.session.manager() for a standalone manager, or tempo() when you want the same fetch wrapper to handle one-time charges and Sessions.

Current Sessions and Legacy Sessions do not share channel state. Existing Legacy channels should be closed or settled through tempo.sessionLegacy; new channels should be opened through tempo.session or tempo.session.manager(). During a rolling migration, register both tempo.session() and tempo.sessionLegacy.method() if the client may talk to both server versions.

Use tempo.sessionLegacy() only while the server still emits Legacy Sessions Challenges.

Return type

tempo.session.manager() returns a SessionManager object:

type SessionManager = {
  readonly channelId: Hex | undefined
  readonly cumulative: bigint
  readonly opened: boolean
  readonly state: SessionState
  close(): Promise<SessionReceipt | undefined>
  fetch(input: RequestInfo | URL, init?: RequestInit): Promise<PaymentResponse>
  sse(input: RequestInfo | URL, init?: SessionManagerSseOptions): Promise<AsyncIterable<string>>
  topUp(amount: string | bigint): Promise<SessionReceipt | undefined>
  ws(input: string | URL, init?: SessionManagerWebSocketOptions): Promise<SessionManagedWebSocket>
}

ChannelStore

channelStore is optional client persistence for channel hints. It does not authorize payment by itself; the server still verifies the channel snapshot and voucher signatures.

type ChannelStore = {
  get(key: string): Promise<ChannelEntry | undefined> | ChannelEntry | undefined
  set(entry: ChannelEntry): Promise<void> | void
  delete(key: string): Promise<void> | void
}

ChannelEntry

type ChannelEntry = {
  channelId: Hex
  cumulativeAmount: bigint
  deposit: bigint
  descriptor: ChannelDescriptor
  escrow: Address
  chainId: number
  opened: boolean
}
FieldDescription
channelIdLatest known channel ID. Sent as a Payment-Session hint on the next request.
cumulativeAmountLatest local cumulative voucher authorization in raw token units.
depositLatest known deposit in raw token units.
descriptorTIP-1034 descriptor used if the server cannot provide a snapshot.
escrowReserve precompile address used to derive the channel ID.
chainIdTempo chain ID used to derive the channel ID and voucher domain.
openedWhether the channel was open when stored. Closed channels are ignored.

SessionSnapshot

SessionSnapshot is the server-provided channel state used by bootstrap and channelStore hydration.

type SessionSnapshot = {
  acceptedCumulative: string
  chainId: number
  channelId: Hex
  closeRequestedAt?: string
  deposit: string
  descriptor: ChannelDescriptor
  escrow: Address
  requiredCumulative: string
  settled: string
  spent: string
  units?: number
}

Snapshot helpers

Use these helpers when you need to read or write the Payment-Session-Snapshot header yourself.

import { tempo } from 'mppx/client'
 
const header = tempo.session.manager.serializeSnapshot(snapshot)
const snapshot = tempo.session.manager.deserializeSnapshot(header)

PaymentResponse

session.fetch() returns a standard Response extended with payment metadata:

type PaymentResponse = Response & {
  challenge: TempoSessionChallenge | null
  channelId: Hex | null
  cumulative: bigint
  receipt: SessionReceipt | null
}

SessionManagerSseOptions

session.sse() accepts standard RequestInit fields plus Receipt and cancellation hooks.

type SessionManagerSseOptions = RequestInit & {
  onReceipt?: (receipt: SessionReceipt) => void
  signal?: AbortSignal
}

SessionManagerWebSocketOptions

type SessionManagerWebSocketOptions = {
  onReceipt?: (receipt: SessionReceipt) => void
  protocols?: string | string[]
  signal?: AbortSignal
}

SessionManagedWebSocket

session.ws() returns a managed WebSocket facade. Payment protocol frames are handled internally; application listeners only receive application messages and close/error/open events.

type SessionManagedWebSocket = {
  readonly bufferedAmount: number
  readonly extensions: string
  readonly protocol: string
  readonly readyState: number
  readonly url: string
  onclose: ((event: CloseEvent) => void) | null
  onerror: ((event: Event) => void) | null
  onmessage: ((event: MessageEvent<string>) => void) | null
  onopen: ((event: Event) => void) | null
  addEventListener(type: string, listener: EventListener, options?: boolean | AddEventListenerOptions): void
  close(code?: number, reason?: string): void
  removeEventListener(type: string, listener: EventListener): void
  send(data: string): void
}

SessionState

session.state exposes the reducer state for UI, logs, or app-level orchestration.

StatusMeaning
idleNo session Challenge has been handled yet.
challengedA tempo/session Challenge was selected.
hydratingA server snapshot is being used to restore a reusable channel.
openingAn opening channel Credential is being created or submitted.
activeChannel is open or hydrated and can sign vouchers.
voucherNeededServer requested a larger voucher and no top-up is required.
toppingUpServer-requested cumulative spend exceeds current deposit.
settlingAccepted voucher spend is being settled on-chain.
closeRequestedUnilateral close has been requested and withdrawal is not yet available.
withdrawableClose delay elapsed and funds can be withdrawn.
closingCooperative close Credential or close transaction is in flight.
closedChannel close finalized.

SessionReceipt

The Receipt returned by session.close() and available on PaymentResponse.receipt:

type SessionReceipt = {
  acceptedCumulative: string
  challengeId: string
  channelId: Hex
  intent: 'session'
  method: 'tempo'
  reference: string
  spent: string
  status: 'success'
  timestamp: string
  txHash?: Hex
  units?: number
}

Parameters

account (optional)

  • Type: Account

Account to sign channel transactions and vouchers with.

autoSwap (optional)

  • Type: boolean | { slippage?: number; tokenIn?: Address[] }

Automatically acquire the session currency from fallback stablecoins before opening or topping up a channel. Use the object form to set a maximum slippage percentage and the fallback token order.

bootstrap (optional)

  • Type: boolean

Enables a same-route HEAD bootstrap from a server session snapshot before opening a new channel.

channelStore (optional)

  • Type: ChannelStore

Store for reusable session channels. Defaults to an in-memory store.

client (optional)

  • Type: Client

Viem client instance. Shorthand for getClient: () => client.

decimals (optional)

  • Type: number
  • Default: 6

Token decimals used to convert maxDeposit to raw units.

escrow (optional)

  • Type: Address

Reserve precompile address override.

fetch (optional)

  • Type: typeof globalThis.fetch
  • Default: globalThis.fetch

Custom fetch function for HTTP probes, management posts, and paid retries.

getClient (optional)

  • Type: (parameters: { chainId?: number }) => MaybePromise<Client>

Function that returns a viem client for the given chain ID.

maxDeposit (optional)

  • Type: string

Maximum deposit in human-readable units. Converted to raw units via decimals.

topUpAmount (optional)

  • Type: string

Preferred automatic top-up size in human-readable units. When omitted, mppx uses a bounded server suggestedDeposit, then the exact shortfall.

webSocket (optional)

  • Type: WebSocketConstructor

WebSocket constructor for runtimes without a global WebSocket.