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.
mppx 0.13.0 removed the deprecated Legacy Sessions manager exports.
Usage
import { } from 'mppx/client'
import { } from 'accounts'
const = .({ : false }) // Avoid double 402 handling; mppx is configured below.
await .({ : 'wallet_connect' })
const = ..({
: .({ : true }),
: [4217], // Tempo mainnet
: .,
: '1',
})
// Make a paid request — opens a channel on first call
const = await .('https://api.example.com/resource')
.(.)
200 // Access payment metadata
.(.)
.(.)
// Close the channel and settle on-chain
const = await .()import { } from 'mppx/client'
import { } from 'viem/accounts'
const = ('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef')
const = ..({
,
: [4217], // Tempo mainnet
: '1',
})
// Make a paid request — opens a channel on first call
const = await .('https://api.example.com/resource')
.(.)
200 // Access payment metadata
.(.)
.(.)
// Close the channel and settle on-chain
const = await .()If the server refreshes the Challenge during close, the manager signs the refreshed accepted cumulative amount. It rejects a close Receipt with an invalid amount, spend below the latest locally confirmed amount, or spend above the signed cumulative amount.
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.
import { } from 'mppx/client'
import { } from 'accounts'
const = .({ : false }) // Avoid double 402 handling; mppx is configured below.
await .({ : 'wallet_connect' })
const = ..({
: .({ : true }),
: .,
: '5',
})
const = await .('https://api.example.com/stream', {
() {
.('Receipt:', )
},
: .(30_000),
})
for await (const of ) {
.()
}
await .()import { } from 'mppx/client'
import { } from 'viem/accounts'
const = ..({
: ('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'),
: '5',
})
const = await .('https://api.example.com/stream', {
() {
.('Receipt:', )
},
: .(30_000),
})
for await (const of ) {
.()
}
await .()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.
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 url = new URL('wss://api.example.com/ws/chat')
url.searchParams.set('prompt', 'Tell me something interesting')
const socket = await session.ws(url, {
onReceipt(receipt) {
console.log('Receipt:', receipt)
},
})
socket.addEventListener('message', (event) => {
process.stdout.write(event.data)
})
await new Promise<void>((resolve) => {
socket.addEventListener('close', () => resolve(), { once: true })
})
const receipt = await session.close()import { tempo } from 'mppx/client'
import { privateKeyToAccount } from 'viem/accounts'
const session = tempo.session.manager({
account: privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'),
maxDeposit: '1',
})
const url = new URL('wss://api.example.com/ws/chat')
url.searchParams.set('prompt', 'Tell me something interesting')
const socket = await session.ws(url, {
onReceipt(receipt) {
console.log('Receipt:', receipt)
},
})
socket.addEventListener('message', (event) => {
process.stdout.write(event.data)
})
await new Promise<void>((resolve) => {
socket.addEventListener('close', () => resolve(), { once: true })
})
const receipt = await session.close()With top-up
Add deposit to the active channel without closing it.
import { } from 'mppx/client'
import { } from 'accounts'
const = .({ : false }) // Avoid double 402 handling; mppx is configured below.
await .({ : 'wallet_connect' })
const = ..({
: .({ : true }),
: .,
: '10',
})
await .('https://api.example.com/resource')
const = await .('2')
.(?.)
successimport { } from 'mppx/client'
import { } from 'viem/accounts'
const = ..({
: ('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'),
: '10',
})
await .('https://api.example.com/resource')
const = await .('2')
.(?.)
successWith 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.
import { , } from 'mppx/client'
import { } from 'accounts'
const = .({ : false }) // Avoid double 402 handling; mppx is configured below.
await .({ : 'wallet_connect' })
const = ()
const = ..({
: .({ : true }),
,
: .,
: '1',
})import { , } from 'mppx/client'
import { } from 'viem/accounts'
const = ()
const = ..({
: ('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'),
,
: '1',
})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() in mppx 0.12.0 and earlier. Close or settle existing Legacy channels before upgrading because current Sessions don't share their channel state.
Use tempo.session.manager() for direct lifecycle control, or tempo() when the same fetch wrapper handles one-time charges and Sessions.
import { , } from 'mppx/client'
import { , } from 'viem'
import { } from 'accounts'
import { as } from 'viem/chains'
const = .({ : false }) // Avoid double 402 handling; mppx is configured below.
await .({ : 'wallet_connect' })
const { : } = .({
: [
({
: .({ : true }),
: () =>
({
: ,
: ('https://rpc.tempo.xyz'),
}),
: '1',
}),
],
: false,
})
const = await ('https://api.example.com/resource')import { , } from 'mppx/client'
import { , } from 'viem'
import { } from 'viem/accounts'
import { as } from 'viem/chains'
const = ('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef')
const { : } = .({
: [
({
,
: () =>
({
: ,
: ('https://rpc.tempo.xyz'),
}),
: '1',
}),
],
: false,
})
const = await ('https://api.example.com/resource')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 = {
chainId: number
channelId: Hex
cumulativeAmount: bigint
deposit: bigint
descriptor: ChannelDescriptor
escrow: Address
opened: boolean
uncertainTopUp?: 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. |
uncertainTopUp | A dispatched top-up has an unknown outcome. Reconcile it against trusted chain state before another deposit or channel replacement. |
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
Pass signal to cancel discovery, Credential creation, or connection establishment. Cancellation rejects session.ws() and prevents authorization on a socket that hasn't opened. Closing or failing the socket before it opens also rejects the connection attempt.
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.
allowCustomEscrow (optional)
- Type:
boolean - Default:
false
Accepts a noncanonical reserve contract advertised by the server. Leave this disabled unless you trust the server's custom deployment. Persisted channel state remains bound to the resolved address.
allowedChainIds (optional)
- Type:
readonly number[]
Allowlist of Tempo chain IDs for Sessions, bootstrap proofs, and restored snapshots. Use [4217] for mainnet. A single entry supplies an omitted Challenge chain; multiple entries require the Challenge or resolved client to select one. An empty array rejects every chain.
The manager rejects a configured client, resolved client, or Session snapshot on a conflicting chain.
autoSwap (optional)
- Type:
boolean | { fallback?: boolean; maxAmountIn?: bigint; slippage?: number; tokenIn?: Address[] }
Automatically acquire the session currency from fallback stablecoins before opening or topping up a channel. The swap acquires only the missing balance. Use the object form to set a slippage percentage from 0 to 50, fallback token order, and a positive input-token base-unit maxAmountIn. The default input budget assumes 1:1 token units plus slippage; set maxAmountIn for non-parity pairs.
The default swap sources are pathUSD, USDC.e, and OUSD in that order. Set fallback: false with tokenIn to allow only those listed input tokens; otherwise the defaults follow your selected tokens.
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.
credentialContext (optional)
- Type:
unknown
Base method context supplied to every Credential the manager creates. The value must match the Sessions context schema. Manager-generated operation fields override matching fields in this context.
decimals (optional)
- Type:
number - Default:
6
Token decimals used to convert maxDeposit to raw units.
escrow (optional)
- Type:
Address
Exact reserve contract address pin. The server-advertised address must match this value, even when allowCustomEscrow is true.
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.
Automatic and manual funding decisions are serialized for managers sharing a store within one process. The manager refreshes the active deposit before each decision. A top-up request that times out after 30 seconds, fails, or is canceled after dispatch leaves uncertainTopUp in the channel store and blocks further deposits until you reconcile the pending transaction against trusted chain state. Coordinate store writes and funding across processes yourself.
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.