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.
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
}| Field | Description |
|---|---|
channelId | Latest known channel ID. Sent as a Payment-Session hint on the next request. |
cumulativeAmount | Latest local cumulative voucher authorization in raw token units. |
deposit | Latest known deposit in raw token units. |
descriptor | TIP-1034 descriptor used if the server cannot provide a snapshot. |
escrow | Reserve precompile address used to derive the channel ID. |
chainId | Tempo chain ID used to derive the channel ID and voucher domain. |
opened | Whether 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.
| Status | Meaning |
|---|---|
idle | No session Challenge has been handled yet. |
challenged | A tempo/session Challenge was selected. |
hydrating | A server snapshot is being used to restore a reusable channel. |
opening | An opening channel Credential is being created or submitted. |
active | Channel is open or hydrated and can sign vouchers. |
voucherNeeded | Server requested a larger voucher and no top-up is required. |
toppingUp | Server-requested cumulative spend exceeds current deposit. |
settling | Accepted voucher spend is being settled on-chain. |
closeRequested | Unilateral close has been requested and withdrawal is not yet available. |
withdrawable | Close delay elapsed and funds can be withdrawn. |
closing | Cooperative close Credential or close transaction is in flight. |
closed | Channel 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.