Claude Skill

mpp

Build with MPP (Machine Payments Protocol) - the open protocol for machine-to-machine payments over HTTP 402. Use when building paid APIs, payment-gated content or endpoints, AI agent payment flows, MCP tool payments, pay-per-token streaming, or metered pay-as-you-go billing. Cov

LLM Mart · 0 points · 18 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download tenequm-skills-skills_mpp-1ff2284.zip · 76 KB
Part of tenequm/skills — 25 skills

Install

skills CLI npx skills add https://github.com/tenequm/skills/tree/main/skills/mpp
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install tenequm-skills@llmmart
Git git clone https://github.com/tenequm/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole tenequm/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

MPP - Machine Payments Protocol

MPP is an open protocol (co-authored by Tempo and Stripe) that standardizes HTTP 402 Payment Required for machine-to-machine payments. Clients pay in the same HTTP request - no accounts, API keys, or checkout flows needed.

The core protocol spec is submitted to the IETF as the Payment HTTP Authentication Scheme.

Code in this skill uses placeholder token names (<USDC_TEMPO_MAINNET>, <PATHUSD_TESTNET>); the real addresses live in the Tempo documentation and references/tempo-method.md.

Core Architecture

Three primitives power every MPP payment:

  1. Challenge - server-issued payment requirement (in WWW-Authenticate: Payment header)
  2. Credential - client-submitted payment proof (in Authorization: Payment header)
  3. Receipt - server confirmation of successful payment (in Payment-Receipt header)

Payment Methods & Intents

MPP is payment-method agnostic. Each method defines its own settlement rail:

Method Rail SDK Package Status
Tempo TIP-20 stablecoins on Tempo chain mppx (built-in) Production
Stripe Cards/wallets (SPT) + on-chain crypto deposit mppx (built-in) Production
EVM EIP-3009 stablecoin authorizations (x402-exact compatible) mppx (built-in) Production
Lightning Bitcoin over Lightning Network @buildonspark/lightning-mpp-sdk Production
Stellar SEP-41 tokens on Stellar, charge + channel @stellar/mpp Production (channel wire spec still being drafted - subject to change)
Solana Solana-native charge + session (SOL, SPL, Token-2022) @solana/mpp Production
Monad Monad charge (ERC-3009, settlement modes) @monad-crypto/mpp Production
NEAR Intents Cross-chain charge via 1Click deposit addresses @defuse-protocol/nearintents-mpp-sdk Production (not trustless - routes through a settlement backend, advertised as methodDetails.settlementBackend: "near-intents" for per-method risk policy)
RedotPay RedotPay balance (rdt) or stablecoin proof, charge only @redotpay/mpp Production
Card Encrypted network tokens (Visa) mpp-card Production
Custom Any rail Method.from() + Method.toClient/toServer Extensible

Per-method deep dives: references/tempo-method.md, references/stripe-method.md, references/lightning-method.md, references/custom-methods.md.

Intent Pattern Best For
charge One-time payment per request API calls, content access, fixed-price endpoints
session Pay-as-you-go over payment channels LLM streaming, metered billing, high-frequency APIs
subscription Recurring access via an authorized key (Tempo) - see references/subscriptions.md Plans/tiers where access is separated from per-request billing

Quick Start: Server (TypeScript)

import { Mppx, tempo } from 'mppx/server'

const mppx = Mppx.create({
  methods: [tempo({
    currency: '<PATHUSD_TESTNET>', // pathUSD testnet
    recipient: '0xYourAddress',
  })],
})

export async function handler(request: Request) {
  const result = await mppx.charge({ amount: '0.01' })(request)
  if (result.status === 402) return result.challenge
  return result.withReceipt(Response.json({ data: '...' }))
}

Install: npm install mppx viem (mppx 0.8.15 requires viem >= 2.54.0).

Validate the finished server end-to-end with npx mppx validate http://localhost:3000.

Quick Start: Client (TypeScript)

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

// Polyfills globalThis.fetch to handle 402 automatically
Mppx.create({
  methods: [tempo({ account: privateKeyToAccount('0x...') })],
})

const res = await fetch('https://api.example.com/paid')
// Payment happens transparently when server returns 402

In browsers, mppx 0.6.0 changed the default: polyfilled fetch only sends Accept-Payment to same-origin endpoints, so cross-origin paid APIs need acceptPaymentPolicy ('always' / { origins: [...] }). Client fetch retries incremental challenges up to maxPaymentRetries (default 3). For non-global alternatives (Fetch.from/polyfill/restore, Mppx.restore()), see references/typescript-sdk.md.

Quick Start: Server (Python)

from fastapi import FastAPI
from mpp import Credential, Receipt
from mpp.server import Mpp
from mpp.methods.tempo import tempo, ChargeIntent

app = FastAPI()
server = Mpp.create(method=tempo(
    currency="<PATHUSD_TESTNET>",
    recipient="0xYourAddress", intents={"charge": ChargeIntent()},
))

@app.get("/resource")
@server.pay(amount="0.50")
async def get_resource(request, credential: Credential, receipt: Receipt):
    return {"data": "paid content", "payer": credential.source}

Install: pip install "pympp[tempo]". See references/python-sdk.md for full patterns.

Quick Start: Server (Rust)

Install: cargo add mpp --features tempo,server. See references/rust-sdk.md for full patterns.

Framework Middleware (TypeScript)

Each framework has its own import (mppx/nextjs, mppx/hono, mppx/express, mppx/elysia):

// Next.js
import { Mppx, tempo } from 'mppx/nextjs'
const mppx = Mppx.create({ methods: [tempo({ currency: '<PATHUSD_TESTNET>', recipient: '0x...' })] })
export const GET = mppx.charge({ amount: '0.1' })(() => Response.json({ data: '...' }))

// Hono
import { Mppx, tempo } from 'mppx/hono'
app.get('/resource', mppx.charge({ amount: '0.1' }), (c) => c.json({ data: '...' }))

See references/typescript-sdk.md for Express and Elysia examples.

Sessions: Pay-as-You-Go Streaming

Sessions open a payment channel once, then use off-chain vouchers for each request - no blockchain transaction per request. Sub-100ms latency, near-zero per-request fees.

Sessions v2 (default since mppx 0.7.0): tempo.session() is the TIP-1034 precompile channel flow; the earlier escrow-contract implementation is Sessions v1, still available as the deprecated tempo.sessionLegacy. A v2-expecting client rejects a v1 session and falls back to the charge path, so keep client and server on matching flows. Two client APIs: tempo.session({ account, maxDeposit }) registers the method with Mppx.create() (transparent 402 handling via fetch), while tempo.session.manager({ account, maxDeposit }) returns a managed client for direct lifecycle control (.sse(), .close()).

// Server - session endpoint with automatic settlement
const mppx = Mppx.create({
  methods: [tempo.session({
    currency: '<PATHUSD_TESTNET>', recipient: '0x...',
    store: Store.redis(redis),
    settlementSchedule: { amount: '1.00', intervalMs: 300_000 },
    bootstrap: true, // let returning clients recover their channel on this route
  })],
})
const result = await mppx.session({ amount: '0.001', unitType: 'token' })(request)
if (result.status === 402) return result.challenge
return result.withReceipt(Response.json({ data: '...' }))
// Server - SSE streaming with per-word billing
export const GET = mppx.session({ amount: '0.001', unitType: 'word' })(
  async () => async function* (stream) {
    for (const word of ['hello', 'world']) {
      await stream.charge()
      yield word
    }
  }
)

// Client - session with auto-managed channel
Mppx.create({ methods: [tempo({ account, maxDeposit: '1' })] })
const res = await fetch('http://localhost:3000/api/resource')
// 1st request: opens channel on-chain; 2nd+: off-chain vouchers

Sessions also stream over WebSocket via Ws.serve(). See references/sessions.md for the full lifecycle, settlement, stores, SSE and WebSocket patterns, and channel recovery.

Multi-Method Support

Accept Tempo stablecoins, Stripe cards, and Lightning Bitcoin on a single endpoint:

const mppx = Mppx.create({
  methods: [
    tempo({ currency: '<PATHUSD_TESTNET>', recipient: '0x...' }),
    stripe.charge({ client: new Stripe(key), networkId: 'profile_...', paymentMethodTypes: ['card'] }),
    spark.charge({ mnemonic: process.env.MNEMONIC! }),
  ],
})

Use Mppx.compose() to present multiple methods in a single 402 response with per-route pricing. Apply the same branch at the challenge site and the verification site, or the 402 advertises fewer options than the server accepts. See references/typescript-sdk.md.

Payment Links (HTML)

Setting html: true on a payment method config renders a browser-friendly payment page when a 402 endpoint is visited in a browser, with theming, multi-method compose tabs, and Solana wallet support. Service workers handle credential submission, then the page reloads with the paid response.

Customize via mppx/html exports (Config, Text, Theme), and build a custom method's payment link with Html.init(methodName).

Zero-Dollar Auth (Proof Credentials)

Authenticate agent identity without payment. Clients sign an EIP-712 proof over the challenge ID instead of creating a transaction - no gas burned, no funds transferred.

// Server - zero-dollar charge, with a store for replay protection
const mppx = Mppx.create({
  methods: [tempo.charge({ currency: '<PATHUSD_TESTNET>', recipient: '0x...', store })],
})
const result = await mppx.charge({ amount: '0' })(request)

Since mppx 0.8.0 these proofs are bound to the payer wallet: the EIP-712 Proof typed data (exposed as tempo.Proof) carries an account field at domain version 3, so a proof signed for one account no longer verifies against another.

Use cases: identity verification, long-running job polling, paid unlock with free subsequent access, multi-step agent pipelines. See mpp.dev/advanced/identity.

Payments Proxy

Gate existing APIs behind MPP payments:

// import { openai, Proxy } from 'mppx/proxy' - a service inside Proxy.create({ services: [...] })
openai({
  apiKey: process.env.OPENAI_API_KEY,
  routes: {
    'POST /v1/chat/completions': mppx.charge({ amount: '0.05' }),
    'GET /v1/models': true, // literal `true` marks a free route
  },
})

Built-in presets openai(), anthropic(), stripe(), plus custom() for any upstream. See references/discovery-and-proxy.md for Proxy.create(), the discovery endpoints it serves, and the discovery() helper for non-proxy servers.

MCP Transport

MCP tool calls can require payment using JSON-RPC error code -32042 (servers may also issue -32043):

// Server - import tempo from mppx/server, NOT mppx/tempo
import { McpServer } from 'mppx/mcp/server'
import { tempo } from 'mppx/server'
const server = McpServer.wrap(baseServer, { methods: [tempo.charge({ /* ... */ })], secretKey })

// Client - payment-aware MCP client (import tempo from mppx/client)
import { McpClient } from 'mppx/mcp/client'
import { tempo } from 'mppx/client'
const mcp = McpClient.wrap(client, { methods: [tempo({ account })] })
const result = await mcp.callTool({ name: 'premium_tool', arguments: {} })

MCP-over-HTTP challenges settle in the same payment-aware fetch, and transports are pluggable via Transport.from/http/mcp/mcpSdk on both sides. See references/transports.md.

Privy Server Wallets

createViemAccount from @privy-io/node/viem (needs @privy-io/node >= 0.20.0) returns a viem Account backed by a Privy server wallet, so it drops into tempo({ account }) wherever a local account would go.

Server-side signing works with app-owned server wallets; user-owned embedded wallets require authorization keys or key quorums. See references/typescript-sdk.md for the full setup and the manual toAccount() construction.

Testing & CLI

# Create an account (stored in keychain), then fund it on testnet
npx mppx account create
npx mppx account fund --network testnet

# Make a paid request
npx mppx http://localhost:3000/resource

# Parse a challenge without signing it
npx mppx sign --dry-run --challenge '<www-authenticate value>'

# Validate a server implementation end-to-end
npx mppx validate http://localhost:3000

The CLI also covers init, sessions (list/view/close), discover, services, mcp add, and skills add. Config comes from MPPX_CONFIG or an explicit --config - there is no auto-discovery from the working directory. Full reference: references/cli.md.

SDK Packages

Language Package Install
TypeScript mppx npm install mppx
Python pympp pip install "pympp[tempo]"
Rust mpp cargo add mpp --features tempo,client,server
Ruby mpp-rb (official, by Stripe) see repo for gem name
Go mpp-go (official, by Tempo) go get github.com/tempoxyz/mpp-go
Elixir mpp (community) hex.pm/packages/mpp
Swift mpp-swift (community) see repo

Capability notes, checked against SDK source rather than the docs matrices (upstream publishes two that disagree):

  • Session intent: TypeScript and Rust only.
  • Proof Credentials (zero-dollar auth): TypeScript, Rust, and Ruby. Not pympp - the Python Tempo method implements only hash and transaction payload types.
  • Stripe, MCP, and event handling: TypeScript, Python, Rust, Ruby. Not the official mpp-go, which ships client/server/charge/fee-sponsorship/proof with net/http, Gin, Echo, and Chi middleware. A separate community Go mppx (cp0x) also exists.

Go and Ruby have first-class SDK doc pages at mpp.dev/sdk/go and mpp.dev/sdk/ruby.

Always import Mppx and tempo from the subpath matching your context (mppx/server, mppx/client, or the framework subpath). Note: Mppx and tempo are NOT exported from mppx/tempo - that subpath only exports Session and Ws. The authoritative subpath table is in references/typescript-sdk.md.

Key Concepts

  • Challenge/Credential/Receipt: The three protocol primitives. Challenge IDs are HMAC-SHA256 bound to prevent tampering. See references/protocol-spec.md
  • Split payments: One charge across multiple recipients in a single transaction (1-10 splits, per-split memos, expectedRecipients). See references/tempo-method.md
  • Fee sponsorship: Server pays gas on behalf of clients, capped by maxInFlightReservations / maxInFlightTotalFee
  • Relays: Delegate credential validation and broadcast to Tempo API or a compatible relay via tempo.charge({ relay })
  • Push/pull modes: Client broadcasts the transaction (push) or the server does (pull)
  • Client chain pinning: tempo.charge({ expectedChainId }) rejects challenges for the wrong Tempo network
  • Reusable client channels: pass a channelStore to persist and reuse payer session channels across processes
  • x402 interop: evm.charge({ x402: { facilitator } }) serves native MPP and x402 "exact" challenges from one route; the client prefers Payment-auth challenges
  • Custom methods: Implement any payment rail with Method.from(). See references/custom-methods.md

Payment Hooks

Attach logging, metrics, or tracing without touching the handler. Register on the object returned by Mppx.create(); each registration returns an unsubscribe function.

  • Server (mppx/server): onChallengeCreated, onPaymentSuccess, onPaymentFailed, onSessionSettlement, on('*')
  • Client (mppx/client): onChallengeReceived, onCredentialCreated, onPaymentResponse, onPaymentFailed

Server handlers are awaited inline on the request path - keep them fast. onPaymentFailed is the practical way to see the real error behind an opaque 402. See references/typescript-sdk.md and mpp.dev/advanced/payment-hooks.

Managing Agent Spend

Bound an agent's payment authority with Tempo access keys - delegated signing keys with built-in spend controls, their own expiry, and a revocation path.

import { Expiry } from 'accounts'
import { numberToHex, parseUnits } from 'viem'
import { Scopes } from 'viem/tempo'

const accessKey = {
  expiry: Expiry.days(7),
  limits: [{ token: usdc, limit: numberToHex(parseUnits('10', 6)), period: 86_400 }], // 10 USDC/day
  scopes: [Scopes.tip20(usdc).transfer({ recipients: [recipientAddress] })],
}
// Authorize: provider.request({ method: 'wallet_connect', params: [{ capabilities: { authorizeAccessKey: accessKey } }] })

Mppx.create({
  methods: [tempo({
    account: provider.getAccount(),
    ...provider.getMppxParameters({ accessKey: accessKeyAddress }),
  })],
})

Spend limits are hex-encoded - pass numberToHex(parseUnits(...)), not a raw bigint. Separate keys per app/tool/deployment keep delegated runtimes isolated. See mpp.dev/guides/managing-agent-spend and Tempo access keys.

Production Gotchas

The failure modes that cost the most time. Full detail in references/production-gotchas.md:

  • Tempo has no native gas token. Set feeToken or call setUserToken, or transactions fail with gas_limit: 0. "Fund with ETH" errors mean "fund with the stablecoin fee token"
  • Sessions do not settle themselves. Configure settlementSchedule or run your own tempo.settle() / tempo.settleBatch() sweep, paired with a close policy for idle channels - otherwise revenue accrues as unredeemed vouchers and channels stay open holding payer deposits
  • Charge settles before your handler runs. Use validateCredential then broadcastCredential when payment should depend on the work succeeding. Challenges expire after 5 minutes by default
  • Never use Store.memory() in production. Lost channel state means deposits stay reserved indefinitely
  • Set realm explicitly. Env vars outrank the per-request hostname, and Kubernetes HOSTNAME rotates every deploy, breaking mppscan attribution
  • Session voucher, close, and topUp credentials are bodyless POSTs, so a body validator running before mppx.session() rejects them with a spurious 400. Clone the request before reading its body, or mppx sees an empty one and returns 402
  • Large 402 headers overflow nginx's 4k default buffer and surface as 502

References

File Content
references/protocol-spec.md Challenge/Credential/Receipt, status codes, security
references/typescript-sdk.md mppx: server, client, middleware, transports, stores
references/cli.md mppx CLI: requests, validate, sign, accounts, config
references/production-gotchas.md Field-tested failure modes and their fixes
references/sessions.md Channels, vouchers, settlement, SSE/WS, recovery
references/subscriptions.md Subscription intent: activation, renewal, cancellation
references/tempo-method.md Tempo: fees, relays, push/pull, splits, sessions
references/stripe-method.md Stripe: SPT fiat flow, crypto deposit, Elements
references/discovery-and-proxy.md Proxy services, discovery documents, registries
references/transports.md HTTP, MCP, and WebSocket transport bindings
references/python-sdk.md pympp: @server.pay, async client, charge intent
references/rust-sdk.md mpp Rust: server/client, features, sessions
references/lightning-method.md Lightning: BOLT11 charge, bearer sessions, Spark
references/custom-methods.md Method.from, toClient, toServer patterns

Official Resources

Files (skills)
  • references
    • cli.md 5.8 KB
      # mppx CLI
      
      Verified against mppx 0.8.15 (`mppx --help`).
      
      ## Making Requests
      
      ```bash
      # Paid request (payment handled automatically)
      npx mppx https://api.example.com/data
      
      # POST with a JSON body
      npx mppx -X POST -J '{"prompt":"hello"}' https://api.example.com/chat
      
      # Show response headers, then full negotiation detail
      npx mppx -i https://api.example.com/data
      npx mppx -vv https://api.example.com/data
      ```
      
      Key root flags:
      
      | Flag | Purpose |
      |---|---|
      | `--account, -a` | Account name (env: `MPPX_ACCOUNT`) |
      | `--config, -c` | Path to config file (env: `MPPX_CONFIG`) |
      | `--data, -d` / `--json-body, -J` | Request body / JSON body (implies POST) |
      | `--header, -H` | Add header (repeatable) |
      | `--method, -X` | HTTP method |
      | `--method-opt, -M` | Method-specific option, `key=value` (repeatable) |
      | `--network` | `mainnet` or `testnet` |
      | `--rpc-url, -r` | RPC endpoint (env: `MPPX_RPC_URL`, falls back to `RPC_URL`) |
      | `--session` | Session selection: `auto` (default), `new`, or a channel ID |
      | `--auto-swap` / `--pay-with` / `--slippage` | Tempo auto-swap controls |
      | `--include, -i` | Include response headers in output |
      | `--verbose, -v` | Verbosity (`-v` details, `-vv` headers) |
      | `--currency` | Payment currency/token address to select |
      | `--confirm` | Show confirmation prompts |
      
      Global output flags apply to every command: `--format <toon|json|yaml|md|jsonl>`, `--filter-output`, `--full-output`, `--token-count`, `--token-limit`, `--token-offset`, `--schema`, and `--llms` / `--llms-full` for an LLM-readable manifest. `--mcp` runs the CLI itself as an MCP stdio server.
      
      ## Inspecting a Challenge Without Paying
      
      There is no `--inspect` flag. Use `mppx sign --dry-run`, which parses and validates a challenge without signing:
      
      ```bash
      # Parse a challenge without signing it
      npx mppx sign --dry-run --challenge 'Payment id="...", method="tempo", ...'
      
      # Or sign it and emit the Authorization header value
      curl -si https://api.example.com/data | grep -i www-authenticate | npx mppx sign
      ```
      
      `mppx sign` accepts the challenge via `--challenge/-C` or on stdin, and shares the account, network, RPC, and auto-swap flags with the root command.
      
      ## Validating a Server
      
      `mppx validate` runs an end-to-end conformance check against an MPP server: discovery, challenge formats, error handling, and the full payment flow.
      
      ```bash
      npx mppx validate http://localhost:4242
      
      # Skip discovery and test one endpoint directly
      npx mppx validate http://localhost:4242 -e POST:/paid --body '{"prompt":"hi"}'
      ```
      
      | Flag | Purpose |
      |---|---|
      | `--endpoint, -e` | Endpoint to test as `METHOD:path`; skips discovery |
      | `--body` | Request body. In discovery mode, JSON keyed by path is a per-path mapping |
      | `--query` | Query parameter `key=value` (repeatable) |
      | `--header, -H` | Request header `key:value` (repeatable) |
      | `--yes, -y` | Auto-approve mainnet payments |
      | `--output-json, -j` | JSON output (auto-enabled in known agent environments) |
      | `--verbose, -v` | Verbosity level |
      
      On testnets and Stripe test mode the CLI completes roundtrip test payments automatically. On mainnet it can complete **real** payments from the local wallet, which is why `--yes` exists as an explicit opt-in. Run it against both a sandbox and a production build of your server.
      
      The same checks are available programmatically through the `mppx/validation` export, for wiring conformance into CI.
      
      ## Accounts
      
      ```bash
      npx mppx account create           # create (stored in system keychain)
      npx mppx account list             # list all accounts
      npx mppx account view             # show account address
      npx mppx account default          # set the default account
      npx mppx account export           # export a local account's private key
      npx mppx account fund --network testnet   # fund with testnet tokens
      npx mppx account delete
      ```
      
      `mppx account fund` is testnet-only; the mainnet option was removed in 0.8.6.
      
      ## Sessions
      
      ```bash
      npx mppx sessions list            # list persistent payment sessions
      npx mppx sessions view <id>
      npx mppx sessions close <id>      # settle and close a channel
      ```
      
      The CLI reuses sessions automatically (`--session auto`). Pass `--session new` to force a fresh channel or `--session <channelId>` to target a specific one. Session state lives in the Tempo Wallet channel database, which the `mppx/client/node` SQLite `ChannelStore` also reads.
      
      ## Discovery and Services
      
      ```bash
      npx mppx discover generate        # generate a discovery document
      npx mppx discover validate        # validate one
      npx mppx services                 # browse the MPP services registry
      ```
      
      ## Integrations
      
      ```bash
      npx mppx init                     # create an mppx.config.ts in the current directory
      npx mppx mcp add                  # register mppx as an MCP server
      npx mppx mcp doctor               # diagnose MCP registration
      npx mppx skills add               # sync MPP skill files to your agents
      npx mppx skills list
      npx mppx completions              # shell completion script
      ```
      
      ## Config File
      
      Extend the CLI with custom payment methods via `mppx.config.(js|mjs|ts)`:
      
      ```typescript
      // mppx.config.ts
      import { defineConfig } from 'mppx/cli'
      export default defineConfig({ plugins: [myCustomMethod()] })
      ```
      
      Config resolution has exactly two paths: the `MPPX_CONFIG` env var, or an explicit `--config ./mppx.config.ts`. There is **no** auto-discovery from the current or parent directories (removed in 0.8.1), so a config file sitting in the working directory is ignored unless you point at it.
      
      Plugin authoring helpers live in `mppx/cli/plugins`. Note this is a module export, not a CLI verb - there is no `mppx plugins` command.
      
      ## Environment Variables
      
      | Variable | Purpose |
      |---|---|
      | `MPPX_ACCOUNT` | Default account name |
      | `MPPX_CONFIG` | Path to the config file |
      | `MPPX_RPC_URL` | RPC endpoint (takes precedence) |
      | `RPC_URL` | RPC endpoint fallback |
      
    • custom-methods.md 9.4 KB
      # Custom Payment Methods
      
      ## Overview
      
      MPP supports dynamic extensibility through custom payment methods. Any payment rail - other blockchains, card processors, proprietary billing systems, loyalty points - can be integrated by implementing three pieces:
      
      1. **Method definition** - schema describing the credential and request shapes
      2. **Client implementation** - how to create a credential (proof of payment)
      3. **Server implementation** - how to verify a credential
      
      Custom methods plug into the same `Mppx.create()` pipeline and work across all transports (HTTP, MCP, JSON-RPC) and frameworks (Hono, Express, Next.js, etc.) without changes.
      
      ---
      
      ## Define a Method
      
      Use `Method.from()` to define a new payment method with its name, intent, and Zod schemas:
      
      ```ts
      import { Method } from 'mppx'
      import { z } from 'zod'
      
      const lightningCharge = Method.from({
        intent: 'charge',
        name: 'lightning',
        schema: {
          credential: {
            payload: z.object({
              preimage: z.string().length(64), // 32-byte hex
            }),
          },
          request: z.object({
            invoice: z.string(), // BOLT11 invoice
            paymentHash: z.string().length(64),
            network: z.enum(['mainnet', 'regtest']).default('mainnet'),
          }),
        },
      })
      ```
      
      `Method.from()` returns a base method definition. It has no behavior - just the type contract that both client and server implementations must satisfy.
      
      **Parameters:**
      
      | Field | Type | Description |
      |---|---|---|
      | `intent` | `string` | Payment intent (e.g. `'charge'`, `'session'`) |
      | `name` | `string` | Method identifier (e.g. `'lightning'`, `'loyalty-points'`) |
      | `schema.credential.payload` | `ZodType` | Zod schema for the credential payload |
      | `schema.request` | `ZodType` | Zod schema for the challenge request fields |
      
      ---
      
      ## Client Implementation
      
      Use `Method.toClient()` to add credential creation logic to a base method:
      
      ```ts
      const lightningClient = Method.toClient(lightningCharge, {
        async createCredential({ challenge, context }) {
          const request = challenge.request // decoded request object
          const invoice = request.invoice
      
          // Pay the invoice and get the preimage
          const preimage = await payInvoice(invoice)
      
          return {
            payload: { preimage },
          }
        },
      })
      ```
      
      `createCredential` receives:
      - `challenge` - the parsed challenge from the server's 402 response
      - `context` - optional context passed from the application (e.g. wallet instance, user preferences)
      
      It must return a serialized credential matching the `schema.credential.payload` Zod schema.
      
      ---
      
      ## Server Implementation
      
      Use `Method.toServer()` to add verification logic to a base method.
      
      **Note on `verify` vs `validate` + `broadcast`.** The single `verify` hook shown below is now marked deprecated upstream: it "combines both operations and may consume payment state, so it cannot support a safe pre-check endpoint." The replacement splits it in two - `validate` for a non-mutating check that the credential satisfies the challenge, and `broadcast` for settlement:
      
      ```ts
      const lightningServer = Method.toServer(lightning, {
        async validate({ credential, challenge }) {
          // non-mutating: does this credential satisfy the challenge?
          assertPreimageMatches(credential.payload.preimage, challenge.request.paymentHash)
        },
        async broadcast({ credential, challenge }) {
          // mutating: settle and return the receipt
          return {
            reference: challenge.request.paymentHash,
            settlement: { amount: String(challenge.request.amount), currency: 'BTC' },
          }
        },
      })
      ```
      
      `verify` still works and is still required by the type, so existing methods keep functioning. Implement the split pair for new methods, and for any method that should support a pre-check endpoint or hold settlement until downstream work succeeds. The rest of this page shows the `verify` form, which remains the simplest illustration of the contract.
      
      ```ts
      import { createHash } from 'node:crypto'
      
      const lightningServer = Method.toServer(lightningCharge, {
        async verify({ credential, challenge }) {
          const { preimage } = credential.payload
          const { paymentHash } = challenge.request
      
          // Verify: sha256(preimage) == paymentHash
          const hash = createHash('sha256')
            .update(Buffer.from(preimage, 'hex'))
            .digest('hex')
      
          if (hash !== paymentHash) {
            throw new Error('Invalid preimage: hash mismatch')
          }
      
          return {
            reference: paymentHash,
            settlement: {
              amount: challenge.request.amount,
              currency: 'BTC',
            },
          }
        },
      })
      ```
      
      `verify` receives:
      - `credential` - the parsed credential from the client
      - `challenge` - the original challenge
      
      It must return a receipt object with `reference` and `settlement`, or throw an error if verification fails.
      
      ---
      
      ## Full Lightning Example
      
      Complete custom method implementation - from definition through usage:
      
      ### Method Definition
      
      ```ts
      import { Method } from 'mppx'
      import { z } from 'zod'
      
      const lightning = Method.from({
        intent: 'charge',
        name: 'lightning',
        schema: {
          credential: {
            payload: z.object({
              preimage: z.string().length(64),
            }),
          },
          request: z.object({
            invoice: z.string(),
            paymentHash: z.string().length(64),
            amount: z.number(),
            network: z.enum(['mainnet', 'regtest']).default('mainnet'),
          }),
        },
      })
      ```
      
      ### Client
      
      ```ts
      const lightningClient = Method.toClient(lightning, {
        async createCredential({ challenge }) {
          const { invoice } = challenge.request
      
          // Use any Lightning wallet SDK to pay the invoice
          const preimage = await payInvoice(invoice)
      
          return {
            payload: { preimage },
          }
        },
      })
      ```
      
      ### Server
      
      ```ts
      import { createHash } from 'node:crypto'
      
      const lightningServer = Method.toServer(lightning, {
        async verify({ credential, challenge }) {
          const { preimage } = credential.payload
          const { paymentHash } = challenge.request
      
          const hash = createHash('sha256')
            .update(Buffer.from(preimage, 'hex'))
            .digest('hex')
      
          if (hash !== paymentHash) {
            throw new Error('Preimage verification failed')
          }
      
          return {
            reference: paymentHash,
            settlement: {
              amount: String(challenge.request.amount),
              currency: 'BTC',
            },
          }
        },
      })
      ```
      
      ### Usage
      
      ```ts
      import { Mppx } from 'mppx/server'
      
      // Server
      const mppx = Mppx.create({
        methods: [lightningServer],
      })
      
      export async function handler(req: Request) {
        const result = await mppx.charge({ amount: '1000' })(req)
        if (result.status === 402) return result.challenge
        return result.withReceipt(Response.json({ data: 'paid' }))
      }
      ```
      
      ```ts
      import { Mppx } from 'mppx/client'
      
      // Client
      Mppx.create({
        methods: [lightningClient],
      })
      
      const res = await fetch('https://api.example.com/data')
      ```
      
      ---
      
      ## Method Architecture
      
      The method system uses a layered architecture:
      
      ### Base Method (`Method.from`)
      
      The base method defines the type contract:
      
      | Property | Type | Description |
      |---|---|---|
      | `method` | `string` | Full method identifier (derived from name + intent) |
      | `name` | `string` | Method name (e.g. `'lightning'`) |
      | `schema` | `object` | Zod schemas for credential payload and request |
      
      ### Client Method (`Method.toClient`)
      
      Extends the base method with client-side behavior:
      
      | Property | Type | Description |
      |---|---|---|
      | `createCredential` | `function` | Creates a credential from a challenge. Receives `{ challenge, context }`, returns serialized credential |
      
      ### Server Method (`Method.toServer`)
      
      Extends the base method with server-side behavior:
      
      | Property | Type | Description |
      |---|---|---|
      | `verify` | `function` | **Deprecated.** Verifies a credential and returns a receipt. Receives `{ credential, challenge }` |
      | `validate` | `function` | Optional non-mutating pre-check that a credential satisfies the challenge |
      | `broadcast` | `function` | Settles the payment and returns a receipt |
      | `defaults` | `object` | Optional default values for challenge request fields |
      | `transformRequest` | `function` | Optional transform applied to the request before challenge generation |
      | `onRespond` | `function` | Optional hook called after verification, before response is sent |
      | `transport` | `object` | Optional transport override for custom encoding |
      
      ### Defaults Example
      
      ```ts
      const server = Method.toServer(method, {
        verify: async ({ credential }) => { /* ... */ },
        defaults: {
          currency: 'BTC',
          network: 'mainnet',
        },
      })
      ```
      
      ### Transform Request Example
      
      ```ts
      const server = Method.toServer(method, {
        verify: async ({ credential }) => { /* ... */ },
        transformRequest: (request, { amount }) => ({
          ...request,
          invoice: generateInvoice(amount),
          paymentHash: getPaymentHash(invoice),
        }),
      })
      ```
      
      ---
      
      ## SDK References
      
      ### `Method.from(options)`
      
      Creates a base method definition.
      
      ```ts
      Method.from({
        intent: string,
        name: string,
        schema: {
          credential: { payload: ZodType },
          request: ZodType,
        },
      })
      ```
      
      ### `Method.toClient(method, implementation)`
      
      Adds client behavior to a base method.
      
      ```ts
      Method.toClient(method, {
        createCredential: async ({ challenge, context }) => ({
          payload: { /* ... */ },
        }),
      })
      ```
      
      ### `Method.toServer(method, implementation)`
      
      Adds server behavior to a base method.
      
      ```ts
      Method.toServer(method, {
        verify: async ({ credential, challenge }) => ({
          reference: string,
          settlement: { amount: string, currency: string },
        }),
        defaults?: object,
        transformRequest?: (request, chargeOptions) => transformedRequest,
        onRespond?: (receipt, response) => void,
        transport?: TransportOverride,
      })
      ```
      
    • discovery-and-proxy.md 4.5 KB
      # Discovery and the Payments Proxy
      
      Two related surfaces: making an existing API payable (`mppx/proxy`), and publishing a machine-readable description of what your service charges for (`mppx/discovery`).
      
      ## Payments Proxy (`mppx/proxy`)
      
      Gate an upstream API behind MPP payments without touching it.
      
      ```ts
      import { Proxy, openai, anthropic, stripe } from 'mppx/proxy'
      import { Mppx, tempo } from 'mppx/server'
      
      const mppx = Mppx.create({
        methods: [tempo()],
        secretKey: process.env.MPP_SECRET_KEY,
      })
      
      const proxy = Proxy.create({
        title: 'My API Proxy',
        description: 'Paid access to AI APIs',
        basePath: '/api',
        services: [
          openai({
            apiKey: process.env.OPENAI_API_KEY,
            routes: {
              'POST /v1/chat/completions': mppx.charge({ amount: '0.005' }),
              'GET /v1/models': true, // free passthrough
            },
          }),
          anthropic({
            apiKey: process.env.ANTHROPIC_API_KEY,
            routes: { 'POST /v1/messages': mppx.charge({ amount: '0.01' }) },
          }),
        ],
      })
      ```
      
      Built-in service presets, all from `mppx/proxy`: `openai()`, `anthropic()`, `stripe()`.
      
      ### Free Routes
      
      A route is free when its value is the literal `true`. There is no `mppx.free()` helper.
      
      ```ts
      routes: {
        'GET /v1/models': true,                              // free passthrough
        'POST /v1/chat/completions': mppx.charge({ amount: '0.005' }),
      }
      ```
      
      Free routes still run `rewriteRequest`, so header injection and path rewriting apply as normal.
      
      ### Custom Services
      
      `Service.from` (exported as `custom`) wraps any upstream:
      
      ```ts
      import { Proxy, custom } from 'mppx/proxy'
      
      const service = custom({
        title: 'Internal Search',
        baseUrl: 'https://search.internal.example.com',
        bearer: process.env.SEARCH_TOKEN,        // Authorization: Bearer <token>
        headers: { 'X-Client': 'mpp-proxy' },    // static headers on every upstream call
        docs: { homepage: '...', apiReference: '...' },
        docsLlmsUrl: 'https://example.com/llms.txt',
        rewriteRequest: (req, ctx) => req,       // mutate the upstream request
        rewriteResponse: (res, ctx) => res,      // mutate the response on the way back
        routes: {
          'POST /search': { pay: mppx.charge({ amount: '0.002' }), options: { apiKey: '...' } },
          'GET /health': true,
        },
      })
      ```
      
      Route values accept a payment handler, the literal `true`, or a `{ pay, options }` object when a single endpoint needs its own per-endpoint configuration.
      
      ### Handlers
      
      ```ts
      // Fetch API (Cloudflare Workers, Bun, Deno, Next.js, Hono, Elysia, SvelteKit)
      export default { fetch: proxy.fetch }
      
      // Node.js http server
      import http from 'node:http'
      http.createServer(proxy.listener).listen(3000)
      ```
      
      ### Discovery Endpoints
      
      The proxy auto-serves these (all active):
      
      - `GET /discover` - JSON service list
      - `GET /discover/{id}` and `GET /discover/{id}.md` - single service detail
      - `GET /discover/all` and `GET /discover/all.md` - all services with full route details
      - `GET /llms.txt` - LLM-readable overview (`GET /discover.md` is an alias)
      
      Content negotiation is user-agent aware: the proxy returns markdown instead of JSON when the caller is a known AI user agent (for example `ChatGPT-User`, `ClaudeBot`, `PerplexityBot`) or a terminal client (`curl`, `HTTPie`, `mppx`). Agents therefore get readable docs from the same URLs a program gets JSON from.
      
      ## Discovery Documents (`mppx/discovery`)
      
      Any server can publish a discovery document without running a proxy. The `discovery()` helper generates a `GET /openapi.json` endpoint from your route configuration, annotating each paid route with canonical `x-payment-info.offers[]` entries, plus service-level `x-service-info`.
      
      ```ts
      import { discovery } from 'mppx/discovery'
      
      app.route('/', discovery({
        title: 'My API',
        description: 'Paid endpoints',
        routes: {
          'POST /summarize': mppx.charge({ amount: '0.01' }),
        },
      }))
      ```
      
      Helpers for Next.js and a CLI for static generation ship alongside it; `mppx discover generate` and `mppx discover validate` drive the same machinery from the command line.
      
      ### Registries
      
      Register the service so agents can find it:
      
      - [MPPScan](https://mppscan.com) - public registry with search and analytics (one-click register)
      - [MPP Services directory](https://mpp.dev/services) - curated list (submit a PR)
      - `mppx services` browses the registry from the CLI
      - [Services MCP](https://mpp.dev/mcp/services) - agent-facing discovery of the curated directory
      
      Note that MPPScan attributes payments by **realm**, not by transaction hash, and session traffic settles off-chain - see `references/production-gotchas.md` for what that means for reported volume.
      
    • lightning-method.md 7.2 KB
      # Lightning Payment Method
      
      ## Overview
      
      Lightning enables Bitcoin payments over the Lightning Network using the `@buildonspark/lightning-mpp-sdk` package. Payments use **BOLT11 invoices** - the standard Lightning payment request format.
      
      Key properties:
      
      - **Cryptographic verification**: `sha256(preimage) == paymentHash` - verification is entirely local, no RPC or API calls needed
      - **Synchronous settlement**: payment settles before the HTTP response returns
      - **Global and permissionless**: no accounts, KYC, or payment processor needed
      - **Self-custodial**: both client and server use Spark wallets backed by BIP-39 mnemonics
      
      ---
      
      ## Lightning Charge
      
      One-time payment per request. Server generates a BOLT11 invoice, client pays it, server verifies the preimage.
      
      ### Server Setup
      
      ```ts
      import { spark } from '@buildonspark/lightning-mpp-sdk/server'
      
      const charge = spark.charge({
        mnemonic: process.env.MNEMONIC!, // BIP-39 mnemonic for Spark wallet
      })
      ```
      
      The server method handles:
      1. Generating a BOLT11 invoice for the requested amount.
      2. Extracting the `paymentHash` from the invoice.
      3. Verifying the client's preimage: `sha256(hex_to_bytes(preimage)) == paymentHash`.
      
      ### Client Setup
      
      ```ts
      import { spark } from '@buildonspark/lightning-mpp-sdk/client'
      
      const charge = spark.charge({
        mnemonic: process.env.MNEMONIC!, // BIP-39 mnemonic for Spark wallet
      })
      ```
      
      The client method auto-pays the BOLT11 invoice from the challenge and returns the preimage as the credential.
      
      ### Full Charge Example
      
      ```ts
      import { Mppx } from 'mppx/server'
      import { spark } from '@buildonspark/lightning-mpp-sdk/server'
      
      // Server
      const mppx = Mppx.create({
        methods: [spark.charge({ mnemonic: process.env.MNEMONIC! })],
      })
      
      export async function handler(req: Request) {
        const result = await mppx.charge({ amount: '100' })(req) // 100 satoshis
        if (result.status === 402) return result.challenge
        return result.withReceipt(Response.json({ data: 'paid content' }))
      }
      ```
      
      ```ts
      import { Mppx } from 'mppx/client'
      import { spark } from '@buildonspark/lightning-mpp-sdk/client'
      
      // Client
      Mppx.create({
        methods: [spark.charge({ mnemonic: process.env.MNEMONIC! })],
      })
      
      const res = await fetch('https://api.example.com/data')
      // 402 -> auto-pay BOLT11 invoice -> credential with preimage -> 200
      ```
      
      ### Request Fields
      
      | Field | Type | Required | Description |
      |---|---|---|---|
      | `amount` | `number` | Yes | Payment amount in satoshis |
      | `currency` | `string` | Yes | Always `'BTC'` |
      | `methodDetails.invoice` | `string` | Yes | BOLT11 payment request string |
      | `methodDetails.paymentHash` | `string` | Yes | SHA-256 hash (hex) that the preimage must satisfy |
      | `methodDetails.network` | `string` | No | `'mainnet'` (default) or `'regtest'` |
      
      ### Credential Payload
      
      | Field | Type | Required | Description |
      |---|---|---|---|
      | `preimage` | `string` | Yes | 32-byte hex preimage proving payment |
      
      ### Verification
      
      Verification is a single hash operation - no network calls:
      
      ```
      sha256(hex_to_bytes(preimage)) == paymentHash
      ```
      
      If the hash matches, the payment is cryptographically proven. The preimage can only be obtained by paying the invoice through the Lightning Network.
      
      ### With Regtest
      
      For local development and testing, use the regtest network:
      
      ```ts
      // Server
      const charge = spark.charge({
        mnemonic: process.env.MNEMONIC!,
        network: 'regtest',
      })
      
      // Client
      const charge = spark.charge({
        mnemonic: process.env.MNEMONIC!,
        network: 'regtest',
      })
      ```
      
      ---
      
      ## Lightning Session
      
      Sessions use a deposit invoice as an upfront payment. The preimage from paying the deposit becomes a bearer token for subsequent requests.
      
      ### How It Works
      
      1. **Open**: Server generates a deposit invoice. Client pays it. The preimage becomes the session bearer token.
      2. **Per-request**: Client sends the preimage with each request. Server verifies `sha256(preimage) == paymentHash` - a single hash operation.
      3. **Top-up**: If the session balance runs low, the server issues a fresh deposit invoice. Client pays it, and the new preimage extends the session.
      4. **Close**: Server refunds unspent balance via the client's return invoice.
      
      ### Server Setup
      
      ```ts
      import { spark } from '@buildonspark/lightning-mpp-sdk/server'
      
      const session = spark.session({
        mnemonic: process.env.MNEMONIC!,
      })
      ```
      
      ### Client Setup
      
      ```ts
      import { spark } from '@buildonspark/lightning-mpp-sdk/client'
      
      const session = spark.session({
        mnemonic: process.env.MNEMONIC!,
      })
      ```
      
      ### Full Session Example
      
      ```ts
      import { Mppx } from 'mppx/server'
      import { spark } from '@buildonspark/lightning-mpp-sdk/server'
      
      // Server
      const mppx = Mppx.create({
        methods: [spark.session({ mnemonic: process.env.MNEMONIC! })],
      })
      
      export async function handler(req: Request) {
        const result = await mppx.session({
          amount: '10', // 10 satoshis per request
          unitType: 'request',
        })(req)
        if (result.status === 402) return result.challenge
        return result.withReceipt(Response.json({ data: 'session content' }))
      }
      ```
      
      ```ts
      import { Mppx } from 'mppx/client'
      import { spark } from '@buildonspark/lightning-mpp-sdk/client'
      
      // Client
      Mppx.create({
        methods: [spark.session({ mnemonic: process.env.MNEMONIC! })],
      })
      
      // 1st request: pays deposit invoice, receives bearer token
      const res1 = await fetch('https://api.example.com/data')
      
      // 2nd+ requests: uses preimage as bearer token (no new Lightning payment)
      const res2 = await fetch('https://api.example.com/data')
      ```
      
      ### Session Lifecycle Details
      
      **Per-request verification** is extremely fast:
      
      ```
      sha256(preimage) == paymentHash  // single CPU hash operation
      ```
      
      No RPC calls, no network requests, no database lookups. This makes Lightning sessions suitable for high-frequency APIs.
      
      **Top-up** works transparently:
      
      1. Server detects session balance is low.
      2. Server issues a fresh deposit invoice in the 402 response.
      3. Client pays the new invoice.
      4. New preimage extends the session.
      
      **Close** refunds unspent balance:
      
      1. Server calculates unspent amount from the deposit.
      2. Client provides a return invoice.
      3. Server pays the return invoice, refunding the client.
      
      ---
      
      ## Cleanup
      
      Lightning methods maintain WebSocket connections to the Spark network. Always call `cleanup()` when shutting down:
      
      ```ts
      const charge = spark.charge({ mnemonic: process.env.MNEMONIC! })
      
      // ... use the method ...
      
      // Required: close WebSocket connections
      await charge.cleanup()
      ```
      
      For sessions:
      
      ```ts
      const session = spark.session({ mnemonic: process.env.MNEMONIC! })
      
      // ... use the method ...
      
      await session.cleanup()
      ```
      
      Failing to call `cleanup()` will leave WebSocket connections open, potentially causing resource leaks in long-running processes.
      
      ---
      
      ## Choosing an Intent
      
      | | Charge | Session |
      |---|---|---|
      | **Pattern** | One Lightning payment per request | Deposit upfront, bearer token for many requests |
      | **Latency** | Lightning payment per request (~1-3s) | First request: Lightning payment; subsequent: hash check (~microseconds) |
      | **Best for** | Infrequent, high-value API calls | High-frequency APIs, streaming, metered billing |
      | **Verification** | `sha256(preimage) == paymentHash` | `sha256(preimage) == paymentHash` (same, but amortized) |
      | **Refunds** | Not applicable (exact amount per request) | Unspent balance refunded via return invoice |
      
    • production-gotchas.md 13 KB
      # Production Gotchas
      
      Field-tested failure modes when running MPP servers and clients in production.
      
      ## Tempo Gas (CRITICAL)
      
      **Tempo has no native gas token.** Unlike Ethereum (ETH for gas) or Solana (SOL for fees), Tempo charges transaction fees in stablecoins. Every transaction must specify which stablecoin pays for gas. There are two ways:
      
      1. **Per-transaction `feeToken`** - set in the transaction itself:
      ```typescript
      const prepared = await prepareTransactionRequest(client, {
        account,
        calls: [{ to, data }],
        feeToken: '<USDC_TEMPO_MAINNET>',
      } as never)
      ```
      
      2. **Account-level default via `setUserToken`** - one-time setup, applies to all future transactions:
      ```typescript
      import { setUserToken } from 'viem/tempo'
      await client.fee.setUserTokenSync({ token: '<USDC_TEMPO_MAINNET>' })
      ```
      
      **Without either, transactions fail silently with `gas_limit: 0`.** The mppx SDK handles this internally for payment transactions, but any direct on-chain calls (settle, close, custom contract interactions) must set `feeToken` explicitly or ensure `setUserToken` was called for the account.
      
      **"Fund with ETH/gas" errors are misleading on Tempo** - read them as "fund with the stablecoin fee token." The **server/recipient wallet itself** must hold the stablecoin fee token before it can broadcast `session.close()` or settle; otherwise the close silently fails and surfaces as a generic 402, leaving client deposits locked.
      
      Asymmetry worth knowing: in charge **push** mode the payer broadcasts the transfer, so the recipient wallet needs no fee token at all for that path. It needs fee-token balance only for server-signed operations (settle, close, sponsored charges).
      
      ## Settlement Is Not Automatic
      
      A session server verifies and stores vouchers, but **nothing redeems them on-chain unless you configure it**. Left alone, a session server accrues signed vouchers it never converts to funds, and channels stay open with payer deposits reserved.
      
      Configure `settlementSchedule` on `tempo.session()`, or drive `tempo.settle()` / `tempo.settleBatch()` from your own sweep. See `references/sessions.md` for both. Settling claims revenue while keeping the channel reusable; closing settles and releases the payer's remaining reservation. A server that only ever settles leaves abandoned channels open indefinitely, so pair a settle cadence with a close policy for idle channels.
      
      ## Payment Timing
      
      **Charge settles during verification, before your handler runs.** By the time route logic executes, funds have already moved - an empty result, an upstream 5xx, or a business-rule rejection is still a completed payment. There is no post-handler skip.
      
      When you need to gate payment on the work succeeding, use the split credential lifecycle: `mppx.validateCredential()` is a non-mutating pre-check, and `mppx.broadcastCredential()` performs settlement. The older combined `verify` path is deprecated precisely because it "combines both operations and may consume payment state, so it cannot support a safe pre-check endpoint."
      
      **Challenges expire after 5 minutes by default.** If you restructure toward "serve first, settle after the upstream call," compare your p95 upstream latency against that window - a slow upstream can expire the authorization and hand the client a 402 after a long wait. Set `expires` explicitly when the work is slow.
      
      ## Setup
      
      **Self-payment trap**: The payer and recipient cannot be the same wallet address. When testing with the CLI, create a separate client account (`mppx account create -a client`) and fund it separately.
      
      **Recipient wallet initialization**: TIP-20 token accounts on Tempo must be initialized before they can receive tokens (similar to Solana ATAs). Send a tiny amount (e.g. 0.01 USDC) to the recipient address first.
      
      ## Server
      
      **Set `realm` explicitly for mppscan attribution.** The `realm` value is hashed into Tempo's attribution memo and is how mppscan correlates on-chain transactions to registered servers. Resolution order is **explicit value > env vars > request URL hostname > `"MPP Payment"`**. The env vars checked include `MPP_REALM`, `FLY_APP_NAME`, `HEROKU_APP_NAME`, `HOST`, `HOSTNAME`, `RAILWAY_PUBLIC_DOMAIN`, `RENDER_EXTERNAL_HOSTNAME`, `VERCEL_URL`, `WEBSITE_HOSTNAME`.
      
      Two consequences:
      
      - **In Kubernetes, `HOSTNAME` is the pod name** (e.g. `web-69d986c8d8-6dtdx`) which rotates on every deploy, giving a new server fingerprint each time. Set `MPP_REALM` to your stable public domain.
      - **Env vars outrank the per-request hostname.** A server fronting several hostnames resolves every request to the same env-derived realm, so multi-host or multi-brand deployments must pass `realm` explicitly - typically one `Mppx.create()` instance per realm, selected by the incoming host.
      
      ```typescript
      Mppx.create({
        methods: [tempo({ ... })],
        realm: 'api.example.com', // or process.env.MPP_REALM
        secretKey,
      })
      ```
      
      **`tempo()` vs explicit registration**: `tempo({ ... })` registers both `charge` and `session` intents with shared config. When you need different config per intent (e.g. session needs `store` and `sse: { poll: true }` but charge doesn't), register them explicitly:
      
      ```typescript
      import { Mppx, Store, tempo } from 'mppx/server'
      Mppx.create({
        methods: [
          tempo.charge({ currency, recipient }),
          tempo.session({ currency, recipient, store: Store.memory(), sse: { poll: true } }),
        ],
        secretKey,
      })
      ```
      
      **Hono multiple headers**: `c.header(name, value)` replaces by default. When emitting multiple `WWW-Authenticate` values (e.g. charge + session intents), the second call silently overwrites the first. Prefer `Mppx.compose()`, which handles multi-header emission correctly. If composing manually, use `{ append: true }`:
      
      ```typescript
      c.header('WWW-Authenticate', chargeWwwAuth)
      c.header('WWW-Authenticate', sessionWwwAuth, { append: true })
      ```
      
      Apply the same branch at both the challenge site and the verification site. If you compose several methods when verifying but emit a single-method challenge, the 402 advertises fewer options than the server actually accepts.
      
      **CORS headers**: `WWW-Authenticate` and `Payment-Receipt` must be listed in `access-control-expose-headers` or browsers/clients won't see them.
      
      **Rotate `MPP_SECRET_KEY` with overlap**: challenge IDs are HMAC-bound to the secret, so a hard swap invalidates every in-flight challenge. Staged rollout: start issuing new challenges with the new key, keep verifying the previous key during a short overlap window, then drop the old key after outstanding challenges have expired. If your deployment can't verify current-and-previous keys, do a coordinated cutover and wait out the old challenge TTL.
      
      **SSE utilities import path**: `Session.Sse.iterateData` is exported from `mppx/tempo`, NOT `mppx/server`:
      
      ```typescript
      import { Mppx, Store, tempo } from 'mppx/server'
      import { Session } from 'mppx/tempo'
      const iterateSseData = Session.Sse.iterateData
      ```
      
      ## Diagnostics
      
      mppx catches verification failures internally and re-emits them as a plain 402 with a fresh challenge, so a bad RPC endpoint, a failed broadcast, and a missing signer all look identical from outside. Three ways to see through it:
      
      - `PaymentError` carries a structured `details` record (alongside `hint`) surfaced in the RFC 9457 problem document - read it rather than inferring from the status code.
      - Register `onPaymentFailed` server-side; the handler receives the underlying error.
      - From the client, `mppx -v` (details) and `-vv` (headers) show the negotiation.
      
      ## Stores
      
      **Never use `Store.memory()` in production.** It loses all channel state on restart/redeploy. When state is lost, the server can't close channels or settle funds - client deposits stay reserved indefinitely. Use a persistent store.
      
      Built-in store adapters (all handle BigInt serialization via `ox`'s `Json` module):
      
      ```typescript
      import { Store } from 'mppx/server'
      
      Store.memory()               // development only
      Store.redis(redisClient)     // ioredis, node-redis, Valkey (added in 0.4.9)
      Store.upstash(upstashClient) // Upstash Redis / Vercel KV
      Store.cloudflare(kvNamespace) // Cloudflare KV
      Store.from({ get, put, delete }) // custom adapter
      ```
      
      **AtomicStore** (0.5.7+): Extends `Store` with an `update(key, fn)` method for safe concurrent read-modify-write. Used internally for replay protection and channel state. All built-in adapters support atomic updates. Custom adapters via `Store.from()` get an optimistic-retry implementation automatically.
      
      **Polling mode**: If your store doesn't implement the optional `waitForUpdate()` method (e.g. custom adapters via `Store.from()`), pass `sse: { poll: true }` to `tempo.session()`. Otherwise SSE streams hang waiting for event-driven wakeups that never come.
      
      ## Request Handling
      
      **Session voucher POSTs have no body.** Mid-stream voucher POSTs carry only `Authorization: Payment` - no JSON body. If your middleware decides charge vs session based on `body.stream`, vouchers hit the charge path. Check the **credential's intent** instead. As of mppx 0.4.9 the SDK skips route amount/currency/recipient validation for topUp and voucher credentials, so body-derived pricing mismatches no longer cause spurious 402 rejections.
      
      **`close`/`topUp` management credentials are also bodyless.** They arrive as POSTs with only `Authorization: Payment`. If you run your own request-body validation (e.g. a Zod schema on the tool payload) before handing the request to `mppx.session()`, it rejects these with a spurious **400** before mppx can answer them (mppx replies `204 No Content`). Exempt session-management credentials from your body validator - gate on the credential's intent/action, not on body presence.
      
      **Clone the request before reading the body.** `request.json()` consumes the Request body. If you parse the body first and then pass the original request to `mppx.session()` or `mppx.charge()`, the mppx handler gets an empty body and returns 402. Clone before reading.
      
      ## Pricing & Streaming
      
      **Cheap model zero-charge floor**: Tempo USDC has 6-decimal precision. For very cheap models, per-token cost like `(0.10 / 1_000_000) * 1.3 = 0.00000013` rounds to `"0.000000"` via `toFixed(6)` - effectively zero. Add a minimum tick cost floor:
      
      ```typescript
      const MIN_TICK_COST = 0.000001 // smallest Tempo USDC unit (6 decimals)
      const tickCost = Math.max((outputRate / 1_000_000) * margin, MIN_TICK_COST)
      ```
      
      **SSE chunks != tokens**: Per-SSE-event `stream.charge()` is an acceptable approximation. `stream.charge()` is serial (store read + write per call, per-channelId mutex) - no bulk API exists yet.
      
      **Add upstream timeouts**: Always use `AbortSignal.timeout()` on upstream fetches. A stalled upstream holds the payment channel open, reserving client funds.
      
      ## Observability and Accounting
      
      mppscan indexes MPP **payments by realm**, not raw chain transactions, so looking up a transaction hash there will not find it - use Tempo's own explorer for that. Session traffic compounds this: per-request vouchers are off-chain, so a busy streaming service reads as near-zero volume until settle and close transactions land.
      
      For per-request accounting, keep your own ledger keyed by channel ID. Background-sweep settlements carry no request context, so attribution has to come from your side.
      
      ## Infrastructure
      
      **Nginx proxy buffer overflow**: Large 402 headers can exceed nginx's default 4k `proxy_buffer_size`, causing **502 Bad Gateway**. Fix: `nginx.ingress.kubernetes.io/proxy-buffer-size: "16k"`. Debug by port-forwarding directly to the pod - if you get 402 there, the issue is in the ingress layer.
      
      **Reverse-proxy scheme mismatch**: Behind a TLS-terminating proxy (Caddy/nginx/CDN), the request the server sees can be `http://` while the public origin is `https://`. If the challenge binds the resource URL, the signed payment then fails re-verification - a double-402 loop. Trust the forwarded-proto header or configure the public resource URL explicitly.
      
      ## Client / Tempo CLI
      
      **Network selection**: the CLI defaults to Tempo mainnet. Use `--network testnet`, `--rpc-url`, or the `MPPX_RPC_URL` / `RPC_URL` env vars for testnet.
      
      **Stale sessions after redeploy**: When the server redeploys and loses in-memory session state, clients get `"Session invalidation claim for channel 0x... was not confirmed on-chain"`. Fix with `mppx sessions close` (or `tempo wallet sessions close` / `sync`). Dispute window is 4-15 min.
      
      ## Client SDK Versioning (advisory)
      
      These are field reports, not documented guarantees - verify against your own versions.
      
      **Pin mppx and viem together.** Bumping `viem` independently of `mppx` has crashed the Tempo charge path with `TypeError: Cannot destructure property 'from' of 'parameters'` - a call-signature mismatch between the mppx build and a newer viem. Treat mppx + viem as a coupled pair. mppx 0.8.15 requires `viem >= 2.54.0`.
      
      **A fast non-402 response is not a payment failure.** `mppx.fetch` sends an initial probe; if the response is not a 402 (e.g. an upstream 500) it returns it as-is immediately (hundreds of ms). Opening a real session channel takes seconds, so a quick failure means the upstream errored before any payment - disambiguate on latency plus status, and don't retry as if payment failed.
      
    • protocol-spec.md 15 KB
      # MPP Core Protocol Specification
      
      ## Protocol Overview
      
      MPP (Machine Payments Protocol) defines a Payment HTTP Authentication Scheme built on **HTTP 402 Payment Required**. It follows the structure of RFC 9110 HTTP Authentication but uses the `402` status code instead of `401` to signal that access requires payment rather than identity credentials.
      
      MPP is submitted to the IETF as an Internet-Draft with intended status **Standards Track** (originally Experimental). The current revision on the datatracker is `draft-ryan-httpauth-payment-01` (expires 2026-09-19). Two draft names are in circulation: `draft-ryan-httpauth-payment` on the IETF datatracker and `draft-httpauth-payment` on paymentauth.org. It leverages existing HTTP semantics, making it compatible with standard web infrastructure - proxies, CDNs, and load balancers pass through the headers without modification.
      
      The flow:
      1. Client requests a protected resource.
      2. Server responds with `402 Payment Required` and a `WWW-Authenticate: Payment` challenge.
      3. Client fulfills payment and sends credentials in `Authorization: Payment`.
      4. Server verifies payment and returns the resource with a `Payment-Receipt` header.
      
      ```http
      GET /api/data HTTP/1.1
      Host: api.example.com
      ```
      
      ```http
      HTTP/1.1 402 Payment Required
      WWW-Authenticate: Payment id="abc123", realm="api.example.com", method="tempo", intent="charge", ...
      Content-Type: application/problem+json
      
      {"type":"https://paymentauth.org/problems/payment-required","title":"Payment Required"}
      ```
      
      ---
      
      ## Challenge Structure
      
      The server issues a challenge via the `WWW-Authenticate` header using the `Payment` scheme.
      
      ### Parameters
      
      | Parameter     | Required | Description                                              |
      |---------------|----------|----------------------------------------------------------|
      | `id`          | Yes      | HMAC-derived challenge identifier (tamper-proof binding) |
      | `realm`       | Yes      | Protection space (typically the host or API namespace)    |
      | `method`      | Yes      | Payment method identifier (e.g., `tempo`, `stripe`, `lightning`) |
      | `intent`      | Yes      | Payment intent type (e.g., `charge`, `session`)          |
      | `request`     | Yes      | Base64url-encoded JSON with method-specific payment details |
      | `expires`     | No       | ISO 8601 timestamp after which the challenge is invalid  |
      | `description` | No       | Human-readable description of the payment                |
      | `digest`      | No       | Content-Digest of the request body (for body binding)    |
      | `opaque`      | No       | Base64url-encoded JCS-serialized flat string-to-string map of server-defined correlation data; the client MUST echo it back unchanged in the credential and MUST NOT modify it |
      
      ### Request Parameter
      
      The `request` parameter is **base64url-encoded JSON** (RFC 4648 section 5, no padding). Its contents are method-specific. Example for Tempo charge:
      
      ```json
      {
        "amount": "1000",
        "currency": "<PATHUSD_TESTNET>",
        "decimals": 6,
        "recipient": "<RECIPIENT_ADDRESS>"
      }
      ```
      
      ### Full Challenge Example
      
      ```http
      HTTP/1.1 402 Payment Required
      WWW-Authenticate: Payment id="qB3wErTyU7iOpAsD9fGhJk",
        realm="mpp.dev",
        method="tempo",
        intent="charge",
        request="<JWT_REQUEST_PAYLOAD>",
        expires="2025-04-01T12:05:00Z",
        description="Premium API access"
      Cache-Control: no-store
      ```
      
      ---
      
      ## Challenge ID Binding
      
      The `id` parameter is a cryptographic binding that prevents tampering with challenge parameters without requiring server-side state storage.
      
      ### Construction
      
      The server computes the ID as:
      
      ```
      input = realm | method | intent | request | expires | digest | opaque
      id    = base64url(HMAC-SHA256(server_secret, input))
      ```
      
      Fields are concatenated with `|` as delimiter. Optional fields that are absent are included as empty strings (the delimiter is still present).
      
      ### Properties
      
      - **Tamper-proof**: Any modification to challenge parameters invalidates the ID.
      - **Stateless**: The server needs only its secret key to verify, no database lookup required.
      - **Unique per challenge**: Different parameters always produce different IDs.
      
      ### Verification
      
      When the client echoes the challenge back in the credential, the server recomputes the HMAC from the echoed parameters and compares it to the `id`. A mismatch means the challenge was altered.
      
      ---
      
      ## Credential Structure
      
      After fulfilling payment, the client sends credentials via the `Authorization` header using the `Payment` scheme. The value is **base64url-encoded JSON**.
      
      ### Fields
      
      | Field       | Required | Description                                              |
      |-------------|----------|----------------------------------------------------------|
      | `challenge` | Yes      | The full challenge object echoed back to the server      |
      | `payload`   | Yes      | Method-specific proof of payment (tx hash, PI ID, etc.)  |
      | `source`    | No       | Payer identity (e.g., `did:pkh:eip155:1:0xAbC...`)      |
      
      The `source` field uses the `did:pkh` format for blockchain-based methods, encoding the chain namespace, chain ID, and address.
      
      ### Example
      
      ```http
      POST /api/data HTTP/1.1
      Host: api.example.com
      Authorization: Payment eyJjaGFsbGVuZ2UiOnsia...fQ
      Content-Type: application/json
      
      {"query": "data"}
      ```
      
      Decoded credential payload:
      
      ```json
      {
        "challenge": {
          "id": "qB3wErTyU7iOpAsD9fGhJk",
          "realm": "mpp.dev",
          "method": "tempo",
          "intent": "charge",
          "request": "eyJhbW91bnQiOiIxMDAwIi4uLn0",
          "expires": "2025-04-01T12:05:00Z"
        },
        "payload": {
          "type": "transaction",
          "signature": "0x1b2c3d4e5f6a7b8c9d0e..."
        },
        "source": "<SOURCE_ADDRESS>"
      }
      ```
      
      ---
      
      ## Receipt Structure
      
      On successful payment verification, the server returns the requested resource along with a `Payment-Receipt` header. The value is **base64url-encoded JSON**.
      
      ### Fields
      
      | Field         | Required | Description                                     |
      |---------------|----------|-------------------------------------------------|
      | `challengeId` | Yes      | The challenge ID this receipt corresponds to     |
      | `method`      | Yes      | Payment method used                              |
      | `reference`   | Yes      | Payment reference (tx hash or payment intent ID) |
      | `settlement`  | Yes      | Object with `amount` and `currency`              |
      | `status`      | Yes      | Always `"success"`                               |
      | `timestamp`   | Yes      | ISO 8601 timestamp of settlement                 |
      
      Payment method specifications MAY define additional receipt fields, and mppx preserves them: `Receipt.from`, `Receipt.deserialize`, and `Receipt.fromResponse` pass unknown fields through rather than stripping anything outside the base set. Session receipts use this to carry `acceptedCumulative`, `spent`, `units`, and `txHash`.
      
      ### Example
      
      ```http
      HTTP/1.1 200 OK
      Payment-Receipt: eyJjaGFsbGVuZ2VJZCI6ImRCamZ0...fQ
      Cache-Control: private
      Content-Type: application/json
      
      {"data": "protected content"}
      ```
      
      Decoded receipt:
      
      ```json
      {
        "challengeId": "qB3wErTyU7iOpAsD9fGhJk",
        "method": "tempo",
        "reference": "0xtx789abc...",
        "settlement": {
          "amount": "1000",
          "currency": "<PATHUSD_TESTNET>"
        },
        "status": "success",
        "timestamp": "2025-04-01T12:00:00Z"
      }
      ```
      
      ---
      
      ## Status Codes
      
      MPP departs from traditional HTTP authentication by using `402` consistently for all payment-related challenges.
      
      | Code | Usage                                                                 |
      |------|-----------------------------------------------------------------------|
      | 402  | All payment challenges: initial challenge, failed credentials, expired challenges, insufficient payment. Unlike other auth schemes that use 401 for failed credentials, MPP uses 402 consistently. |
      | 401  | Authentication failures **unrelated** to payment (e.g., missing API key, invalid OAuth token). |
      | 403  | Payment **succeeded** but access is denied by server policy (e.g., geo-restriction, rate limit, content policy). |
      
      This distinction is important: a `402` always means the client should attempt payment. A `401` means the issue is identity, not payment. A `403` means payment went through but access is still denied - the client should not retry payment.
      
      ---
      
      ## Error Handling
      
      MPP uses **RFC 9457 Problem Details** (`application/problem+json`) for error responses.
      
      ### Error Type URIs
      
      All error types are under `https://paymentauth.org/problems/`:
      
      | Type                    | Description                                        |
      |-------------------------|----------------------------------------------------|
      | `payment-required`      | No payment credential provided                     |
      | `payment-insufficient`  | Payment amount too low                             |
      | `payment-expired`       | Challenge has expired                              |
      | `verification-failed`   | Payment proof could not be verified on-chain/off-chain |
      | `method-unsupported`    | Requested payment method not supported (returns HTTP **400**, not 402) |
      | `malformed-credential`  | Credential JSON is invalid or missing fields       |
      | `invalid-challenge`     | Challenge ID does not match (tampered or unknown)  |
      
      ### Example Error Response
      
      ```http
      HTTP/1.1 402 Payment Required
      WWW-Authenticate: Payment id="newChallenge...", method="tempo", intent="charge", ...
      Retry-After: 30
      Content-Type: application/problem+json
      
      {
        "type": "https://paymentauth.org/problems/payment-expired",
        "title": "Payment Challenge Expired",
        "status": 402,
        "detail": "The challenge expired at 2025-04-01T12:00:00Z. A new challenge is provided.",
        "instance": "/api/data"
      }
      ```
      
      The `Retry-After` header (in seconds) indicates when the client may retry. Servers should include it for `payment-expired` and transient `verification-failed` errors.
      
      ---
      
      ## Request Body Binding
      
      For requests with bodies (`POST`, `PUT`, `PATCH`), the server can bind the challenge to the request body using the `digest` parameter.
      
      The digest follows **RFC 9530 Content-Digest** format:
      
      ```http
      POST /api/transfer HTTP/1.1
      Host: api.example.com
      Content-Type: application/json
      Content-Digest: sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:
      
      {"to": "0xRecipient", "amount": 50}
      ```
      
      The server includes the digest in the challenge:
      
      ```http
      HTTP/1.1 402 Payment Required
      WWW-Authenticate: Payment id="...",
        realm="api.example.com",
        method="tempo",
        intent="charge",
        request="...",
        expires="2025-04-01T12:05:00Z",
        digest="sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:"
      ```
      
      On credential submission, the server verifies that the `Content-Digest` of the submitted body matches the `digest` in the echoed challenge. This prevents a client from paying for one operation and submitting a different request body.
      
      ---
      
      ## Multiple Challenges
      
      A server can offer multiple payment methods by including separate `WWW-Authenticate` headers:
      
      ```http
      HTTP/1.1 402 Payment Required
      WWW-Authenticate: Payment id="abc", realm="api.example.com", method="tempo",
        intent="charge", request="eyJhbW91bnQ...", expires="2025-04-01T12:05:00Z"
      WWW-Authenticate: Payment id="def", realm="api.example.com", method="stripe",
        intent="charge", request="eyJ0byI6Ii4...", expires="2025-04-01T12:05:00Z"
      WWW-Authenticate: Payment id="ghi", realm="api.example.com", method="lightning",
        intent="charge", request="eyJyZWNpcGl...", expires="2025-04-01T12:05:00Z"
      ```
      
      The client selects the method it supports and responds with the corresponding challenge. The server must accept any of the offered methods.
      
      ---
      
      ## Security Considerations
      
      ### Transport Security
      
      TLS 1.2 or higher is **required**. Payment credentials and receipts must never be transmitted over plaintext HTTP. Servers should reject non-TLS requests with `301` or `308` redirects to HTTPS.
      
      ### Replay Protection
      
      Payment proofs must be **single-use**. For blockchain transactions, this is inherent (a tx hash can only be mined once). For off-chain methods like Stripe, the server must track used payment intent IDs and reject duplicates.
      
      ### Idempotency
      
      Servers must not produce side effects on unpaid requests. The `402` response must be safe to retry. For operations that create resources, servers should support the `Idempotency-Key` header:
      
      ```http
      POST /api/resource HTTP/1.1
      Host: api.example.com
      Idempotency-Key: unique-request-id-12345
      Authorization: Payment eyJjaGFsbGVuZ2...
      ```
      
      This ensures that if a client retries after a network failure, the server does not double-charge or duplicate the operation.
      
      ### Amount Verification
      
      Clients should verify that the requested payment amount is reasonable before submitting payment. Machine agents should enforce configurable spend limits and flag unusual amounts.
      
      ### Credential Handling
      
      - Payment credentials are **bearer tokens** - possession grants access.
      - Servers must not log credential values or receipt references in access logs.
      - Credentials should be held in memory only for the duration of the request.
      
      ### Caching
      
      - `402` responses: `Cache-Control: no-store` (challenges are time-bound and unique).
      - Responses with receipts: `Cache-Control: private` (receipts are user-specific).
      - Shared caches (CDNs) must not cache `402` responses.
      
      ---
      
      ## SDK Helpers for the Primitives
      
      `mppx` exposes the spec objects above as first-class helpers, useful when implementing a transport or handling credentials out of band:
      
      | Helper | Purpose |
      |---|---|
      | `Challenge.from` / `serialize` / `deserialize` | Construct and encode a challenge |
      | `Challenge.fromResponse` / `fromHeaders` / `fromMethod` | Parse a challenge from a 402 response, raw headers, or a method definition |
      | `Challenge.verify` | Recompute and check the HMAC binding |
      | `Challenge.meta` | Extract correlation data (the `opaque` map) from a challenge |
      | `Credential.from` / `serialize` / `deserialize` / `fromRequest` | Build and parse credentials |
      | `Credential.extractPaymentScheme` | Detect whether an `Authorization` header carries the `Payment` scheme |
      | `Receipt.from` / `serialize` / `deserialize` / `fromResponse` | Build and parse receipts |
      | `PaymentRequest.from` / `serialize` / `deserialize` | Handle the base64url `request` parameter |
      | `BodyDigest.compute` / `verify` | RFC 9530 Content-Digest for request body binding |
      | `Expires.minutes` / `hours` | Build `expires` values |
      
      ## Extensibility
      
      ### Custom Parameters
      
      Servers may include custom parameters in the challenge. Custom parameter names must be **lowercase** and should use a vendor prefix (e.g., `x-vendor-param`). Unknown parameters are ignored by clients.
      
      ```http
      WWW-Authenticate: Payment id="...", method="tempo",
        realm="api.example.com", intent="charge", request="...",
        expires="2025-04-01T12:05:00Z", x-vendor-tier="premium"
      ```
      
      ### Size Considerations
      
      Challenges should remain **under 8KB** total (including all header overhead) to ensure compatibility with common HTTP infrastructure. The `request` parameter carries the bulk of the data; if method-specific details exceed this, use a reference URL pattern instead.
      
      ### Internationalization
      
      - Method identifiers are **ASCII-only**, lowercase, using hyphens as separators.
      - The `description` parameter supports **UTF-8** for human-readable text.
      - All JSON payloads (request, credential, receipt) use UTF-8 encoding.
      
    • python-sdk.md 6.8 KB
      # pympp Python SDK
      
      Verified against pympp 0.9.1. Python supports the **charge** intent, plus Stripe, fee sponsorship, event handling, and MCP support.
      
      **Not supported in pympp:**
      
      - The **session** (payment-channel) intent - TypeScript and Rust only.
      - **Proof Credentials** (zero-dollar auth). The Tempo method implements only the `hash` and `transaction` payload types; there is no `proof` payload. Upstream's two capability matrices disagree on this point, so treat the SDK source as authoritative.
      
      ## What's New
      
      **0.9.1** - Security fix: ABI-encoded calldata with trailing padding bytes is now rejected in Tempo transfer, approve, and swap calls. Exact-length validation constants were added so calldata that does not match the expected byte length precisely is rejected outright.
      
      **0.9.0**
      
      - **Credential `source` validation**: the Tempo hash-credential verification path validates the credential `source`, requiring the TIP-20 transfer to originate from the declared source address. A `validate_sender` callback (`SenderValidation` / `ValidateSender`) on `ChargeIntent` adds custom sender checks.
      - **Sponsored charge pre-broadcast simulation**: sponsored (fee-payer) charges dry-run the co-signed transaction via `tempo_simulateV1` before broadcasting; if it would revert on-chain the sponsor rejects it (fails closed).
      - **MCP support**: pympp covers the MCP transport.
      
      ## Installation
      
      ```bash
      # Core SDK
      pip install pympp
      
      # With Tempo payment method
      pip install "pympp[tempo]"
      ```
      
      **Requirements:** Python 3.10+
      
      **Dependencies:**
      - Core: `httpx`
      - With `[tempo]`: `pytempo`, `eth-account`, `rlp`
      
      ---
      
      ## Server (FastAPI)
      
      ### Basic Setup
      
      ```python
      from fastapi import FastAPI, Request
      from fastapi.responses import JSONResponse
      from mpp import Challenge
      from mpp.server import Mpp
      from mpp.methods.tempo import tempo, ChargeIntent
      
      app = FastAPI()
      
      # Auto-detects realm from env vars
      # Auto-generates secret_key to .env if not present
      mpp = Mpp.create(
          method=tempo(
              currency="<PATHUSD_TESTNET>",
              recipient="0xYourAddress",
              intents={"charge": ChargeIntent()},
          ),
      )
      ```
      
      ### Charge Endpoint (decorator)
      
      The current pympp API gates a route with the `@server.pay(amount=...)` decorator. On payment the handler receives an injected `Credential` and `Receipt`; the decorator emits the 402 challenge and attaches the receipt automatically:
      
      ```python
      from mpp import Credential, Receipt
      from mpp.server import Mpp
      from mpp.methods.tempo import tempo, ChargeIntent
      
      server = Mpp.create(
          method=tempo(
              currency="<PATHUSD_TESTNET>",
              recipient="0xYourAddress",
              intents={"charge": ChargeIntent()},
          ),
      )
      
      @app.get("/resource")
      @server.pay(amount="0.50")
      async def get_resource(request, credential: Credential, receipt: Receipt):
          return {"data": "paid content", "payer": credential.source}
      ```
      
      ### Full FastAPI Example
      
      ```python
      from fastapi import FastAPI
      from mpp import Credential, Receipt
      from mpp.server import Mpp
      from mpp.methods.tempo import tempo, ChargeIntent
      
      app = FastAPI()
      server = Mpp.create(
          method=tempo(
              currency="<PATHUSD_TESTNET>",
              recipient="0xYourAddress",
              intents={"charge": ChargeIntent()},
          ),
      )
      
      @app.get("/api/data")
      @server.pay(amount="0.10")
      async def get_data(request, credential: Credential, receipt: Receipt):
          return {"data": "premium content", "payer": credential.source}
      
      @app.get("/api/free")
      async def get_free():
          return {"data": "free content"}
      ```
      
      ---
      
      ## Client
      
      ### Async Client
      
      The client is an async context manager that wraps `httpx.AsyncClient` with automatic 402 handling:
      
      ```python
      from mpp.client import Client
      from mpp.methods.tempo import tempo, TempoAccount, ChargeIntent
      
      account = TempoAccount.from_key("0xYourPrivateKey")
      
      async with Client(
          methods=[
              tempo(account=account, intents={"charge": ChargeIntent()}),
          ],
      ) as client:
          response = await client.get("https://api.example.com/data")
          print(response.json())
      ```
      
      ### HTTP Methods
      
      The client exposes standard HTTP methods:
      
      ```python
      async with Client(methods=[tempo(account=account, intents={"charge": ChargeIntent()})]) as client:
          # GET
          res = await client.get("https://api.example.com/data")
      
          # POST
          res = await client.post("https://api.example.com/submit", json={"key": "value"})
      
          # PUT
          res = await client.put("https://api.example.com/update", json={"key": "new_value"})
      
          # DELETE
          res = await client.delete("https://api.example.com/item/123")
      
          # Generic request
          res = await client.request("PATCH", "https://api.example.com/partial", json={"field": "val"})
      ```
      
      ### One-Off Request
      
      For single requests without managing a client lifecycle:
      
      ```python
      from mpp.client import get
      
      response = await get(
          "https://api.example.com/data",
          methods=[tempo(account=account, intents={"charge": ChargeIntent()})],
      )
      print(response.json())
      ```
      
      ---
      
      ## Sessions / Streaming (not supported in Python)
      
      The session (payment-channel) intent - off-chain vouchers, SSE/WebSocket streaming, pay-as-you-go billing - is **not implemented in pympp**. The official SDK capability matrix lists the session intent for TypeScript and Rust only. For metered/streaming billing in Python today, fall back to per-request `charge` calls, or run the session-billing tier on the TypeScript (`mppx`) or Rust (`mpp`) server. Watch [mpp.dev/sdk/python](https://mpp.dev/sdk/python) for session support.
      
      ---
      
      ## Core Types
      
      ### Challenge
      
      ```python
      from mpp import Challenge
      
      # Parse from WWW-Authenticate header
      challenge = Challenge.from_www_authenticate(header_value)
      
      # Serialize back to header
      header = challenge.to_www_authenticate(realm="api.example.com")
      
      # Access fields
      challenge.id
      challenge.method
      challenge.intent
      challenge.request
      challenge.expires
      ```
      
      ### Credential
      
      ```python
      from mpp import Credential
      
      # Create a credential
      credential = Credential(
          id=challenge.id,
          payload={"type": "hash", "hash": "0xabc123..."},
          source="did:pkh:eip155:4217:0x1234...",
      )
      
      # Serialize to Authorization header value
      auth_header = credential.to_authorization()
      
      # Parse from Authorization header
      credential = Credential.from_authorization(header_value)
      ```
      
      ### Receipt
      
      ```python
      from mpp import Receipt
      
      # Parse from Payment-Receipt header
      receipt = Receipt.from_payment_receipt(header_value)
      
      # Serialize to header value
      header = receipt.to_payment_receipt()
      
      # Access fields
      receipt.challenge_id
      receipt.method
      receipt.reference
      receipt.settlement  # {"amount": "0.01", "currency": "USD"}
      receipt.status      # "success"
      receipt.timestamp
      ```
      
      ---
      
      ## Automatic 402 Handling
      
      The `Client` context manager and the one-off `get` helper already intercept 402 responses and retry with payment credentials automatically. Pass timeout, connection-pool, and proxy options through to the underlying `httpx.AsyncClient` when you need fine-grained control over the HTTP client.
      
    • rust-sdk.md 6.7 KB
      # mpp Rust SDK
      
      ## Installation
      
      ```bash
      # Core only
      cargo add mpp
      
      # With Tempo support (client + server)
      cargo add mpp --features tempo,client,server
      ```
      
      ---
      
      ## Feature Flags
      
      | Feature | Description |
      |---|---|
      | `client` | Client providers, `Fetch` trait, credential creation |
      | `evm` | Shared EVM utilities (signing, address parsing) |
      | `middleware` | `reqwest-middleware` integration for automatic 402 handling |
      | `server` | Verification, `ChargeMethod`, challenge generation |
      | `tempo` | Tempo blockchain support (includes `evm`), Tempo-specific types |
      | `tower` | `tower` service integration |
      | `axum` | Axum extractors/handlers (implies `server`) |
      | `ws` | WebSocket session transport (implies `server` + `client`) |
      | `sqlite` | Persist and rehydrate native sessions (implies `tempo`) |
      | `stripe` | Stripe payment method |
      | `utils` | Shared helper utilities |
      
      Features are additive. `tempo` implies `evm`. `middleware` implies `client`. `axum` implies `server`. `ws` implies `server` and `client`. `sqlite` implies `tempo`.
      
      TLS backends: `reqwest-default-tls` (the default feature), `reqwest-native-tls`, `reqwest-rustls-tls`.
      
      ---
      
      ## What's New in 0.11.0
      
      - **TIP-1034 session client primitives**: descriptor-backed channels, precompile ABI helpers, voucher signing, and fee-sponsored session opens. Rust is one of only two SDKs with the session intent.
      - **`ChargeMethod::with_validate_sender`**: the Tempo hash-credential path now parses the `did:pkh:eip155` source before reserving the transaction hash and requires TIP-20 transfers to originate from the declared source address. The callback authorizes smart-account and relayer flows where the on-chain sender differs from the declared source.
      - **Sponsored charge dry-run**: sponsored charges simulate the co-signed transaction via `tempo_simulateV1` before broadcasting. The check **fails closed** - if the simulation RPC is unavailable, the charge is rejected.
      - **`TempoProvider::with_expected_chain_id`**: client-side chain pinning, rejecting charge challenges whose `methodDetails.chainId` conflicts with the configured chain.
      - **Breaking (source-level)**: `PaymentFailedContext` gained a `reason: Option<PaymentFailureReason>` field and is now `#[non_exhaustive]`. Construct it via `PaymentFailedContext::new()` and destructure with `..` so future fields stay non-breaking.
      - **Security**: oversized `WWW-Authenticate` `request` parameters are rejected before decoding.
      
      ---
      
      ## Server Quick Start
      
      ```rust
      use mpp::server::{Mpp, tempo, TempoConfig};
      use mpp::{parse_authorization, format_www_authenticate};
      
      // Create server instance
      let mpp = Mpp::create(tempo(TempoConfig {
          recipient: "0xYourAddress",
          currency: "<PATHUSD_TESTNET>",
          testnet: true,
          ..Default::default()
      }))?;
      
      // Generate challenge for 402 response
      let challenge = mpp.charge("0.50")?;
      let www_authenticate = format_www_authenticate(&challenge)?;
      // Set header: WWW-Authenticate: {www_authenticate}
      // Return 402
      
      // On retry: parse and verify credential
      let credential = parse_authorization(auth_header)?;
      let receipt = mpp.verify_credential(&credential).await?;
      let payment_receipt = receipt.to_header();
      // Set header: Payment-Receipt: {payment_receipt}
      // Return 200 with content
      ```
      
      ### Axum Example
      
      ```rust
      use axum::{extract::Request, http::StatusCode, response::IntoResponse, Json};
      use mpp::server::{Mpp, tempo, TempoConfig};
      use mpp::{parse_authorization, format_www_authenticate};
      
      async fn paid_handler(req: Request) -> impl IntoResponse {
          let mpp = Mpp::create(tempo(TempoConfig {
              recipient: "0xYourAddress",
              ..Default::default()
          })).unwrap();
      
          let auth = req.headers()
              .get("Authorization")
              .and_then(|v| v.to_str().ok());
      
          match auth {
              None => {
                  let challenge = mpp.charge("0.10").unwrap();
                  let header = format_www_authenticate(&challenge).unwrap();
                  (
                      StatusCode::PAYMENT_REQUIRED,
                      [("WWW-Authenticate", header)],
                      Json(serde_json::json!({"error": "Payment required"})),
                  ).into_response()
              }
              Some(auth_header) => {
                  let credential = parse_authorization(auth_header).unwrap();
                  let receipt = mpp.verify_credential(&credential).await.unwrap();
                  (
                      StatusCode::OK,
                      [("Payment-Receipt", receipt.to_header())],
                      Json(serde_json::json!({"data": "paid content"})),
                  ).into_response()
              }
          }
      }
      ```
      
      ---
      
      ## Client Quick Start
      
      ```rust
      use alloy::signers::local::PrivateKeySigner;
      use mpp::client::{TempoProvider, send_with_payment};
      
      let signer = PrivateKeySigner::from_bytes(&private_key)?;
      let provider = TempoProvider::new(signer);
      
      // send_with_payment handles the 402 flow automatically:
      // 1. Sends the request
      // 2. If 402, parses the challenge
      // 3. Signs and submits payment
      // 4. Retries with credential
      let response = send_with_payment(
          &provider,
          reqwest::Client::new()
              .get("https://api.example.com/data"),
      ).await?;
      
      println!("{}", response.text().await?);
      ```
      
      ### With reqwest-middleware
      
      The `middleware` feature provides automatic 402 handling as reqwest middleware:
      
      ```rust
      use mpp::middleware::PaymentMiddleware;
      use reqwest_middleware::ClientBuilder;
      
      let payment = PaymentMiddleware::new(provider);
      let client = ClientBuilder::new(reqwest::Client::new())
          .with(payment)
          .build();
      
      // All requests through this client handle 402 automatically
      let res = client.get("https://api.example.com/data").send().await?;
      ```
      
      ---
      
      ## Common Cargo.toml Configurations
      
      ### Client Only
      
      ```toml
      [dependencies]
      mpp = { version = "0.11", features = ["client", "tempo"] }
      ```
      
      ### Server Only
      
      ```toml
      [dependencies]
      mpp = { version = "0.11", features = ["server", "tempo"] }
      ```
      
      ### Both Client and Server
      
      ```toml
      [dependencies]
      mpp = { version = "0.11", features = ["client", "server", "tempo"] }
      ```
      
      ### Client with reqwest-middleware
      
      ```toml
      [dependencies]
      mpp = { version = "0.11", features = ["middleware", "tempo"] }
      reqwest = { version = "0.12", features = ["json"] }
      reqwest-middleware = "0.4"
      ```
      
      ---
      
      ## Key Types
      
      ```rust
      use mpp::{Challenge, Credential, Receipt};
      use mpp::server::{Mpp, ChargeMethod, TempoConfig};
      use mpp::client::{TempoProvider, Fetch};
      
      // Challenge: parsed from WWW-Authenticate header
      let challenge: Challenge = parse_www_authenticate(header)?;
      
      // Credential: built from challenge + payment proof
      let credential = Credential {
          challenge: challenge.clone(),
          payload: serde_json::json!({"type": "hash", "hash": "0x..."}),
          source: Some("did:pkh:eip155:4217:0x1234...".into()),
      };
      
      // Receipt: returned after verification
      let receipt: Receipt = mpp.verify_credential(&credential).await?;
      ```
      
    • sessions.md 19.8 KB
      # Sessions
      
      ## Why Sessions
      
      Usage-based billing needs payment verification that keeps pace with the service. LLM inference generates hundreds of tokens - paying per-token on-chain would add seconds of latency per charge. Sessions fix this: one deposit into an on-chain escrow, then off-chain vouchers verified with CPU-only signature checks (~microseconds). The bottleneck becomes CPU, not blockchain TPS.
      
      Key insight: a session amortizes on-chain cost across many interactions. Instead of 0.001 USD per on-chain tx per request, you pay one open tx + one close tx regardless of how many requests happen in between.
      
      ## Sessions v2 (default) vs Legacy v1
      
      Since mppx 0.7.0, `tempo.session()` is the **v2** flow built on the TIP-1034 session precompile. The earlier contract-backed escrow implementation (the escrow-contract and channel-recovery mechanics described later in this file) is **Sessions v1**, still shipped as `tempo.sessionLegacy` on both `mppx/server` and `mppx/client` - now carrying an explicit `@deprecated` marker pointing at `tempo.session()`.
      
      - **Default:** `tempo.session()` = v2; `tempo.sessionLegacy()` = v1, deprecated.
      - **Interop cliff:** a v2-expecting client rejects a v1 session challenge (it lacks `methodDetails.sessionProtocol: "v2"`) and falls back to the charge path. A server still on old mppx serving v1 sessions silently denies newer clients their working path - keep client and server on matching flows, or advertise v2.
      - **Refunds:** MPP defines no refund protocol - a charge is refunded out-of-protocol by sending funds back to the payer. For sessions, v2 reserves funds in the channel without immediately claiming them, so unclaimed reserved funds are refunded by default. v1 refunds by closing the channel (unspent escrow returned to the client).
      - **Where the funds live:** v2 uses a fixed TIP-20 channel precompile (`tip20ChannelEscrow`) at the same address on mainnet (4217) and Moderato testnet (42431). v1 used a deployed `TempoStreamChannel` contract per network.
      
      Two client entry points:
      - `tempo.session({ account, maxDeposit })` - creates the method registered in `Mppx.create()`; the managed `fetch` opens and reuses the channel transparently.
      - `tempo.session.manager({ account, maxDeposit })` - returns a managed client for direct lifecycle control (`.sse()`, `.close()`), used when you drive the session yourself instead of through `fetch`.
      
      ## Session Lifecycle
      
      Four phases define a session's life:
      
      ### 1. Open
      
      Client deposits tokens into an on-chain escrow contract, creating a payment channel. A unique `channelId` identifies the channel and holds deposited TIP-20 tokens.
      
      ### 2. Session (Vouchers)
      
      Client signs EIP-712 typed vouchers with increasing cumulative amounts. Each voucher states "I have consumed up to X total." The server verifies each voucher with `ecrecover` - no RPC calls needed. The delta (current voucher minus previous voucher) represents the cost of the current request.
      
      ### 3. Top Up
      
      If the channel balance runs low, the client deposits more tokens without closing. The session continues uninterrupted. When streaming, the server emits a `payment-need-voucher` SSE event to signal the client needs to top up.
      
      ### 4. Close
      
      Either party can close the channel. The server calls `close()` on the escrow contract with the highest voucher, settling the final balance on-chain. Any unspent deposit is refunded to the client.
      
      ## Server Integration
      
      ```typescript
      import { Mppx, Store, tempo } from 'mppx/server'
      const mppx = Mppx.create({
        methods: [tempo({
          recipient: '0x...',
          store: Store.memory(), // or Store.cloudflare(), Store.upstash()
        })],
      })
      
      export async function handler(request: Request) {
        const result = await mppx.session({
          amount: '0.001',
          unitType: 'token',
        })(request)
        if (result.status === 402) return result.challenge
        return result.withReceipt(Response.json({ data: '...' }))
      }
      ```
      
      - `mppx.session()` returns a handler that manages the per-request lifecycle automatically. It does **not** settle on-chain by itself - see Settlement below.
      - `result.status === 402` means the client has not yet opened a channel or the voucher is missing/invalid.
      - `result.challenge` sends the 402 response with payment requirements.
      - `result.withReceipt` attaches the payment receipt header to the response.
      - `voucherSigner` (renamed from `authorizedSigner` in mppx 0.6.29) now exists **only on the legacy v1 path**; v2 derives voucher authority from the session descriptor. The on-chain v1 `channels()` ABI field is still named `authorizedSigner`.
      
      ## Settlement
      
      Verifying a voucher is not the same as getting paid. The server stores the highest accepted voucher; converting that into an on-chain transfer is a separate step, and **nothing does it automatically unless you configure it**. A server that never settles accrues signed vouchers it cannot spend, while payer deposits stay reserved in open channels.
      
      ### Scheduled settlement (recommended)
      
      `settlementSchedule` is server-owned - clients never see it and cannot influence it. Any of its three triggers can fire:
      
      ```typescript
      tempo.session({
        currency: '<PATHUSD_TESTNET>',
        recipient: '0x...',
        store,
        settlementSchedule: {
          units: 10_000,      // settle after this many additional paid units
          amount: '1.00',     // ...or this much additional settled amount
          intervalMs: 300_000, // ...or this long since the last scheduled settlement
        },
      })
      ```
      
      ### Manual settlement
      
      Drive settlement yourself when you want a sweep on your own cadence, or need to drain channels at shutdown:
      
      ```typescript
      import { tempo } from 'mppx/server'
      
      // Settle one channel: reads the highest stored voucher and submits it on-chain.
      const txHash = await tempo.settle(store, client, channelId)
      
      // Batch-settle precompile-backed channels.
      await tempo.settleBatch(store, client, channelIds)
      ```
      
      Both throw rather than silently no-op: `ChannelNotFoundError` if the channel is unknown, and a verification error if the channel is not precompile-backed or has no voucher to settle.
      
      ### Settle vs close
      
      - **Settle** claims what has been consumed so far and leaves the channel open and reusable. This is what you want on a live channel.
      - **Close** performs a final settlement and releases the payer's remaining reservation.
      
      Settling alone is not a complete policy: a client that walks away leaves its channel open indefinitely with funds reserved. Pair a settle cadence with a close policy for channels that have been idle long enough that the session is clearly over.
      
      ### Observing settlement
      
      The `onSessionSettlement` server hook fires for both scheduled and explicit settlements, with chain-agnostic context (`trigger`, `txHash`, `channelId`, cumulative amount, incremental delta). Use it to build the ledger that on-chain data alone will not give you, since per-request vouchers never appear as individual transactions.
      
      ## Client Integration
      
      ```typescript
      import { Mppx, tempo } from 'mppx/client'
      Mppx.create({
        methods: [tempo({ account, maxDeposit: '1' })], // Lock up to 1 pathUSD
      })
      // 1st request: opens channel on-chain, sends initial voucher
      // 2nd+ requests: off-chain vouchers (no on-chain tx)
      const res = await fetch('http://localhost:3000/api/resource')
      ```
      
      - `maxDeposit`: maximum tokens locked in escrow. At $0.01/unit, 1 pathUSD covers 100 requests.
      - If the server sets `suggestedDeposit`, the client uses `min(suggestedDeposit, maxDeposit)`.
      - Channels remain open for reuse across multiple requests. Close explicitly when done.
      - `channelStore`: pass a store to persist and reuse payer session channels across processes/restarts (mppx 0.8.0). On Node, `mppx/client/node` provides a SQLite store that shares Tempo Wallet's channel database. The client-side `authorizedSigner` override was removed in 0.8.0 - voucher authority now derives from the selected account.
      - `topUpAmount`: preferred top-up size. The default is a bounded server suggestion and then the **exact shortfall**, which means a fine-grained stream can trigger a top-up round-trip per shortfall. Setting a `topUpAmount` batches those into fewer, larger top-ups without changing what the payer ultimately pays, since the server settles actual spend.
      
      Client sessions rehydrate from server snapshots, reconciling the last accepted voucher against on-chain channel state before continuing cumulative payments - so a client that restarts mid-session resumes rather than opening a second channel.
      
      ## SSE Streaming
      
      Per-token billing over Server-Sent Events enables real-time charging for streamed content.
      
      ### Server
      
      ```typescript
      const mppx = Mppx.create({
        methods: [tempo({ currency: '0x20c0...', recipient: '0x...', sse: true })],
      })
      export const GET = mppx.session({ amount: '0.001', unitType: 'word' })(
        async () => {
          return async function* (stream) {
            yield JSON.stringify({ title: 'Example' })
            for (const word of words) {
              await stream.charge() // deducts from session balance
              yield word
            }
          }
        }
      )
      ```
      
      ### Client
      
      ```typescript
      // .sse()/.close() live on the managed client from tempo.session.manager()
      const session = tempo.session.manager({ account, maxDeposit: '1' })
      const stream = await session.sse('http://localhost:3000/api/poem')
      for await (const word of stream) {
        process.stdout.write(word + ' ')
      }
      const receipt = await session.close()
      ```
      
      Since mppx 0.8.12, the polyfilled/standalone client `fetch` also handles payment-aware session SSE responses, so a plain `fetch` against a streaming endpoint renews vouchers mid-stream rather than truncating when the balance runs out. `session.sse()` remains the explicit path when you want direct lifecycle control.
      
      ### Streaming Behavior
      
      - `withReceipt` accepts an async generator - each `yield` produces one SSE event and one charge.
      - If the balance is exhausted mid-stream, the server emits a `payment-need-voucher` event and pauses until the client sends a new voucher.
      - The client SSE handler auto-renews vouchers transparently, so the stream resumes without application-level intervention.
      
      ## WebSocket Streaming
      
      WebSocket transport provides bidirectional streaming payments, where voucher delivery and content streaming happen over the same persistent connection (no separate HTTP POSTs for vouchers).
      
      ### Server
      
      ```typescript
      import { Ws } from 'mppx/tempo'
      import { Store, tempo } from 'mppx/server'
      
      const wsHandler = Ws.serve({
        methods: [tempo.session({ currency: '0x20c0...', recipient: '0x...', store: Store.redis(redis) })],
        secretKey: process.env.MPP_SECRET_KEY!,
        async onMessage(ws, data, stream) {
          const parsed = JSON.parse(data)
          for (const token of generateTokens(parsed.prompt)) {
            await stream.charge()
            ws.send(JSON.stringify({ mpp: 'message', data: token }))
          }
        },
      })
      ```
      
      ### Message Types
      
      ```typescript
      // Client → Server
      { mpp: 'authorization', authorization: string }     // Payment credential
      { mpp: 'payment-close-request' }                    // Request channel close
      
      // Server → Client
      { mpp: 'message', data: string }                    // Paid content
      { mpp: 'payment-need-voucher', data: NeedVoucherEvent } // Top-up request
      { mpp: 'payment-receipt', data: SessionReceipt }    // Payment receipt
      { mpp: 'payment-close-ready', data: SessionReceipt } // Close confirmation
      { mpp: 'payment-error', status: number, message: string } // Error
      ```
      
      ### Key Differences from SSE
      
      - **Bidirectional**: Both credential submission and content delivery happen over the same WebSocket connection (SSE uses separate HTTP POST for vouchers)
      - **In-band close**: Channel close negotiation happens via WebSocket messages rather than HTTP requests
      - **Security hardening**: Close receipts bound to signed close amount, spend committed only on actual delivery, local `maxDeposit` enforced, delivered chunks tracked for fallback close on disconnect
      
      ## Channel Recovery After Restarts
      
      ### Server bootstrap (preferred)
      
      Set `bootstrap: true` on the session method and the server emits bootstrap hints so a returning client lazily recovers its previous channel from the same protected route before opening a new one:
      
      ```typescript
      tempo.session({ currency, recipient, store, bootstrap: true })
      ```
      
      Bootstrap snapshots carry the highest signed voucher, so the client reconciles against on-chain state and continues the existing cumulative sequence.
      
      Note what bootstrap can and cannot do. Challenge generation is computed from method defaults and does not query the store, and an unpaid first request carries no payer identity - so the server has nothing to look up for a client it has never seen on that route. Client-side channel persistence (`channelStore`) remains the reliable resumption path across processes; bootstrap covers the same-route case.
      
      ### Manual `channelId` (fallback)
      
      Pass `channelId` to `mppx.session()` so returning clients recover existing on-chain channels instead of opening new ones. The `SessionMethodDetails` type has an optional `channelId` field. When included in the 402 challenge, the client SDK's `tryRecoverChannel()` reads on-chain state and resumes the existing channel.
      
      The server doesn't auto-populate this - it's the application's job:
      
      ```typescript
      import { Credential } from 'mppx'
      
      // Extract channelId from the credential's payload before calling mppx.session()
      let channelId: string | undefined
      try {
        const credential = Credential.fromRequest(request)
        if (credential.challenge.intent === 'session') {
          const payload = credential.payload as { channelId?: string }
          channelId = payload.channelId
        }
      } catch {
        // No credential yet
      }
      
      // Pass channelId so the 402 challenge includes it for client-side recovery
      const result = await mppx.session({
        amount: tickCost,
        unitType: 'token',
        ...(channelId && { channelId }),
      })(request)
      ```
      
      **Why this matters:** After a server restart, even with a persistent store, the first voucher from a returning client may fail verification (e.g., store was briefly unavailable). Without `channelId` in the re-issued 402 challenge, the client opens a new channel - locking more USDC in escrow while the old deposit sits unclaimed. With `channelId`, the client recovers the existing on-chain channel and continues using it.
      
      ## Session Receipts
      
      Session receipts differ from charge receipts:
      
      - `reference` contains `channelId` (a bytes32 hash), not a transaction hash.
      - `acceptedCumulative` is the running total the server has accepted, `spent` the amount consumed, and `units` the metered count - a per-request receipt reports cumulative state, not the cost of that one request. Deriving a per-call price by reading the challenge amount at credential time gives blank or wrong values; compute deltas from `acceptedCumulative` instead.
      - `txHash` is only populated once settlement lands on-chain, so it is absent on ordinary voucher receipts.
      - Calling `close()` returns a receipt that includes the `txHash` of the settlement transaction.
      
      ## Store Backends
      
      Sessions require state storage for channel data. Available backends:
      
      | Backend | Usage | Notes |
      |---------|-------|-------|
      | `Store.memory()` | In-memory | Development only, state lost on restart |
      | `Store.redis()` | Redis client | Self-hosted or managed Redis |
      | `Store.cloudflare()` | Cloudflare KV | Edge-compatible |
      | `Store.upstash()` | Upstash Redis | Serverless Redis |
      | Custom | Implement interface | Requires async `get`, `set`, `delete` methods |
      
      ## Escrow Contracts (Sessions v1)
      
      Everything in this section (through Payer-Initiated Recovery) describes the **v1** contract-backed flow used by `tempo.sessionLegacy`. Sessions v2 replaces it with the TIP-20 channel precompile and does not expose these operations; the payer-initiated recovery path below still matters for channels opened under v1. The `TempoStreamChannel` on-chain escrow manages deposits, settlements, and refunds for v1.
      
      ### Deployed Addresses
      
      - **Mainnet** (chain 4217): `<ESCROW_MAINNET>`
      - **Testnet Moderato** (chain 42431): `<ESCROW_TESTNET>`
      
      ### Contract Operations
      
      - **deposit**: Lock tokens into a channel.
      - **settle**: Batch-settle vouchers, updating the channel's consumed amount.
      - **close**: Final settlement plus refund of unspent tokens to the client. **Payee-only** - only the server (payee) can call this.
      - **requestClose**: Payer-initiated close request. Starts a grace period during which the server can still settle outstanding vouchers.
      - **withdraw**: Payer reclaims deposit after the grace period expires. Emits `ChannelExpired`.
      - **CLOSE_GRACE_PERIOD**: View function returning the grace period duration (900 seconds / 15 minutes on mainnet).
      
      ### Payer-Initiated Recovery
      
      If the server is unresponsive or fails to close a channel, the payer can recover locked funds using a two-step process:
      
      1. **`requestClose(channelId)`** - signals intent to close. The server gets a grace period (15 min on mainnet) to settle any outstanding vouchers it holds.
      2. **Wait for grace period** - `CLOSE_GRACE_PERIOD()` returns the duration. The `CloseRequested` event includes `closeGraceEnd` timestamp.
      3. **`withdraw(channelId)`** - after the grace period, the payer reclaims the full unsettled deposit.
      
      ```typescript
      import { createClient, encodeFunctionData, http, parseAbi, type Hex } from 'viem'
      import { prepareTransactionRequest, readContract, sendRawTransaction, signTransaction } from 'viem/actions'
      import { tempo } from 'viem/chains'
      
      const ESCROW = '<ESCROW_MAINNET>' as Hex
      const USDC = '<USDC_TEMPO_MAINNET>' as Hex
      
      const abi = parseAbi([
        'function requestClose(bytes32 channelId)',
        'function withdraw(bytes32 channelId)',
        'function CLOSE_GRACE_PERIOD() view returns (uint64)',
        'function channels(bytes32) view returns (bool finalized, uint64 closeRequestedAt, address payer, address payee, address token, address authorizedSigner, uint128 deposit, uint128 settled)',
      ])
      
      const client = createClient({ account, chain: tempo, transport: http('https://rpc.tempo.xyz') })
      
      // Step 1: Request close
      const data = encodeFunctionData({ abi, functionName: 'requestClose', args: [channelId] })
      const prepared = await prepareTransactionRequest(client, {
        account, calls: [{ to: ESCROW, data }], feeToken: USDC,
      } as never)
      const serialized = await signTransaction(client, { ...prepared, account } as never) as Hex
      await sendRawTransaction(client, { serializedTransaction: serialized })
      
      // Step 2: Wait for grace period (15 min on mainnet), then withdraw
      const withdrawData = encodeFunctionData({ abi, functionName: 'withdraw', args: [channelId] })
      // ... same prepare + sign + send pattern
      ```
      
      Key details:
      - `close()` is **payee-only**. Calling it as the payer reverts with `NotPayee()`.
      - During the grace period, the server can still call `close()` with the highest voucher to claim earned funds.
      - If the server does nothing, `withdraw()` returns the entire unsettled deposit to the payer.
      - Tempo transactions use the `calls` pattern and `feeToken` for gas payment in USDC. Use `signTransaction(client, ...)` (not `account.signTransaction`) to get Tempo's custom serializer.
      - To find open channels, query `ChannelOpened` events filtered by payer address, then check each channel's state via `channels(channelId)`. Paginate event queries in chunks of 100k blocks (Tempo RPC limit).
      
      ## Security
      
      - Voucher replay checks cover settled vouchers and mismatched credential sources (hardened in mppx 0.8.7), so a voucher cannot be re-presented after its cumulative amount has been settled, nor reused from a different declared source.
      - WebSocket close receipts are bound to the signed close amount, spend is committed only when chunks are actually delivered, local `maxDeposit` is enforced on streamed voucher requests, and delivered chunks are tracked for a fallback close on disconnect.
      - `maxDeposit` is the payer's hard ceiling on funds at risk in a channel. Set it deliberately: it bounds the loss if a server never closes.
      
      ## Performance Characteristics
      
      | Metric | Value |
      |--------|-------|
      | Voucher verification | ~microseconds (single `ecrecover`) |
      | RPC calls during session | None (only on open/close/settle) |
      | On-chain cost | Amortized: 0.001 USD total vs 0.001 USD per request for charge |
      | Throughput | Hundreds of vouchers per second per channel |
      
    • stripe-method.md 10.9 KB
      # Stripe Payment Method
      
      ## Overview
      
      Stripe MPP supports **two payment methods**:
      
      - **Fiat (SPT)** - **Shared Payment Tokens** let one Stripe account (client) create a token that another Stripe account (server) can use to charge cards, wallets, and other Stripe-supported methods. Requires a US legal entity. The server identifies itself via its Stripe **profile** (`profile_...`) ID as `networkId`.
      - **Crypto** - direct on-chain payment using Stripe-managed crypto deposit addresses on Tempo, captured automatically when funds settle. See "Crypto (On-Chain) Method" below.
      
      SPT flow:
      1. Server responds with 402 challenge containing Stripe payment requirements.
      2. Client creates an SPT (via the `@stripe/link-cli` spend-request flow, or a server-side proxy that holds the Stripe secret key).
      3. Client sends the SPT as the credential payload.
      4. Server creates a PaymentIntent using the SPT and confirms payment.
      
      SPTs are single-use, scoped to a specific amount and currency, and bound to the server's Stripe profile ID.
      
      ---
      
      ## Server Setup
      
      Import Stripe charge from `mppx/server` or `mppx/stripe`:
      
      ```ts
      import Stripe from 'stripe'
      import { stripe } from 'mppx/server'
      ```
      
      ### With Stripe SDK Instance
      
      ```ts
      const charge = stripe.charge({
        client: new Stripe(process.env.STRIPE_SECRET_KEY!),
        networkId: process.env.STRIPE_PROFILE_ID!, // Stripe profile ID (profile_...)
        paymentMethodTypes: ['card'],
      })
      ```
      
      ### With Secret Key
      
      ```ts
      const charge = stripe.charge({
        secretKey: process.env.STRIPE_SECRET_KEY!,
        networkId: process.env.STRIPE_PROFILE_ID!, // profile_...
        paymentMethodTypes: ['card'],
      })
      ```
      
      ### With Metadata
      
      Attach metadata to the PaymentIntent for tracking, reconciliation, or plan gating:
      
      ```ts
      const charge = stripe.charge({
        client: new Stripe(process.env.STRIPE_SECRET_KEY!),
        networkId: process.env.STRIPE_PROFILE_ID!, // profile_...
        paymentMethodTypes: ['card'],
        metadata: { plan: 'pro', feature: 'api-access' },
      })
      ```
      
      ### Multiple Payment Methods
      
      Accept cards, Link, and other Stripe-supported methods:
      
      ```ts
      const charge = stripe.charge({
        secretKey: process.env.STRIPE_SECRET_KEY!,
        networkId: process.env.STRIPE_PROFILE_ID!, // profile_...
        paymentMethodTypes: ['card', 'link'],
      })
      ```
      
      ### Server Parameters
      
      | Parameter | Type | Required | Description |
      |---|---|---|---|
      | `client` | `Stripe` | One of `client` or `secretKey` | Stripe SDK instance |
      | `secretKey` | `string` | One of `client` or `secretKey` | Stripe secret key. mppx then makes raw API calls to Stripe instead of using an SDK instance - use this when you do not need to customize the SDK |
      | `networkId` | `string` | Yes | Stripe profile (`profile_...`) ID from your Stripe Dashboard |
      | `paymentMethodTypes` | `string[]` | Yes | Accepted payment methods (e.g. `['card']`, `['card', 'link']`) |
      | `decimals` | `number` | No | Currency decimal places (`2` for fiat) |
      | `metadata` | `Record<string, string>` | No | Key-value pairs attached to the PaymentIntent |
      | `html` | `object` | No | Payment-link mode: `{ createTokenUrl, publishableKey }` renders a browser payment page for the Stripe method |
      
      ---
      
      ## Client Setup
      
      Import from `mppx/client` or `mppx/stripe`:
      
      ```ts
      import { stripe } from 'mppx/client'
      ```
      
      ### Simple Setup
      
      ```ts
      const charge = stripe({
        client: stripeJs, // Stripe.js instance (optional)
        createToken: async (params) => {
          const res = await fetch('/api/create-spt', {
            method: 'POST',
            body: JSON.stringify(params),
          })
          return res.json()
        },
        paymentMethod: 'pm_card_visa', // default payment method
      })
      ```
      
      ### With Stripe Elements
      
      Use the `onChallenge` callback to render Stripe Elements and collect the payment method interactively:
      
      ```ts
      const charge = stripe({
        createToken: async (params) => {
          const res = await fetch('/api/create-spt', {
            method: 'POST',
            body: JSON.stringify(params),
          })
          return res.json()
        },
        onChallenge: async (challenge, elements) => {
          // Render Stripe Elements UI for user to enter card details
          const { paymentMethod } = await elements.submit()
          return { paymentMethod: paymentMethod.id }
        },
      })
      ```
      
      ### Manual Flow
      
      For full control over credential creation:
      
      ```ts
      import { Challenge } from 'mppx'
      
      const challenge = Challenge.fromResponse(response)
      const credential = await charge.createCredential({
        challenge,
        context: { paymentMethod: 'pm_card_visa' },
      })
      ```
      
      ### Client Parameters
      
      | Parameter | Type | Required | Description |
      |---|---|---|---|
      | `client` | `StripeJs` | No | Stripe.js instance (for Elements integration) |
      | `createToken` | `(params) => Promise<SPT>` | Yes | Callback to create SPT via proxy endpoint |
      | `externalId` | `string` | No | External reference ID for tracking |
      | `paymentMethod` | `string` | No | Default payment method ID (e.g. `pm_card_visa`) |
      
      ---
      
      ## SPT Creation
      
      SPT creation requires a Stripe secret key, so it cannot happen in an untrusted client. The current canonical approach is the **`@stripe/link-cli`** spend-request flow:
      
      ```bash
      # 1. Create a spend request (issues the SPT). Add --test in a sandbox.
      npx @stripe/link-cli spend-request create \
        --payment-method-id csmrpd_xxx \
        --amount 100 \
        --credential-type shared_payment_token \
        --network-id profile_... \
        --request-approval
      
      # 2. Pay the MPP endpoint using the spend request
      npx @stripe/link-cli mpp pay https://your-endpoint.com/resource \
        --spend-request-id lsrq_xxx \
        --method POST \
        --data '{ ... }'
      ```
      
      For a programmatic client, the `createToken` callback on the client method (above) calls a server-side endpoint you control that holds the Stripe secret key and returns the SPT, which the client then includes in the credential payload. The legacy `stripe.rawRequest('POST', '/v1/shared_payment/granted_tokens', ...)` endpoint is no longer documented in Stripe's canonical MPP guide - prefer the `@stripe/link-cli` flow.
      
      ### Security: derive SPT parameters server-side
      
      The SPT proxy endpoint is a payment-authorization boundary, not a passthrough. Per Stripe's guidance, the server **must** derive SPT parameters (amount, currency, expiry, limits) itself rather than accepting them from the client: "A thin proxy that forwards client-supplied parameters effectively delegates payment authorization to an untrusted client."
      
      In practice: take the challenge (or your own pricing table) as the source of truth for amount and currency, and never let a request body set them.
      
      ## Crypto (On-Chain) Method
      
      Stripe MPP can also accept direct on-chain payments via Tempo crypto deposit addresses. Stripe generates a deposit address per PaymentIntent and captures automatically when funds settle on-chain. Crypto PaymentIntents require API version `2026-03-25.preview` or later.
      
      **Availability**: stablecoin payments require requesting the **Stablecoins and Crypto** payment method in the Stripe Dashboard, and are available to businesses in all US states except New York, plus 30+ other countries on request. Fiat SPT requires a US legal entity.
      
      ```ts
      import Stripe from 'stripe'
      
      const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!, {
        apiVersion: '2026-03-25.preview',
      })
      
      // Generate a Tempo deposit address backed by a Stripe PaymentIntent
      const paymentIntent = await stripeClient.paymentIntents.create({
        amount: 1000,
        currency: 'usd',
        payment_method_types: ['crypto'],
        payment_method_data: { type: 'crypto' },
        payment_method_options: {
          crypto: { mode: 'deposit', deposit_options: { networks: ['tempo'] } },
        },
        confirm: true,
      })
      // paymentIntent.next_action.crypto_display_details.deposit_addresses.tempo.address
      // -> use as the `recipient` in a tempo.charge() method
      ```
      
      Present fiat (SPT) and crypto together on one endpoint with `Mppx.compose(...)`. See [docs.stripe.com/payments/machine/mpp](https://docs.stripe.com/payments/machine/mpp) for the full two-method handler and the [deposit mode integration guide](https://docs.stripe.com/payments/deposit-mode-stablecoin-payments) for how crypto PaymentIntents behave.
      
      Note that Stripe's canonical example derives the MPP `secretKey` from the Stripe secret rather than managing a separate one:
      
      ```ts
      const mppSecretKey = crypto.createHmac('sha256', process.env.STRIPE_SECRET_KEY!)
        .update('mpp-challenge-signing').digest('base64')
      ```
      
      Sandbox PaymentIntents do not monitor crypto testnets, so testnet deposits are not auto-detected - use Stripe's simulate-crypto-deposit test helper.
      
      ---
      
      ## Request Fields
      
      The challenge `request` parameter (base64url-encoded JSON) contains:
      
      | Field | Type | Required | Description |
      |---|---|---|---|
      | `amount` | `string` | Yes | Payment amount in smallest currency unit |
      | `currency` | `string` | Yes | ISO 4217 currency code (e.g. `usd`, `eur`) |
      | `decimals` | `number` | Yes | Currency decimal places (e.g. `2` for cents) |
      | `description` | `string` | No | Human-readable payment description |
      | `expires` | `string` | No | ISO 8601 expiration timestamp (defaults to 5 minutes) |
      | `externalId` | `string` | No | External reference for idempotency/tracking |
      | `methodDetails.networkId` | `string` | Yes | Stripe Business Network profile ID |
      | `methodDetails.paymentMethodTypes` | `string[]` | Yes | Accepted payment method types |
      | `methodDetails.metadata` | `object` | No | Metadata key-value pairs |
      
      Example decoded request:
      
      ```json
      {
        "amount": 100,
        "currency": "usd",
        "description": "API access",
        "methodDetails": {
          "networkId": "acct_1234567890",
          "paymentMethodTypes": ["card"],
          "metadata": { "plan": "pro" }
        }
      }
      ```
      
      ---
      
      ## Credential Payload
      
      The credential `payload` sent by the client contains:
      
      | Field | Type | Required | Description |
      |---|---|---|---|
      | `spt` | `string` | Yes | Shared Payment Token (starts with `spt_`) |
      | `externalId` | `string` | No | External reference for tracking |
      
      Example credential payload:
      
      ```json
      {
        "spt": "<STRIPE_SPT_TOKEN>",
        "externalId": "order-456"
      }
      ```
      
      The server uses the SPT to create a PaymentIntent, confirm payment, and return a receipt with the PaymentIntent ID as the `reference`.
      
      ---
      
      ## Full Example
      
      ### Server
      
      ```ts
      import Stripe from 'stripe'
      import { Mppx, stripe } from 'mppx/server'
      
      const mppx = Mppx.create({
        methods: [
          stripe.charge({
            client: new Stripe(process.env.STRIPE_SECRET_KEY!),
            networkId: process.env.STRIPE_NETWORK_ID!,
            paymentMethodTypes: ['card'],
            metadata: { service: 'my-api' },
          }),
        ],
      })
      
      export async function handler(req: Request) {
        const result = await mppx.charge({ amount: '1.00' })(req)
        if (result.status === 402) return result.challenge
        return result.withReceipt(Response.json({ data: 'paid content' }))
      }
      ```
      
      ### Client
      
      ```ts
      import { Mppx, stripe } from 'mppx/client'
      
      Mppx.create({
        methods: [
          stripe({
            createToken: async (params) => {
              const res = await fetch('/api/create-spt', {
                method: 'POST',
                body: JSON.stringify(params),
              })
              return res.json()
            },
            paymentMethod: 'pm_card_visa',
          }),
        ],
      })
      
      const res = await fetch('https://api.example.com/paid')
      // 402 -> SPT creation -> credential -> 200
      ```
      
    • subscriptions.md 3.5 KB
      # Subscription Intent (Tempo)
      
      Recurring access on Tempo via an **authorized access key** rather than a payment per request. Activation collects the first period and authorizes a key; later requests reuse that key with no per-request payment; the server renews in the background; access is cancelled or revoked server-side.
      
      Subscriptions are Tempo-only. The intent is `subscription`, alongside `charge` and `session`.
      
      ## Server Setup
      
      ```ts
      import { Mppx, Store, tempo } from 'mppx/server'
      
      const mppx = Mppx.create({
        methods: [
          tempo.subscription({
            currency: '<USDC_TEMPO_MAINNET>',
            recipient: '0xYourAddress',
            periodCount: '1',
            periodUnit: 'month',
            store: Store.redis(redisClient), // durable store required
          }),
        ],
        secretKey: process.env.MPP_SECRET_KEY,
      })
      ```
      
      A **durable store is mandatory**. Subscription state (the authorized key, period boundaries, cancellation and revocation timestamps) must survive restarts, or renewals and access checks break.
      
      ### Period Units
      
      There is a divergence between the SDK and the docs worth knowing before you pick a value:
      
      - The mppx type is `SubscriptionPeriodUnit = 'dev_second' | 'day' | 'week'`.
      - The published request schema documents `periodUnit` as `day`, `week`, or `month`.
      
      `dev_second` exists in the SDK for fast test cycles (a "period" of one second) but is undocumented; `month` is documented but absent from the SDK's union. Verify against the version you have pinned before relying on either edge, and prefer `day` or `week` for code that must work on both.
      
      `periodCount` is a positive integer count of `periodUnit` values per billing period.
      
      ## Request Handling
      
      ```ts
      const result = await mppx.subscription({ /* per-route overrides */ })(request)
      if (result.status === 402) return result.challenge
      return result.withReceipt(Response.json({ data: '...' }))
      ```
      
      Two options shape how strictly access is gated:
      
      - `requireCredential` - whether an active subscription still has to present a credential on each request, or whether server-side resolution alone suffices.
      - `resolve` - a hook to map an incoming request to a subscription identity when it is not carried in the credential.
      
      `Subscription.fromStore` reads current subscription state directly from the store, for endpoints that need to check entitlement without running the payment handler.
      
      ## Renewal
      
      Renewal happens outside the request path. `tempo.renewSubscription` drives an overdue subscription forward from a background worker:
      
      ```ts
      import { tempo } from 'mppx/server'
      
      // in a cron job or queue consumer
      await tempo.renewSubscription({ store, client, subscriptionId })
      ```
      
      **Concurrency contract:** if one request is already renewing a subscription, another receives `409` with `Retry-After: 1`. Treat that as "retry shortly", not as a failure - it exists so two workers cannot double-charge a period.
      
      ## Cancellation and Revocation
      
      Subscription records carry `canceledAt` and `revokedAt`. Cancelling stops future renewals while leaving the current period intact; revoking withdraws the authorized key immediately. Key revocation goes through the Accounts action `Actions.accessKey.revokeSync`, since the authorization lives on the payer's wallet rather than in your store.
      
      ## Relationship to Access Keys
      
      A subscription is an access key with a spend policy attached, so the agent-spend controls documented in `SKILL.md` apply: token limits, contract/function/recipient scopes, and expiry. The subscription flow authorizes the key on activation and relies on those scopes to bound what the key can do between renewals.
      
    • tempo-method.md 12.8 KB
      # Tempo Payment Method
      
      ## Overview
      
      Tempo is a blockchain purpose-built for payments. Key properties:
      
      - **No native gas token**: fees are paid in stablecoins (USDC, pathUSD), not ETH/SOL
      - **TIP-20 stablecoins**: pathUSD, USDC
      - **~500ms deterministic finality**
      - **Sub-cent fees** with fee sponsorship support
      - **2D nonces**: parallel transaction ordering (no stuck-tx bottlenecks)
      - **Payment lane**: dedicated lane for channel operations (sessions)
      
      ## Gas and Fee Tokens
      
      Tempo has **no native gas token**. Transaction fees are denominated in USD and paid in any TIP-20 stablecoin. Every transaction must know which token to use for fees. Three levels of precedence (highest first):
      
      1. **Transaction-level `feeToken`** - explicit per-transaction:
      ```typescript
      const prepared = await prepareTransactionRequest(client, {
        account,
        calls: [{ to, data }],
        feeToken: '<USDC_TEMPO_MAINNET>', // USDC mainnet (see Token Addresses table below)
      } as never)
      const serialized = await signTransaction(client, { ...prepared, account } as never)
      await sendRawTransaction(client, { serializedTransaction: serialized })
      ```
      
      2. **Account-level default via `setUserToken`** - one-time call to the FeeManager precompile:
      ```typescript
      import { setUserToken } from 'viem/tempo'
      await client.fee.setUserTokenSync({
        token: '<USDC_TEMPO_MAINNET>',
      })
      ```
      After this, all transactions from the account use USDC for gas unless overridden at the transaction level.
      
      3. **Validator default** - if neither is set, the validator's preferred token is used (unreliable - don't depend on this).
      
      **If no fee token can be determined, the transaction fails with `gas_limit: 0`.**
      
      The mppx SDK sets `feeToken` automatically for payment transactions. Direct on-chain calls (manual settle, close, custom contracts) must set it explicitly via `feeToken` in `prepareTransactionRequest` or ensure `setUserToken` was called.
      
      Note: Tempo transactions use a custom serialization format (type 0x76). Always use `signTransaction(client, ...)` from viem/actions (which uses the chain's serializer), NOT `account.signTransaction()` (which uses the default legacy serializer).
      
      ## Tempo Charge
      
      ### Server Configuration
      
      ```ts
      import { tempo } from "mppx/server";
      import { Expires } from "mppx";
      
      const tempoCharge = tempo.charge({
        currency: "<PATHUSD_TESTNET>",
        recipient: "0xYourAddress",
        decimals: 6,
        description: "API access",
        externalId: "order-123",
        feePayer: privateKeyToAccount("0xSponsorPrivateKey"), // optional
        testnet: true,
        waitForConfirmation: true,
      });
      ```
      
      **Parameters:**
      
      | Parameter | Type | Description |
      |---|---|---|
      | `currency` | `string` | TIP-20 token address (required) |
      | `recipient` | `string` | Payment recipient address (required) |
      | `decimals` | `number` | Token decimals (default: 18) |
      | `description` | `string` | Human-readable payment description |
      | `externalId` | `string` | External reference ID |
      | `feePayer` | `Account \| string` | Fee sponsor account or URL |
      | `testnet` | `boolean` | Use Tempo testnet |
      | `waitForConfirmation` | `boolean` | Wait for on-chain confirmation |
      
      **Per-call overrides:**
      
      ```ts
      const handler = mppx.charge({
        amount: "1.00",
        currency: "<USDC_TEMPO_MAINNET>", // override token (USDC.e mainnet)
        recipient: "0xDifferentRecipient",
        feePayer: "https://sponsor.example.com",
        expires: Expires.minutes(10),
      })(request);
      ```
      
      ### Client Configuration
      
      ```ts
      import { tempo } from "mppx/client";
      import { privateKeyToAccount } from "viem/accounts";
      
      const tempoClient = tempo.charge({
        account: privateKeyToAccount("0xYourPrivateKey"),
        autoSwap: true,
        clientId: "my-app",
        mode: "pull",
      });
      ```
      
      **Parameters:**
      
      | Parameter | Type | Description |
      |---|---|---|
      | `account` | `Account` | viem account (privateKey, Passkey, or WebCrypto) |
      | `autoSwap` | `boolean \| object` | Auto-swap from fallback stablecoin |
      | `clientId` | `string` | Client identifier |
      | `mode` | `'push' \| 'pull'` | Transaction broadcast mode (default: `'pull'`) |
      | `getClient` | `function` | Custom viem client factory |
      | `expectedChainId` | `number` | Reject charge challenges whose `methodDetails.chainId` differs - pins the client to one Tempo network (mppx 0.7.0+) |
      
      **autoSwap options:**
      
      ```ts
      // Simple - use defaults
      autoSwap: true
      
      // Advanced - configure slippage and allowed input tokens
      autoSwap: {
        slippage: 2, // percent
        tokenIn: ["0xFallbackTokenAddress"],
      }
      ```
      
      ## Push vs Pull Modes
      
      ### Pull Mode (Default)
      
      1. Client signs the transaction
      2. Client sends serialized signed tx to server
      3. Server broadcasts (enables fee sponsorship)
      4. Credential payload: `{ type: 'transaction', signature }`
      
      ```ts
      // Client - pull mode (default)
      const client = tempo.charge({
        account: privateKeyToAccount("0x..."),
        mode: "pull",
      });
      ```
      
      ### Push Mode
      
      1. Client builds, signs, and broadcasts the transaction itself
      2. Client sends tx hash to server for verification
      3. Credential payload: `{ type: 'hash', hash }`
      
      ```ts
      // Client - push mode (e.g. browser wallet)
      const client = tempo.charge({
        account: privateKeyToAccount("0x..."),
        mode: "push",
      });
      ```
      
      The server handles both modes automatically - no server-side changes needed.
      
      ## Fee Sponsorship
      
      Available in pull mode only. The server co-signs the transaction with the fee payer account before broadcasting, so the client never needs gas tokens.
      
      ```ts
      // Option 1: Direct account
      const charge = tempo.charge({
        currency: "<PATHUSD_TESTNET>",
        recipient: "0xRecipient",
        feePayer: privateKeyToAccount("0xSponsorPrivateKey"),
        testnet: true,
      });
      
      // Option 2: External sponsor service
      const charge = tempo.charge({
        currency: "<PATHUSD_TESTNET>",
        recipient: "0xRecipient",
        feePayer: "https://sponsor.example.com",
        testnet: true,
      });
      ```
      
      Fee sponsorship is ignored for push-mode clients (they broadcast themselves).
      
      ### Sponsorship Budget Caps
      
      Sponsoring gas means paying for other people's transactions, so the sponsor path carries two in-flight budget limits:
      
      | Parameter | Default | Purpose |
      |---|---|---|
      | `maxInFlightReservations` | `100` | Maximum sponsored transactions awaiting a terminal receipt |
      | `maxInFlightTotalFee` | `maxTotalFee * 10` | Maximum total fee reserved across in-flight sponsorships |
      
      Sponsorship also rejects non-canonical fee-payer calldata and client-supplied access lists before signing, so a client cannot smuggle extra state access into a transaction the server pays for.
      
      ## Relays
      
      A relay delegates credential validation and broadcast to Tempo API or a compatible MPP relay, so your server does not need its own RPC path or broadcast wallet:
      
      ```ts
      const charge = tempo.charge({
        currency: "<USDC_TEMPO_MAINNET>",
        recipient: "0xRecipient",
        relay: { apiKey: process.env.TEMPO_API_KEY, apiBaseUrl: "https://api.tempo.xyz" },
      })
      ```
      
      The relay contract has two hooks. `validate` (optional) checks whether a credential is valid for the underlying rail without reserving funds or making state changes; `broadcast` performs settlement. This mirrors the `validateCredential` / `broadcastCredential` split on the server. See [mpp.dev/advanced/relays](https://mpp.dev/advanced/relays).
      
      ## Optimistic Verification
      
      Skip waiting for on-chain confirmation. Returns immediately after simulation succeeds. Use when latency matters more than guaranteed confirmation.
      
      ```ts
      const charge = tempo.charge({
        currency: "<PATHUSD_TESTNET>",
        recipient: "0xRecipient",
        waitForConfirmation: false,
        testnet: true,
      });
      ```
      
      ## Tempo Session
      
      Since mppx 0.7.0, `tempo.session()` is the **v2** (TIP-1034 precompile) flow; the prior contract-backed flow is `tempo.sessionLegacy` (server and client). Use `tempo.session()` for `Mppx.create()` registration and `tempo.session.manager({ account, maxDeposit })` for direct lifecycle control (`.sse()`, `.close()`). `tempo.common()` registers the Tempo charge + session bundle in one call.
      
      ### Server Configuration
      
      ```ts
      import { Store, tempo } from "mppx/server";
      
      const tempoSession = tempo.session({
        currency: "<PATHUSD_TESTNET>",
        recipient: "0xYourAddress",
        store: Store.memory(),
        escrowContract: "<ESCROW_TESTNET>",
        sse: true,
      });
      ```
      
      **Parameters:**
      
      | Parameter | Type | Description |
      |---|---|---|
      | `currency` | `string` | TIP-20 token address |
      | `recipient` | `string` | Payment recipient |
      | `store` | `Store` | Session state storage |
      | `escrowContract` | `string` | Escrow contract address (v1) |
      | `sse` | `boolean` | Enable SSE streaming |
      | `settlementSchedule` | `object` | Server-owned automatic settlement by `units`, `amount`, and/or `intervalMs` |
      | `bootstrap` | `boolean` | Emit hints so returning clients recover a prior channel on the same route |
      | `voucherSigner` | `Account` | **v1 only.** Account authorized to sign vouchers (renamed from `authorizedSigner` in mppx 0.6.29; the on-chain `channels()` ABI field is still `authorizedSigner`) |
      
      Without a `settlementSchedule` or your own `tempo.settle()` sweep, the server never converts accepted vouchers into on-chain funds. See `references/sessions.md`.
      
      **Store options:**
      
      ```ts
      Store.memory()       // In-memory (development)
      Store.cloudflare()   // Cloudflare KV
      Store.upstash()      // Upstash Redis
      ```
      
      **Per-call configuration:**
      
      ```ts
      const handler = mppx.session({
        amount: "0.001",
        unitType: "token", // billing unit name: 'token', 'photo', 'word', etc.
      })(request);
      ```
      
      ### Client Configuration
      
      ```ts
      // Direct lifecycle control (.sse()/.close()) comes from tempo.session.manager()
      const session = tempo.session.manager({
        account: privateKeyToAccount("0x..."),
        maxDeposit: "5.00", // max tokens locked in escrow
      });
      ```
      
      The session lifecycle is auto-managed:
      - First request: opens a payment channel (escrow deposit)
      - Subsequent requests: signs vouchers (off-chain, instant)
      - `session.close()`: settles on-chain, reclaims unspent deposit
      
      **SSE streaming:**
      
      ```ts
      const stream = session.sse("https://api.example.com/stream");
      // Returns async iterable
      for await (const event of stream) {
        console.log(event);
      }
      ```
      
      ## Request Schema (Charge)
      
      | Field | Type | Required | Description |
      |---|---|---|---|
      | `amount` | `string` | Yes | Payment amount |
      | `currency` | `address` | Yes | TIP-20 token address |
      | `decimals` | `number` | No | Token decimals |
      | `recipient` | `address` | No | Override recipient |
      | `chainId` | `number` | No | Chain ID |
      | `externalId` | `string` | No | External reference |
      | `memo` | `hex` | No | On-chain memo |
      | `feePayer` | `boolean` | No | Request fee sponsorship |
      | `description` | `string` | No | Payment description |
      
      ## Testnet vs Mainnet
      
      ### Token Addresses
      
      | Network | Token | Address |
      |---|---|---|
      | Testnet | pathUSD | `<PATHUSD_TESTNET>` |
      | Mainnet | USDC.e (Bridged USDC) | `<USDC_TEMPO_MAINNET>` |
      
      ### Escrow Contracts
      
      | Network | Chain ID | Address |
      |---|---|---|
      | Mainnet | 4217 | `<ESCROW_MAINNET>` |
      | Testnet | 42431 | `<ESCROW_TESTNET>` |
      
      ### Configuration
      
      ```ts
      // Testnet
      const charge = tempo.charge({
        currency: "<PATHUSD_TESTNET>",
        recipient: "0xRecipient",
        testnet: true,
      });
      
      // Mainnet - use USDC.e (Bridged USDC), remove testnet flag
      const charge = tempo.charge({
        currency: "<USDC_TEMPO_MAINNET>",
        recipient: "0xRecipient",
      });
      ```
      
      ## Split Payments
      
      Distribute a single charge across multiple recipients in one transaction (0.4.12+). The client constructs a multi-transfer transaction; the server verifies all splits match the challenge requirements.
      
      ```ts
      // Server - configure split recipients
      const charge = tempo.charge({
        currency: "<PATHUSD_TESTNET>",
        recipients: [
          { address: "0xPlatform", share: 0.9 },  // 90% to platform
          { address: "0xCreator", share: 0.1 },    // 10% to creator
        ],
        testnet: true,
      });
      
      // Per-request with dynamic splits
      const result = await mppx.charge({
        amount: "1.00",
        recipients: [
          { address: "0xPlatform", amount: "0.90" },
          { address: "0xCreator", amount: "0.10" },
        ],
      })(request);
      ```
      
      Documented constraints: **1-10 splits per charge**, each split may carry its own memo, and `expectedRecipients` restricts which recipient addresses a split is allowed to target (server-side validation). The server verifies that the sum of split amounts equals the total charge amount and that each transfer targets an expected recipient. See [mpp.dev/guides/split-payments](https://mpp.dev/guides/split-payments) for the full guide.
      
      ## Stripe Integration
      
      Create dynamic recipient addresses backed by Stripe PaymentIntents for deposit-mode crypto payments.
      
      ```ts
      // Stripe PaymentIntent with Tempo network
      const paymentIntent = await stripe.paymentIntents.create({
        amount: 1000,
        currency: "usd",
        payment_method_data: { type: "crypto" },
        payment_method_options: {
          crypto: { mode: "deposit", deposit_options: { networks: ["tempo"] } },
        },
        confirm: true,
      });
      ```
      
      Requires Stripe API version `2026-03-25.preview` or later. See the [deposit mode integration guide](https://docs.stripe.com/payments/deposit-mode-stablecoin-payments) for full PaymentIntent configuration, and `references/stripe-method.md` for the MPP-side wiring.
      
    • transports.md 11.3 KB
      # Transports
      
      ## HTTP Transport
      
      HTTP is the primary transport binding for MPP, using standard RFC 9110 HTTP Authentication headers. All JSON payloads are encoded as **base64url without padding** (RFC 4648 section 5).
      
      ### Challenge Delivery
      
      The server signals payment requirements via `WWW-Authenticate` using the `Payment` scheme:
      
      ```http
      WWW-Authenticate: Payment id="abc123", method="tempo", intent="charge", request="eyJhbW91bnQ..."
      ```
      
      Parameters are key-value pairs following standard HTTP auth parameter syntax. Multiple challenges (multiple payment methods) use separate `WWW-Authenticate` headers.
      
      ### Credential Delivery
      
      The client submits payment proof via `Authorization` using the `Payment` scheme:
      
      ```http
      Authorization: Payment eyJjaGFsbGVuZ2UiOns...
      ```
      
      The value after `Payment ` is a single base64url-encoded JSON object containing `challenge`, `payload`, and optional `source`.
      
      ### Receipt Delivery
      
      The server confirms payment via the `Payment-Receipt` header:
      
      ```http
      Payment-Receipt: eyJjaGFsbGVuZ2VJZCI6...
      ```
      
      The value is base64url-encoded JSON containing `challengeId`, `method`, `reference`, `settlement`, `status`, and `timestamp`.
      
      ### Full HTTP Flow
      
      ```http
      # Step 1: Client requests protected resource
      GET /api/data HTTP/1.1
      Host: api.example.com
      
      # Step 2: Server responds with 402 + challenge
      HTTP/1.1 402 Payment Required
      WWW-Authenticate: Payment id="<EXAMPLE_CHALLENGE_ID>",
        realm="api.example.com",
        method="tempo",
        intent="charge",
        request="<JWT_REQUEST_PAYLOAD>",
        expires="1711929600",
        description="API access: $0.01"
      Cache-Control: no-store
      Content-Type: application/problem+json
      
      {"type":"https://paymentauth.org/problems/payment-required","title":"Payment Required"}
      
      # Step 3: Client fulfills payment and retries with credential
      GET /api/data HTTP/1.1
      Host: api.example.com
      Authorization: Payment eyJjaGFsbGVuZ2UiOnsiaWQiOiJkQmpm...InR5cGUiOiJoYXNoIn19
      
      # Step 4: Server verifies payment and returns resource with receipt
      HTTP/1.1 200 OK
      Payment-Receipt: eyJjaGFsbGVuZ2VJZCI6ImRCamZ0SmVaNENW...
      Cache-Control: private
      Content-Type: application/json
      
      {"data": "protected content"}
      ```
      
      ---
      
      ## MCP Transport
      
      The MCP (Model Context Protocol) transport uses JSON-RPC encoding for payment flows. This enables MCP tool calls to require payment without breaking the JSON-RPC protocol.
      
      The mppx MCP wrappers import from `mppx/mcp/server` and `mppx/mcp/client` (mppx 0.8.0+; the `mppx/mcp-sdk/*` specifiers remain as aliases). MCP-over-HTTP challenges settle in the same payment-aware fetch as HTTP `402`s: `Transport.http()` extracts the `-32042` challenge from `error.data.challenges` and retries with the credential in `_meta`.
      
      ### Challenge Delivery
      
      Payment challenges are returned as JSON-RPC errors with code `-32042` (servers may also issue `-32043`):
      
      ```json
      {
        "jsonrpc": "2.0",
        "id": "req-1",
        "error": {
          "code": -32042,
          "message": "Payment Required",
          "data": {
            "challenges": [
              {
                "id": "<EXAMPLE_CHALLENGE_ID>",
                "realm": "my-mcp-server",
                "method": "tempo",
                "intent": "charge",
                "request": "<JWT_REQUEST_PAYLOAD>",
                "expires": "1711929600",
                "description": "Tool call: premium_analysis"
              }
            ]
          }
        }
      }
      ```
      
      The `challenges` array can contain multiple challenges (one per supported payment method), mirroring the multiple `WWW-Authenticate` headers in HTTP.
      
      ### Credential Delivery
      
      Payment credentials are embedded in the tool call's `_meta` field:
      
      ```json
      {
        "jsonrpc": "2.0",
        "id": "req-2",
        "method": "tools/call",
        "params": {
          "name": "premium_analysis",
          "arguments": { "query": "analyze this" },
          "_meta": {
            "org.paymentauth/credential": {
              "challenge": {
                "id": "<EXAMPLE_CHALLENGE_ID>",
                "realm": "my-mcp-server",
                "method": "tempo",
                "intent": "charge",
                "request": "<JWT_REQUEST_PAYLOAD>",
                "expires": "1711929600"
              },
              "payload": { "type": "hash", "hash": "0xabc123..." },
              "source": "did:pkh:eip155:4217:0x1234..."
            }
          }
        }
      }
      ```
      
      The credential is a JSON object (not base64url-encoded) under the namespaced key `org.paymentauth/credential`.
      
      ### Receipt Delivery
      
      Payment receipts are embedded in the result's `_meta` field:
      
      ```json
      {
        "jsonrpc": "2.0",
        "id": "req-2",
        "result": {
          "content": [
            { "type": "text", "text": "Analysis result: ..." }
          ],
          "_meta": {
            "org.paymentauth/receipt": {
              "challengeId": "<EXAMPLE_CHALLENGE_ID>",
              "method": "tempo",
              "reference": "0xdef456...",
              "settlement": { "amount": "0.01", "currency": "USD" },
              "status": "success",
              "timestamp": "2025-04-01T12:00:00Z"
            }
          }
        }
      }
      ```
      
      ### Full MCP Flow
      
      ```json
      // Step 1: Client calls tool
      {
        "jsonrpc": "2.0", "id": "1",
        "method": "tools/call",
        "params": { "name": "premium_tool", "arguments": {} }
      }
      
      // Step 2: Server returns -32042 error with challenges
      {
        "jsonrpc": "2.0", "id": "1",
        "error": {
          "code": -32042,
          "message": "Payment Required",
          "data": {
            "challenges": [{
              "id": "abc", "method": "tempo", "intent": "charge",
              "request": "<JWT_REQUEST_PAYLOAD>", "expires": "1711929600"
            }]
          }
        }
      }
      
      // Step 3: Client retries with credential in _meta
      {
        "jsonrpc": "2.0", "id": "2",
        "method": "tools/call",
        "params": {
          "name": "premium_tool",
          "arguments": {},
          "_meta": {
            "org.paymentauth/credential": {
              "challenge": { "id": "abc", "method": "tempo", "intent": "charge", "request": "<JWT_REQUEST_PAYLOAD>", "expires": "1711929600" },
              "payload": { "type": "hash", "hash": "0x..." }
            }
          }
        }
      }
      
      // Step 4: Server returns result with receipt in _meta
      {
        "jsonrpc": "2.0", "id": "2",
        "result": {
          "content": [{ "type": "text", "text": "Premium result" }],
          "_meta": {
            "org.paymentauth/receipt": {
              "challengeId": "abc", "method": "tempo", "reference": "0x...",
              "settlement": { "amount": "0.01", "currency": "USD" },
              "status": "success", "timestamp": "2025-04-01T12:00:00Z"
            }
          }
        }
      }
      ```
      
      ---
      
      ## Comparison Table
      
      | Aspect | HTTP | MCP / JSON-RPC | WebSocket |
      |---|---|---|---|
      | **Challenge** | `WWW-Authenticate: Payment ...` header | JSON-RPC error `-32042` in `error.data.challenges` | `payment-error` message |
      | **Credential** | `Authorization: Payment <base64url>` header | `_meta.org.paymentauth/credential` in params | `authorization` message |
      | **Receipt** | `Payment-Receipt: <base64url>` header | `_meta.org.paymentauth/receipt` in result | `payment-receipt` message |
      | **Encoding** | Base64url without padding (RFC 4648) | Native JSON objects | Native JSON objects |
      | **Status code** | HTTP 402 | JSON-RPC error code -32042 (or -32043) | `payment-error` with status |
      | **Multiple methods** | Multiple `WWW-Authenticate` headers | Multiple entries in `challenges` array | N/A (single method per connection) |
      
      ---
      
      ## JSON-RPC Transport
      
      For non-MCP JSON-RPC services, the encoding is identical to the MCP transport. The same `-32042` error code, `_meta` namespacing, and JSON object encoding apply. This ensures any JSON-RPC service can adopt MPP without protocol-specific adaptations.
      
      The only difference is the absence of MCP-specific semantics (tool names, content arrays). The credential and receipt still use the `org.paymentauth/credential` and `org.paymentauth/receipt` keys in `_meta`.
      
      ---
      
      ## WebSocket Transport
      
      WebSocket transport enables real-time streaming payments alongside data delivery. Added in mppx 0.5.x for session-based billing over persistent connections (e.g. LLM token streaming).
      
      ### Message Protocol
      
      All messages are JSON objects with a discriminated `mpp` field:
      
      | Message Type | Direction | Purpose |
      |---|---|---|
      | `{ mpp: 'authorization', authorization: string }` | Client → Server | Submit payment credential |
      | `{ mpp: 'message', data: string }` | Server → Client | Deliver paid content |
      | `{ mpp: 'payment-close-request' }` | Client → Server | Request channel close |
      | `{ mpp: 'payment-close-ready', data: SessionReceipt }` | Server → Client | Close confirmation with receipt |
      | `{ mpp: 'payment-need-voucher', data: NeedVoucherEvent }` | Server → Client | Request top-up voucher |
      | `{ mpp: 'payment-receipt', data: SessionReceipt }` | Server → Client | Payment receipt |
      | `{ mpp: 'payment-error', status: number, message: string }` | Server → Client | Payment error |
      
      ### Server Integration
      
      ```ts
      import { Ws } from 'mppx/tempo'
      
      // WebSocket upgrade handler
      const wsHandler = Ws.serve({
        methods: [tempo.session({ currency, recipient, store })],
        secretKey,
        async onMessage(ws, data, stream) {
          // Process paid message, charge per unit
          await stream.charge()
          ws.send(JSON.stringify({ mpp: 'message', data: result }))
        },
      })
      ```
      
      ### How It Differs From SSE
      
      | Aspect | SSE | WebSocket |
      |---|---|---|
      | Direction | Server → Client (unidirectional) | Bidirectional |
      | Voucher delivery | Separate HTTP POST | In-band `authorization` message |
      | Close flow | Client POSTs close request | In-band `payment-close-request` |
      | Use case | Streaming responses (LLM output) | Interactive sessions, bidirectional streaming |
      
      ### Security
      
      - Close receipts are bound to signed close amount (prevents open receipt replay)
      - Spend committed only when chunks are actually delivered (prevents overcharging)
      - Local `maxDeposit` enforced on streamed voucher requests
      - Delivered chunks tracked for fallback close on disconnect
      
      ---
      
      ## Transport-Agnostic Design
      
      The three core primitives - Challenge, Credential, and Receipt - remain identical regardless of transport. Only the encoding and delivery mechanism changes:
      
      - **Challenge**: Same fields (`id`, `realm`, `method`, `intent`, `request`, `expires`, etc.) whether serialized into an HTTP header or a JSON-RPC error.
      - **Credential**: Same structure (`challenge`, `payload`, `source`) whether base64url-encoded in `Authorization` or embedded as JSON in `_meta`.
      - **Receipt**: Same fields (`challengeId`, `method`, `reference`, `settlement`, `status`, `timestamp`) whether in `Payment-Receipt` header or `_meta`.
      
      This means payment method implementations (Tempo, Stripe, Lightning, custom) work across all transports without modification. The transport layer handles serialization; the payment method handles settlement.
      
      ```ts
      // Same payment method works with all transports
      const method = tempo.charge({ currency: '0x...', recipient: '0x...' })
      
      // HTTP transport
      const mppx = Mppx.create({ methods: [method], transport: 'http' })
      
      // MCP transport
      const server = McpServer.wrap(baseServer, { methods: [method] })
      
      // WebSocket transport (sessions only)
      const wsHandler = Ws.serve({ methods: [tempo.session({ ...opts })], secretKey })
      ```
      
      ## The `Transport` Namespace
      
      Transports are pluggable objects, exported as `Transport` from both `mppx/client` and `mppx/server`:
      
      | Export | Use when |
      |---|---|
      | `Transport.http()` | HTTP header encoding. Also extracts MCP-over-HTTP `-32042` challenges and retries with the credential in `_meta`, so one payment-aware fetch covers both |
      | `Transport.mcp()` | Handling raw JSON-RPC messages directly |
      | `Transport.mcpSdk()` | Integrating with `@modelcontextprotocol/sdk` |
      | `Transport.from()` | Building a custom transport for another wire format |
      
      A custom payment method can also override encoding for a single method via the `transport` option on `Method.toServer()` - see `references/custom-methods.md`.
      
    • typescript-sdk.md 23.4 KB
      # mppx TypeScript SDK Reference
      
      ## Installation
      
      ```bash
      npm install mppx viem
      ```
      
      **Peer dependencies** (install as needed, per mppx 0.8.15):
      - `viem` >= 2.54.0 (required)
      - `@modelcontextprotocol/sdk` >= 1.25.0 (for MCP integration)
      - `hono` >= 4.12.25 (for Hono middleware)
      - `express` >= 5 (for Express middleware)
      - `elysia` >= 1 (for Elysia middleware)
      
      ## Package Exports
      
      Authoritative `exports` keys from `mppx` 0.8.15:
      
      | Subpath | Purpose |
      |---|---|
      | `mppx` | Main entry, core primitives |
      | `mppx/client` | Client SDK (polyfill / manual fetch) |
      | `mppx/client/node` | Node-only client extras: SQLite session `ChannelStore` |
      | `mppx/server` | Server SDK (charge, session, compose) |
      | `mppx/proxy` | Proxy server with service routing |
      | `mppx/stripe`, `mppx/stripe/client`, `mppx/stripe/server` | Stripe payment method |
      | `mppx/evm`, `mppx/evm/client`, `mppx/evm/server` | EVM (EIP-3009) payment method |
      | `mppx/x402` | x402 interop ("exact" flow compatibility) |
      | `mppx/tempo` | Tempo `Session` and `Ws` utilities (note: `tempo`/`Mppx` are NOT here - import those from `mppx/server` or `mppx/client`) |
      | `mppx/html` | Payment link UI customization (Config, Text, Theme types, plus `Html.init(methodName)`, which returns the page context: `challenge`, `config`, `error`, `formattedAmount`, `label`, `root`, `submit`, `text`, `theme`, `vars`) |
      | `mppx/discovery` | OpenAPI-first discovery tooling |
      | `mppx/validation` | Programmatic server validation (the engine behind `mppx validate`) |
      | `mppx/cli`, `mppx/cli/plugins` | CLI config + plugin authoring |
      | `mppx/mcp/client` | MCP client wrapper (0.8.0+; `mppx/mcp-sdk/client` is a retained alias) |
      | `mppx/mcp/server` | MCP server wrapper (0.8.0+; `mppx/mcp-sdk/server` is a retained alias) |
      | `mppx/hono` | Hono framework middleware |
      | `mppx/express` | Express framework middleware |
      | `mppx/nextjs` | Next.js middleware |
      | `mppx/elysia` | Elysia framework middleware |
      
      Lightning, Stellar, Solana, Monad, RedotPay, and Card are **external packages** (`@buildonspark/lightning-mpp-sdk`, `@stellar/mpp`, `@solana/mpp`, `@monad-crypto/mpp`, `@redotpay/mpp`, `mpp-card`), not `mppx` subpaths.
      
      ## Server SDK (`mppx/server`)
      
      ### Creating a Server Instance
      
      ```ts
      import { Mppx, tempo } from 'mppx/server'
      
      const mppx = Mppx.create({
        methods: [tempo()],
        secretKey: process.env.MPP_SECRET_KEY, // HMAC secret for challenge binding (required)
        realm: 'My API',                       // defaults to env detection or "MPP Payment"
        transport: 'http',                      // optional, defaults to auto-detect
      })
      ```
      
      - `secretKey` - HMAC secret used for challenge binding. Required.
      - `realm` - human-readable service name. Defaults to environment detection or `"MPP Payment"`.
      - `methods` - array of payment method handlers (e.g. `tempo()`, `stripe()`).
      - `transport` - protocol transport, auto-detected by default.
      
      ### Charging per Request
      
      ```ts
      const result = await mppx.charge({
        amount: '0.001',
        currency: 'USD',          // optional, defaults to USD
        recipient: '0x...',       // optional, override default recipient
        description: 'API call',  // optional
        expires: Expires.minutes(5), // optional
        externalId: 'inv-123',    // optional, for idempotency/tracking
        feePayer: 'sender',       // optional, 'sender' | 'receiver'
      })(request)
      
      // result.status === 402 - payment required (result.challenge is the 402 response)
      // otherwise paid - result.withReceipt() attaches the receipt to your response
      ```
      
      Full handler example:
      
      ```ts
      const handler = async (req: Request) => {
        const result = await mppx.charge({ amount: '0.01' })(req)
      
        if (result.status === 402) return result.challenge
      
        const response = new Response(JSON.stringify({ data: 'paid content' }))
        return result.withReceipt(response)
      }
      ```
      
      ### Session-Based Billing
      
      ```ts
      const result = await mppx.session({
        amount: '1.00',
        unitType: 'credits',
      })(request)
      ```
      
      ### Composing Methods
      
      Present multiple payment methods in a single 402 response. Accepts handler function refs (0.4.0+), method objects, or `"name/intent"` string keys:
      
      ```ts
      // Handler function refs (preferred, 0.4.0+)
      const result = await mppx.compose(
        mppx.tempo.charge({ amount: '0.01' }),
        mppx.stripe.charge({ amount: '0.01' }),
      )(request)
      if (result.status === 402) return result.challenge
      return result.withReceipt(Response.json({ data: '...' }))
      
      // Tuple syntax also works
      const handler = mppx.compose(
        ['tempo/charge', { amount: '0.01' }],
        ['stripe/charge', { amount: '0.01' }],
      )
      ```
      
      ### Node.js Adapter
      
      ```ts
      import http from 'node:http'
      import { Mppx } from 'mppx/server'
      
      const server = http.createServer(Mppx.toNodeListener(handler))
      ```
      
      ### Manual 402 Response
      
      ```ts
      return Response.requirePayment(challenges)
      ```
      
      ### Credential Lifecycle: validate vs broadcast
      
      Settlement is split into a non-mutating pre-check and a mutating settle, so a server can answer "would this credential work?" without consuming it:
      
      ```ts
      // Non-mutating: does this credential satisfy the challenge?
      const result = await mppx.validateCredential(authorizationHeaderValue)
      
      // Mutating: settle the payment and produce a receipt
      const receipt = await mppx.broadcastCredential(authorizationHeaderValue)
      ```
      
      `mppx.verifyCredential()` is a **deprecated** alias for `broadcastCredential()`: it "maps to the same mutating operation and does not provide validation-only semantics." Use the split pair for anything that needs a safe pre-check endpoint, or that must confirm work succeeded before taking payment.
      
      The same split exists at the method level - see `references/custom-methods.md`.
      
      ## Client SDK (`mppx/client`)
      
      The client module exports `Mppx`, `Fetch`, `Transport`, `Expires`, and `Constants`.
      
      ### Entry Points
      
      ```ts
      import { Fetch, Mppx } from 'mppx/client'
      
      // Standalone payment-aware fetch - no global mutation
      const paidFetch = Fetch.from({ methods: [tempo({ account })] })
      const res = await paidFetch('https://api.example.com/data')
      
      // Explicit global install / uninstall
      Fetch.polyfill({ methods: [tempo({ account })] })
      Fetch.restore()
      
      // Undo an instance's polyfill
      const mppx = Mppx.create({ methods: [tempo({ account })] })
      Mppx.restore()
      ```
      
      Prefer `Fetch.from()` in libraries and tests: patching `globalThis.fetch` in a shared process affects every caller, including ones that should not be paying.
      
      ### With Polyfill (Default)
      
      ```ts
      import { Mppx, tempo } from 'mppx/client'
      
      Mppx.create({
        methods: [tempo()],
        polyfill: true,  // default - wraps globalThis.fetch
      })
      
      // All fetch calls now handle 402 automatically
      const res = await fetch('https://api.example.com/data')
      console.log(await res.json()) // paid content, no manual 402 handling
      ```
      
      ### Without Polyfill
      
      ```ts
      const mppx = Mppx.create({
        methods: [tempo()],
        polyfill: false,
      })
      
      const res = await mppx.fetch('https://api.example.com/data')
      ```
      
      ### Manual Credential Creation
      
      ```ts
      const credential = await mppx.createCredential(response402, context?)
      ```
      
      ### Per-Request Accounts
      
      ```ts
      const res = await mppx.fetch(url, {
        context: { account: specificAccount },
      })
      ```
      
      ### Options
      
      - `methods` - payment method handlers
      - `fetch` - custom fetch implementation (optional)
      - `polyfill` - wrap `globalThis.fetch`, defaults to `true`
      - `transport` - protocol transport (optional)
      - `onChallenge` - callback when a 402 challenge is received (optional)
      - `acceptPaymentPolicy` - controls when the `Accept-Payment` header is injected on outgoing requests: `'always'`, `'same-origin'`, `'never'`, or `{ origins: string[] }` (supports `*.` wildcards)
      - `maxPaymentRetries` - maximum payment challenge retries after the initial response, default `3`. Incremental challenges (a server re-issuing a 402 with adjusted requirements) consume retries
      
      **Breaking change (mppx 0.6.0):** polyfilled `fetch` in browsers no longer sends `Accept-Payment` on every request - it now defaults to **same-origin** only. Non-browser environments are unaffected. Use `acceptPaymentPolicy` to opt cross-origin payment endpoints back in.
      
      ### Node SQLite Channel Store (`mppx/client/node`)
      
      Persist session channels across processes on Node without standing up Redis:
      
      ```ts
      import { createSqliteChannelStore, defaultChannelDatabasePath } from 'mppx/client/node'
      
      const channelStore = createSqliteChannelStore({ path: defaultChannelDatabasePath() })
      Mppx.create({ methods: [tempo.session({ account, maxDeposit: '1', channelStore })] })
      ```
      
      `defaultChannelDatabasePath()` points at Tempo Wallet's own `channels.db`, and the store can read existing wallet-cli v2 session rows - so a CLI-opened channel is reusable from your own process, and vice versa.
      
      ## Framework Middleware
      
      ### Hono
      
      ```ts
      import { Hono } from 'hono'
      import { Mppx, tempo } from 'mppx/hono'
      
      const mppx = Mppx.create({
        methods: [tempo()],
        secretKey: process.env.MPP_SECRET_KEY,
      })
      
      const app = new Hono()
      
      app.get('/paid', mppx.charge({ amount: '0.01' }), (c) => {
        return c.json({ data: 'paid content' })
      })
      ```
      
      ### Express
      
      ```ts
      import express from 'express'
      import { Mppx, tempo } from 'mppx/express'
      
      const mppx = Mppx.create({
        methods: [tempo()],
        secretKey: process.env.MPP_SECRET_KEY,
      })
      
      const app = express()
      
      app.get('/paid', mppx.charge({ amount: '0.01' }), (req, res) => {
        res.json({ data: 'paid content' })
      })
      ```
      
      ### Next.js
      
      ```ts
      import { Mppx, tempo } from 'mppx/nextjs'
      
      const mppx = Mppx.create({
        methods: [tempo()],
        secretKey: process.env.MPP_SECRET_KEY,
      })
      
      export const GET = mppx.charge({ amount: '0.01' })(async (req) => {
        return Response.json({ data: 'paid content' })
      })
      ```
      
      ### Elysia
      
      ```ts
      import { Elysia } from 'elysia'
      import { Mppx, tempo } from 'mppx/elysia'
      
      const mppx = Mppx.create({
        methods: [tempo()],
        secretKey: process.env.MPP_SECRET_KEY,
      })
      
      const app = new Elysia()
        .guard({ beforeHandle: mppx.charge({ amount: '0.01' }) })
        .get('/paid', () => ({ data: 'paid content' }))
      ```
      
      ## Proxy and Discovery
      
      The payments proxy (`mppx/proxy`) and discovery documents (`mppx/discovery`) have their own reference: `references/discovery-and-proxy.md`. Two things worth flagging here because they are easy to get wrong:
      
      - Free proxy routes are declared with the literal value `true` (`'GET /v1/models': true`). There is **no** `mppx.free()` helper.
      - The proxy exposes both `proxy.fetch` (Fetch API runtimes) and `proxy.listener` (Node `http`).
      
      ## MCP SDK
      
      ### Server - Wrapping an MCP Server
      
      ```ts
      import { McpServer } from 'mppx/mcp/server'
      import { Server } from '@modelcontextprotocol/sdk/server/index.js'
      
      const baseServer = new Server({ name: 'my-mcp', version: '1.0.0' })
      
      const server = McpServer.wrap(baseServer, {
        methods: [tempo()],
        secretKey: process.env.MPP_SECRET_KEY,
      })
      ```
      
      Payment errors use MCP error code `-32042`.
      
      ### Client - Wrapping an MCP Client
      
      ```ts
      import { McpClient } from 'mppx/mcp/client'
      import { Client } from '@modelcontextprotocol/sdk/client/index.js'
      
      const baseClient = new Client({ name: 'my-client', version: '1.0.0' })
      
      const client = McpClient.wrap(baseClient, {
        methods: [tempo()],
      })
      ```
      
      The MCP subpaths moved to `mppx/mcp/server` and `mppx/mcp/client` in mppx 0.8.0; the `mppx/mcp-sdk/*` specifiers remain as aliases. `McpClient.wrap` is now the single client-wrap API - the in-place `wrapClient` variant was collapsed into it. MCP-over-HTTP challenges settle in the same payment-aware fetch: `Transport.http()` extracts JSON-RPC `-32042` challenges and retries with the credential in MCP metadata, so a single client can pay both HTTP `402`s and MCP-over-HTTP challenges.
      
      ## Transports
      
      Transports are pluggable on both client and server, exported as a `Transport` namespace from `mppx/client` and `mppx/server`:
      
      | Export | Purpose |
      |---|---|
      | `Transport.http()` | HTTP header encoding; also extracts MCP-over-HTTP `-32042` challenges |
      | `Transport.mcp()` | Raw JSON-RPC message handling |
      | `Transport.mcpSdk()` | For use with `@modelcontextprotocol/sdk` |
      | `Transport.from()` | Build a custom transport |
      
      Use `Transport.mcp()` when handling raw JSON-RPC messages directly; with the official MCP SDK, use `Transport.mcpSdk()` instead.
      
      ## CLI
      
      The CLI has its own reference: `references/cli.md`. Note that `mppx/cli` (config via `defineConfig`) and `mppx/cli/plugins` (plugin authoring) are module exports, not a `mppx plugins` command - no such command exists.
      
      ## Core Primitives
      
      ### Challenge
      
      ```ts
      import { Challenge } from 'mppx'
      
      const challenge = Challenge.from({
        amount: '0.01',
        recipient: '0x...',
        // ...
      })
      
      const serialized = Challenge.serialize(challenge)    // string
      const parsed = Challenge.deserialize(serialized)     // Challenge
      const fromRes = Challenge.fromResponse(response)     // Challenge from 402 response
      const valid = Challenge.verify(challenge, secretKey)  // boolean
      ```
      
      ### Credential
      
      ```ts
      import { Credential } from 'mppx'
      
      const credential = Credential.from({ /* ... */ })
      const serialized = Credential.serialize(credential)
      const parsed = Credential.deserialize(serialized)
      const fromReq = Credential.fromRequest(request)      // extract from incoming request
      ```
      
      ### Receipt
      
      ```ts
      import { Receipt } from 'mppx'
      
      const receipt = Receipt.from({ /* ... */ })
      const serialized = Receipt.serialize(receipt)
      const fromRes = Receipt.fromResponse(response)
      ```
      
      ### Expires Helpers
      
      ```ts
      import { Expires } from 'mppx'
      
      const fiveMin = Expires.minutes(5)
      const twoHours = Expires.hours(2)
      ```
      
      ## Error Classes
      
      **17** error classes extend `PaymentError` and expose `.toProblemDetails()` for RFC 9457 responses.
      
      **General errors:**
      - `MalformedCredentialError` - credential cannot be parsed
      - `InvalidChallengeError` - challenge is invalid or tampered
      - `VerificationFailedError` - signature or HMAC verification failed
      - `PaymentRequiredError` - payment is required (402)
      - `PaymentActionRequiredError` - the payer must take an action before payment can complete
      - `PaymentExpiredError` - challenge or credential has expired
      - `PaymentInsufficientError` - payment amount too low
      - `PaymentMethodUnsupportedError` - method not accepted by server
      - `InvalidPayloadError` - credential payload fails its schema
      - `BadRequestError` - malformed request
      
      **Session-specific errors:**
      - `InsufficientBalanceError` - session channel balance too low
      - `InvalidSignatureError` - session state signature invalid
      - `SignerMismatchError` - voucher signed by an unexpected signer
      - `AmountExceedsDepositError` - cumulative voucher exceeds the channel deposit
      - `DeltaTooSmallError` - voucher increment below the minimum
      - `ChannelNotFoundError` - session channel does not exist
      - `ChannelClosedError` - session channel has been closed
      
      ```ts
      try {
        const result = await mppx.charge({ amount: '0.01' })(request)
      } catch (e) {
        if (e instanceof PaymentExpiredError) {
          console.log(e.toProblemDetails())
          // { type: '...', title: 'Payment Expired', status: 402, detail: '...' }
        }
      }
      ```
      
      ### Structured Error Metadata
      
      `PaymentError` carries a `details` record - "safe method-specific context for diagnostics and relay responses" - alongside the human-facing `hint`. Both surface in the problem document. This is the practical way to see *why* a payment failed, since mppx otherwise re-emits internal failures as a generic 402 with a fresh challenge.
      
      ## Store Interface
      
      For session channel state persistence. All built-in adapters handle BigInt serialization via `ox`'s `Json` module.
      
      ```ts
      import { Store } from 'mppx/server'
      
      // In-memory (development only)
      const store = Store.memory()
      
      // Redis / ioredis / Valkey (added in 0.4.9)
      const store = Store.redis(redisClient) // client needs: get, set, del
      
      // Cloudflare KV
      const store = Store.cloudflare(env.MY_KV_NAMESPACE)
      
      // Upstash Redis / Vercel KV
      const store = Store.upstash(upstashClient)
      
      // Custom adapter
      const store = Store.from({
        get: async (key) => { /* ... */ },
        put: async (key, value) => { /* ... */ },
        delete: async (key) => { /* ... */ },
      })
      
      // Pass to session method config
      tempo.session({ currency, recipient, store, sse: { poll: true } })
      ```
      
      Client-side, pass a `channelStore` to the Tempo **session client** (`tempo.session` / `tempo.session.manager`) to persist and reuse payer session channels across processes (mppx 0.8.0) - distinct from the server-side `store` above. The client-side `authorizedSigner` override was removed in 0.8.0; voucher authority derives from the selected account.
      
      ## AtomicStore
      
      `AtomicStore` extends `Store` with a safe `update(key, fn)` method for concurrent read-modify-write operations (0.5.7+):
      
      ```ts
      import { Store, type AtomicStore } from 'mppx/server'
      
      // All built-in adapters support atomic updates
      const store: AtomicStore<MyItemMap> = Store.redis(redisClient)
      
      // Atomic read-modify-write
      const result = await store.update('channel:0x123', (current) => {
        if (!current) return { value: initialState, result: 'created' }
        return { value: { ...current, settled: current.settled + amount }, result: 'updated' }
      })
      ```
      
      The type system uses a two-slot generic pattern:
      - `Store<itemMap, extended>` - base store with optional extension slot
      - `AtomicStore<itemMap>` = `Store<itemMap, AtomicActions<itemMap>>` - store with `update()` filled in
      - Custom adapters via `Store.from()` get an optimistic-retry `update()` implementation automatically
      - Native adapters (redis, upstash, cloudflare) use their built-in atomic primitives
      
      Use cases: replay protection (atomic deduplication of proof credentials), channel state updates in distributed deployments, SSE session state management.
      
      ## Privy Server Wallets
      
      Use [Privy](https://docs.privy.io) server-managed wallets as MPP signers for agentic payment flows. Install: `npm install @privy-io/node mppx viem`.
      
      ### Recommended: `createViemAccount`
      
      `@privy-io/node` version `0.20.0` or later ships a helper that builds the viem `Account` for you. It "delegates signatures to the Privy wallet, so it replaces any local viem account" - including the Tempo custom-serializer handling that previously had to be written by hand:
      
      ```ts
      import { PrivyClient } from '@privy-io/node'
      import { createViemAccount } from '@privy-io/node/viem'
      import { Mppx, tempo } from 'mppx/client'
      
      const privy = new PrivyClient({
        appId: process.env.PRIVY_APP_ID!,
        appSecret: process.env.PRIVY_APP_SECRET!,
      })
      
      const wallet = await privy.wallets().create({ chain_type: 'ethereum' })
      const account = createViemAccount(privy, { walletId: wallet.id, address: wallet.address })
      
      const mppx = Mppx.create({ polyfill: false, methods: [tempo({ account })] })
      const response = await mppx.fetch('https://api.example.com/paid')
      ```
      
      **Wallet ownership matters.** Server-side signing works with **app-owned server wallets**. User-owned embedded wallets cannot be signed for from a server without authorization keys or key quorums, and attempts fail with a 401 about missing authorization or user signing keys. Provision a server wallet for agent payment flows rather than reaching for a user's embedded wallet.
      
      ### Manual construction (background)
      
      Before `createViemAccount`, the account was assembled by hand with `toAccount()`. It is still useful to understand what the helper does, and necessary if you are on an older `@privy-io/node`:
      
      ```ts
      import { PrivyClient } from '@privy-io/node'
      import { Mppx, tempo } from 'mppx/client'
      import { toAccount } from 'viem/accounts'
      import { keccak256 } from 'viem'
      
      function createPrivyAccount(walletId: string, address: `0x${string}`) {
        return toAccount({
          address,
      
          async signMessage({ message }) {
            const result = await privy.wallets().ethereum().signMessage(walletId, {
              message: typeof message === 'string' ? message : message.raw,
            })
            return result.signature as `0x${string}`
          },
      
          async signTransaction(transaction, options) {
            // Tempo uses a custom serializer - must use raw signSecp256k1
            const serializer = options?.serializer
            if (!serializer) throw new Error('Tempo serializer required')
            const unsignedSerialized = await serializer(transaction)
            const hash = keccak256(unsignedSerialized)
            const { signature } = await privy
              .wallets()
              .ethereum()
              .signSecp256k1(walletId, { params: { hash } })
            const { SignatureEnvelope } = await import('ox/tempo')
            return (await serializer(
              transaction,
              SignatureEnvelope.from(signature) as any,
            )) as `0x${string}`
          },
      
          async signTypedData(typedData) {
            const result = await privy
              .wallets()
              .ethereum()
              .signTypedData(walletId, { params: typedData as any })
            return result.signature as `0x${string}`
          },
        })
      }
      
      const account = createPrivyAccount(wallet.id, wallet.address as `0x${string}`)
      ```
      
      **Key details:**
      - `signTransaction` uses `signSecp256k1` (raw hash signing) because Tempo has a custom serialization format (type `0x76`). Privy's higher-level `signTransaction` doesn't support custom serializers.
      - `signMessage` maps directly to Privy's `signMessage` for EIP-191 personal signatures
      - `signTypedData` maps directly for EIP-712 typed data (used by zero-dollar auth proofs). As of mppx 0.8.0 the Tempo zero-amount `Proof` typed-data includes an `account` field bound to the payer wallet and its domain version is `3` (exposed as `tempo.Proof`) - a proof signed for one account no longer verifies against another
      - For Tempo testnet (Moderato, chain 42431): use `tempo({ account, testnet: true })` in `Mppx.create`
      
      See the [Privy MPP demo](https://github.com/privy-io/examples/tree/main/privy-next-mpp-agent-demo) for a full Next.js reference implementation including wallet creation, funding from a treasury, and executing paid API calls.
      
      ## Zod Validators
      
      Schema validators for MPP-specific types:
      
      ```ts
      import { z } from 'mppx'
      
      const schema = z.object({
        price: z.amount(),       // valid payment amount string
        due: z.datetime(),       // ISO 8601 datetime
        wallet: z.address(),     // EVM address (0x...)
        txHash: z.hash(),        // transaction hash
        sig: z.signature(),      // cryptographic signature
        billing: z.period(),     // billing period
      })
      ```
      
      Conversion and input helpers also ship from the same namespace: `z.datetimeInput()` (accepts the several datetime input shapes mppx tolerates), `z.toDate()`, `z.toDatetimeString()`, and `z.unwrapOptional()`.
      
      ## Payment Hooks
      
      Register on the object returned by `Mppx.create()`. Each registration returns an unsubscribe function.
      
      ```ts
      // Server (mppx/server)
      const payment = Mppx.create({ methods: [tempo.charge(), tempo.session()] })
      payment.onChallengeCreated(({ challenge, method, request }) => {})
      payment.onPaymentSuccess(({ method, receipt, request }) => {})
      payment.onPaymentFailed(({ error, method, submittedChallenge }) => {})
      payment.onSessionSettlement(({ trigger, txHash, channelId, cumulative, delta }) => {})
      payment.on('*', ({ name, payload }) => {})
      
      // Client (mppx/client)
      const mppx = Mppx.create({ methods: [tempo.charge({ account })], polyfill: false })
      mppx.onChallengeReceived(({ challenge }) => { /* return a credential string to override */ })
      mppx.onCredentialCreated(({ challenge }) => {})
      mppx.onPaymentResponse(({ challenge, response }) => {})
      mppx.onPaymentFailed(({ challenge, error }) => {})
      ```
      
      Server handlers are awaited inline and sequentially on the request path, so slow handlers delay the response. Every hook except client `onChallengeReceived` is a pure observer: thrown errors are swallowed and never change payment handling. Filter by intent with `method.intent` (server) or `challenge.intent` (client) - `'charge'`, `'session'`, or `'subscription'`.
      
      `onSessionSettlement` reports on-chain session settlement with chain-agnostic context: the `trigger` that caused it (scheduled vs explicit), the transaction hash, the channel ID, and the cumulative and incremental amounts. It is the hook to wire up if you need a settlement ledger. See [mpp.dev/advanced/payment-hooks](https://mpp.dev/advanced/payment-hooks).
      
  • CHANGELOG.md 13.5 KB
    # Changelog
    
    All notable changes to this skill will be documented in this file.
    
    The format is based on [Keep a Changelog](https://keepachangelog.com/en/2.0.0/),
    and this skill adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
    
    ## [Unreleased]
    
    ## [0.10.2] - 2026-09-09
    
    ### Changed
    - Description condensed to fit the repo's 250-character limit.
    
    ## [0.10.1] - 2026-08-21
    
    ### Changed
    
    - Declared ClawHub browse categories (`finance, development`) and topics in `metadata`, so the release pipeline publishes them instead of leaving the skill in the `other` category.
    
    ### Removed
    
    - `skill-card.md`. The ClawHub CLI strips a root `skill-card.md` from every publish and the registry generates its own card, so the authored file never reached ClawHub.
    
    ## [0.10.0] - 2026-08-07
    
    ### Changed
    
    - Condensed SKILL.md from ~30.6k to ~23.6k chars, removing only duplication: dropped the "When to Use" section, the ASCII sequence diagram, the Tempo token-address table (placeholders now explained in one line), the `Fetch.*` / proxy-endpoint / subpath-export / `Html.init` enumerations, and the `Proxy.create` boilerplate - each fact stays in the matching reference file. Stellar `channel` and NEAR Intents caveats moved into the payment-methods table; the settlement obligation now lives only in Production Gotchas.
    - Trimmed the frontmatter description to WHAT + WHEN (no trigger-keyword dump) and `metadata.openclaw.envVars` to the six genuine MPP variables (CLI-only, alias, Privy, and upstream provider keys are documented in the references instead).
    - Added inline pointers to `references/stripe-method.md`, `references/lightning-method.md`, and `references/subscriptions.md` from the payment-methods and intents tables.
    
    ### Fixed
    
    - `references/sessions.md`: added the missing `Store.redis()` row to the Store Backends table (the file's own WebSocket example uses it), and moved the general Store Backends section out of the "Escrow Contracts (Sessions v1)" block so v1-only content is properly scoped.
    - `references/typescript-sdk.md`: the `mppx/html` row now documents the `Html.init(methodName)` page context fields.
    
    ## [0.9.0] - 2026-07-30
    
    ### Fixed
    
    - Removed three APIs that do not exist in mppx: `mppx.free()` (free proxy routes are the literal value `true`), the `--inspect` CLI flag (use `mppx sign --dry-run`), and `mppx plugins add` (use `mppx skills add` / `mppx mcp add`).
    - Stellar is charge **and** `channel`, not charge-only - the channel intent was never removed; it ships in `@stellar/mpp` with its wire spec still being drafted.
    - `@redotpay/mpp` is published on npm (0.1.2); dropped the "not yet on public npm" caveat.
    - Proof Credentials are in TypeScript, Rust, and Ruby but **not** pympp, whose Tempo method implements only `hash` and `transaction` payload types. Upstream's two capability matrices disagree, so the claim is now sourced from SDK code.
    - Error classes: 14 -> 17. viem peer range `>=2.51.0` -> `>=2.54.0`. Rust feature table gained `sqlite` and the three TLS features.
    - Corrected dead and moved links: `docs.tempo.finance` -> `docs.tempo.xyz`, the Stripe deposit-mode guide, and `/guides/upgrade-x402` -> `/guides/use-mpp-with-x402`. The docs MCP server exposes 8 tools, not 4.
    - Stripe crypto PaymentIntents require API version `2026-03-25.preview` (was `2026-03-04.preview`).
    - Access-key spend limits must be hex-encoded: `numberToHex(parseUnits(...))`, not a raw bigint.
    - Replaced the hand-rolled Privy `toAccount()` + `signSecp256k1` recipe with `createViemAccount` from `@privy-io/node/viem` (requires `@privy-io/node` >= 0.20.0), keeping the manual construction as background.
    
    ### Added
    
    - **Session settlement**: `settlementSchedule` (server-owned, triggered by units, amount, or interval), manual `tempo.settle()` / `tempo.settleBatch()`, and the `onSessionSettlement` hook. Without one of these a session server accumulates vouchers it never redeems on-chain, and channels stay open holding payer deposits.
    - Server-side `bootstrap: true` same-route channel recovery; client `topUpAmount` top-up batching; `maxPaymentRetries` (default 3); payment-aware session SSE on client `fetch` responses.
    - New `references/cli.md`: `mppx validate` end-to-end conformance checking, plus `init`, `sign`, `sessions`, `discover`, `services`, `mcp`, `skills`, `-M`, `--format`, and the `MPPX_*` env vars. The `mppx/validation` export runs the same checks programmatically.
    - `validate`/`broadcast` credential lifecycle (`mppx.validateCredential()` vs `mppx.broadcastCredential()`), and the matching `Method.toServer` split.
    - Tempo API relay via `tempo.charge({ relay })` and the two-hook relay contract.
    - NEAR Intents payment method, including its non-trustless settlement disclosure via `methodDetails.settlementBackend`.
    - New subpath exports `mppx/client/node` (SQLite session channel store sharing Tempo Wallet's database) and `mppx/validation`.
    - `Fetch.from/polyfill/restore`, `Mppx.restore`, the `Transport.from/http/mcp/mcpSdk` namespace, `Html.init`, `BodyDigest`, `PaymentRequest`, `Challenge.meta`, and `Credential.extractPaymentScheme`.
    - New `references/discovery-and-proxy.md` (proxy `Service.from`/`custom`, `rewriteRequest`, `docsLlmsUrl`, `{ pay, options }` endpoints, AI-user-agent markdown negotiation, the `discovery()` helper, `x-service-info`) and `references/subscriptions.md` (renewal worker, `409` + `Retry-After: 1` concurrency contract, cancellation vs revocation).
    - New `references/production-gotchas.md` collecting the field-tested failure modes, including payment-timing (charge settles before the handler runs; 5-minute default challenge expiry), realm resolution precedence, settlement obligations, and mppscan attribution behaviour.
    - Celo and Celo Sepolia EVM chains/assets; sponsored-charge budget caps `maxInFlightReservations` / `maxInFlightTotalFee`; extra Zod helpers (`datetimeInput`, `toDate`, `toDatetimeString`, `unwrapOptional`).
    - Rust 0.11.0: TIP-1034 session client primitives, `ChargeMethod::with_validate_sender`, `tempo_simulateV1` sponsored dry-run, `TempoProvider::with_expected_chain_id`.
    
    ### Changed
    
    - **Breaking:** `SKILL.md` restructured - Production Gotchas, the CLI, proxy/discovery, and subscriptions moved into `references/` to fit the repo's 500-line cap.
    - `tempo.sessionLegacy` is formally `@deprecated`; `voucherSigner` now exists only on the legacy v1 path; the escrow-contract section is labelled Sessions v1, with v2 on the fixed TIP-20 channel precompile.
    - `Method.toServer({ verify })` and `mppx.verifyCredential()` are deprecated in favour of the split `validate` + `broadcast` pair.
    - Subscription period units: the mppx type is `'dev_second' | 'day' | 'week'` (no `month`) while the published schema says day/week/month - divergence flagged rather than silently picking one.
    - x402 interop is configured on the method as `evm.charge({ x402: { facilitator } })`; client negotiation now prefers Payment-auth challenges over x402.
    - `PaymentError` gained a structured `details` record, the practical way to get diagnostics out of an otherwise opaque 402.
    - Session receipts documented as cumulative (`acceptedCumulative`, `spent`, `units`), so per-call amounts must be derived as deltas.
    
    ### Security
    
    - SPT proxy endpoints must derive amount, currency, expiry, and limits server-side; forwarding client-supplied parameters delegates payment authorization to an untrusted client.
    - Sponsorship rejects non-canonical fee-payer calldata and client-supplied access lists; session voucher replay checks hardened for settled vouchers and mismatched credential sources; pympp 0.9.1 rejects ABI calldata with trailing padding; the Rust SDK rejects oversized `WWW-Authenticate` `request` parameters before decoding.
    
    Verified against: mppx@0.8.15, pympp@0.9.1, mpp@0.11.0, @stellar/mpp@0.7.1
    
    ## [0.8.3] - 2026-07-22
    
    ### Added
    
    - skill-card.md release record following NVIDIA's skill-card format
    
    ### Changed
    
    - metadata.openclaw audited against the official ClawHub spec
    
    ## [0.8.2] - 2026-07-10
    
    ### Changed
    - CHANGELOG preamble pinned to Keep a Changelog 2.0.0 (format unchanged; KaC 2.0.0 keeps existing changelogs valid).
    
    ## [0.8.1] - 2026-07-01
    
    ### Added
    - Request-handling gotcha: `close`/`topUp` management credentials are bodyless, so a custom request-body validator running before `mppx.session()` rejects them with a spurious 400 (mppx answers `204`). Exempt session-management credentials - gate on credential intent/action, not body presence.
    
    ## [0.8.0] - 2026-07-01
    
    ### Changed
    - Sessions overhaul: `tempo.session()` is now the TIP-1034 precompile flow (Sessions v2); the escrow-contract flow the skill documented is now `tempo.sessionLegacy` (Sessions v1). Two client APIs: `tempo.session()` (Mppx registration) vs `tempo.session.manager()` (direct lifecycle control - `.sse()`/`.close()`). Documented the v1<->v2 interop cliff.
    - MCP subpaths moved to `mppx/mcp/client` and `mppx/mcp/server` (`mppx/mcp-sdk/*` retained as aliases); `McpClient.wrap` unified. MCP-over-HTTP `-32042` challenges now settle in the payment-aware fetch (`Transport.http()`).
    - SDK table: Go is now the official `mpp-go` (net/http, Gin, Echo, Chi); added community Swift `mpp-swift`; Python and Ruby now have MCP, Stripe, and event handling. Corrected parity sentence (session still TS/Rust only; Go has no Stripe/MCP/events).
    - Solana now supports charge + session (Token-2022), not charge-only.
    - Stellar (`@stellar/mpp` 0.7.0): removed the non-functional channel `open` action - Stellar is charge-only; added payer-bound `signedHash` push, `allowUnsignedPush` opt-in, `credentialTypes` advertisement.
    - Refunds reframed against v2 sessions (unclaimed reserved funds refunded by default).
    - CLI no longer auto-discovers config from local directories (mppx 0.8.1) - pass config explicitly.
    - Peer dep: `hono` >= 4.12.25 (was 4.12.18). Subpath-export labels bumped to mppx 0.8.1.
    
    ### Added
    - Payment Hooks section: server (`onChallengeCreated`/`onPaymentSuccess`/`onPaymentFailed`/`on('*')`) and client (`onChallengeReceived`/`onCredentialCreated`/`onPaymentResponse`/`onPaymentFailed`) lifecycle observability.
    - Managing Agent Spend section: Tempo access keys (delegated signing keys with token limits, contract/function/recipient scopes, expiry) via `provider.getMppxParameters({ accessKey })`.
    - Client chain pinning (`tempo.charge({ expectedChainId })`); `tempo.common()` charge+session bundle alias; pluggable client `channelStore` (client `authorizedSigner` override removed); proxy `anthropic()` service preset; split-payment limits (1-10, per-split memos, `expectedRecipients`).
    - Services MCP discovery server (`mpp.dev/mcp/services`), docs MCP (`mpp.dev/api/mcp`), and `npx skills add tempoxyz/mpp -g` install path; IETF `draft-ryan-httpauth-payment-01`; docs monorepo `tempoxyz/mpp`.
    - pympp 0.9.0: credential `source` validation + `validate_sender` callback on `ChargeIntent`; sponsored charges dry-run via `tempo_simulateV1` before broadcast; Python MCP support.
    - Production gotchas (advisory): mppx/viem version coupling (arity-crash), `mppx.fetch` probe masks upstream 5xx; RedotPay `@redotpay/mpp` not-yet-on-npm caveat.
    
    ### Security
    - Zero-amount proof credentials bound to the payer wallet: EIP-712 `Proof` gains an `account` field, domain version bumped to 3 (`tempo.Proof`).
    
    Verified against: mppx@0.8.1, pympp@0.9.0, @stellar/mpp@0.7.0
    
    ## [0.7.0] - 2026-06-04
    
    ### Fixed
    - Replaced the non-existent `@anthropic-ai/mpp` package name with canonical `mppx` in tempo-method.md (mapped to the correct `mppx/server` / `mppx/client` subpaths, since `mppx/tempo` exports only `Session`).
    - Corrected the proxy discovery claim: `/discover`, `/discover/all`, `/llms.txt` are all active - none return 410.
    - Stripe `networkId` is a Stripe **profile** (`profile_...`) ID, not a Business Network `acct_` ID; SPT creation now uses the `@stripe/link-cli` spend-request flow, not a `shared_payment/granted_tokens` rawRequest.
    - `method-unsupported` returns HTTP 400, not 402.
    - pympp server/client examples updated to the current `@server.pay(...)` decorator API and `TempoAccount.from_key()`; removed the unsupported Python `StreamMethod`/`PaymentTransport` session flow (session intent is TypeScript/Rust only).
    - typescript-sdk.md charge result uses numeric `=== 402` to match the rest of the skill.
    
    ### Added
    - EVM (built-in `mppx/evm`, EIP-3009/x402-exact), Solana (`@solana/mpp`), Monad (`@monad-crypto/mpp`), and RedotPay (`@redotpay/mpp`) payment methods; noted Stellar's `channel` intent.
    - Subscription intent (`mppx.tempo.subscription`): activation, access reuse, background renewal, cancellation, `dev_second` periods.
    - New `mppx` subpaths: `mppx/evm(/client//server)`, `mppx/x402`, `mppx/cli(/plugins)`, `mppx/stripe/client//server`.
    - Refunds concept (charge: out-of-protocol send-back; session: via channel close).
    - Discovery documents (`x-payment-info.offers[]` OpenAPI) and registries (MPPScan, MPP Services directory).
    - Ruby (`mpp-rb`, official by Stripe) and Elixir (`mpp`, community) SDK rows.
    - Stripe on-chain crypto deposit method (API `2026-03-04.preview`) alongside fiat SPT.
    - `opaque` challenge param semantics; MCP `-32043` error code; `acceptPaymentPolicy` option.
    - Production gotchas: "fund with stablecoin fee token, not ETH" on Tempo; `MPP_SECRET_KEY` rotation with overlap; reverse-proxy `http`/`https` scheme mismatch.
    
    ### Changed
    - Rust `mpp` 0.1 -> 0.10; expanded feature flags (`tower`, `axum`, `ws`, `stripe`, `utils`).
    - mppx peer deps: `viem >= 2.51.0`, `hono >= 4.12.18`.
    - Tempo session SDK option `authorizedSigner` -> `voucherSigner` (mppx 0.6.29).
    - IETF draft now Standards Track (was Experimental).
    - Established `metadata.upstream` tracking.
    
    Verified against: mppx@0.6.30, pympp@0.8.2, mpp@0.10.4, @buildonspark/lightning-mpp-sdk@0.1.4, @stellar/mpp@0.6.0, mpp-card@0.1.8
    
  • LICENSE.txt 8.9 KB
    Apache License
    Version 2.0, January 2004
    https://www.apache.org/licenses/
    
    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
    
    1. Definitions.
    
    "License" shall mean the terms and conditions for use, reproduction, and
    distribution as defined by Sections 1 through 9 of this document.
    
    "Licensor" shall mean the copyright owner or entity authorized by the
    copyright owner that is granting the License.
    
    "Legal Entity" shall mean the union of the acting entity and all other
    entities that control, are controlled by, or are under common control with
    that entity. For the purposes of this definition, "control" means (i) the
    power, direct or indirect, to cause the direction or management of such
    entity, whether by contract or otherwise, or (ii) ownership of fifty percent
    (50%) or more of the outstanding shares, or (iii) beneficial ownership of
    such entity.
    
    "You" (or "Your") shall mean an individual or Legal Entity exercising
    permissions granted by this License.
    
    "Source" form shall mean the preferred form for making modifications,
    including but not limited to software source code, documentation source, and
    configuration files.
    
    "Object" form shall mean any form resulting from mechanical transformation or
    translation of a Source form, including but not limited to compiled object
    code, generated documentation, and conversions to other media types.
    
    "Work" shall mean the work of authorship, whether in Source or Object form,
    made available under the License, as indicated by a copyright notice that is
    included in or attached to the work (an example is provided in the Appendix
    below).
    
    "Derivative Works" shall mean any work, whether in Source or Object form,
    that is based on (or derived from) the Work and for which the editorial
    revisions, annotations, elaborations, or other modifications represent, as a
    whole, an original work of authorship. For the purposes of this License,
    Derivative Works shall not include works that remain separable from, or
    merely link (or bind by name) to the interfaces of, the Work and Derivative
    Works thereof.
    
    "Contribution" shall mean any work of authorship, including the original
    version of the Work and any modifications or additions to that Work or
    Derivative Works thereof, that is intentionally submitted to Licensor for
    inclusion in the Work by the copyright owner or by an individual or Legal
    Entity authorized to submit on behalf of the copyright owner. For the
    purposes of this definition, "submitted" means any form of electronic, verbal,
    or written communication sent to the Licensor or its representatives,
    including but not limited to communication on electronic mailing lists, source
    code control systems, and issue tracking systems that are managed by, or on
    behalf of, the Licensor for the purpose of discussing and improving the Work,
    but excluding communication that is conspicuously marked or otherwise
    designated in writing by the copyright owner as "Not a Contribution."
    
    "Contributor" shall mean Licensor and any individual or Legal Entity on
    behalf of whom a Contribution has been received by Licensor and subsequently
    incorporated within the Work.
    
    2. Grant of Copyright License. Subject to the terms and conditions of this
    License, each Contributor hereby grants to You a perpetual, worldwide,
    non-exclusive, no-charge, royalty-free, irrevocable copyright license to
    reproduce, prepare Derivative Works of, publicly display, publicly perform,
    sublicense, and distribute the Work and such Derivative Works in Source or
    Object form.
    
    3. Grant of Patent License. Subject to the terms and conditions of this
    License, each Contributor hereby grants to You a perpetual, worldwide,
    non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this
    section) patent license to make, have made, use, offer to sell, sell, import,
    and otherwise transfer the Work, where such license applies only to those
    patent claims licensable by such Contributor that are necessarily infringed by
    their Contribution(s) alone or by combination of their Contribution(s) with
    the Work to which such Contribution(s) was submitted. If You institute patent
    litigation against any entity (including a cross-claim or counterclaim in a
    lawsuit) alleging that the Work or a Contribution incorporated within the Work
    constitutes direct or contributory patent infringement, then any patent
    licenses granted to You under this License for that Work shall terminate as of
    the date such litigation is filed.
    
    4. Redistribution. You may reproduce and distribute copies of the Work or
    Derivative Works thereof in any medium, with or without modifications, and in
    Source or Object form, provided that You meet the following conditions:
    
    (a) You must give any other recipients of the Work or Derivative Works a copy
    of this License; and
    
    (b) You must cause any modified files to carry prominent notices stating that
    You changed the files; and
    
    (c) You must retain, in the Source form of any Derivative Works that You
    distribute, all copyright, patent, trademark, and attribution notices from
    the Source form of the Work, excluding those notices that do not pertain to
    any part of the Derivative Works; and
    
    (d) If the Work includes a "NOTICE" text file as part of its distribution,
    then any Derivative Works that You distribute must include a readable copy of
    the attribution notices contained within such NOTICE file, excluding those
    notices that do not pertain to any part of the Derivative Works, in at least
    one of the following places: within a NOTICE text file distributed as part of
    the Derivative Works; within the Source form or documentation, if provided
    along with the Derivative Works; or, within a display generated by the
    Derivative Works, if and wherever such third-party notices normally appear.
    The contents of the NOTICE file are for informational purposes only and do not
    modify the License. You may add Your own attribution notices within Derivative
    Works that You distribute, alongside or as an addendum to the NOTICE text from
    the Work, provided that such additional attribution notices cannot be
    construed as modifying the License.
    
    You may add Your own copyright statement to Your modifications and may provide
    additional or different license terms and conditions for use, reproduction, or
    distribution of Your modifications, or for any such Derivative Works as a
    whole, provided Your use, reproduction, and distribution of the Work otherwise
    complies with the conditions stated in this License.
    
    5. Submission of Contributions. Unless You explicitly state otherwise, any
    Contribution intentionally submitted for inclusion in the Work by You to the
    Licensor shall be under the terms and conditions of this License, without any
    additional terms or conditions. Notwithstanding the above, nothing herein
    shall supersede or modify the terms of any separate license agreement you may
    have executed with Licensor regarding such Contributions.
    
    6. Trademarks. This License does not grant permission to use the trade names,
    trademarks, service marks, or product names of the Licensor, except as
    required for reasonable and customary use in describing the origin of the Work
    and reproducing the content of the NOTICE file.
    
    7. Disclaimer of Warranty. Unless required by applicable law or agreed to in
    writing, Licensor provides the Work (and each Contributor provides its
    Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    KIND, either express or implied, including, without limitation, any warranties
    or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
    PARTICULAR PURPOSE. You are solely responsible for determining the
    appropriateness of using or redistributing the Work and assume any risks
    associated with Your exercise of permissions under this License.
    
    8. Limitation of Liability. In no event and under no legal theory, whether in
    tort (including negligence), contract, or otherwise, unless required by
    applicable law (such as deliberate and grossly negligent acts) or agreed to in
    writing, shall any Contributor be liable to You for damages, including any
    direct, indirect, special, incidental, or consequential damages of any
    character arising as a result of this License or out of the use or inability to
    use the Work (including but not limited to damages for loss of goodwill, work
    stoppage, computer failure or malfunction, or any and all other commercial
    damages or losses), even if such Contributor has been advised of the
    possibility of such damages.
    
    9. Accepting Warranty or Additional Liability. While redistributing the Work
    or Derivative Works thereof, You may choose to offer, and charge a fee for,
    acceptance of support, warranty, indemnity, or other liability obligations
    and/or rights consistent with this License. However, in accepting such
    obligations, You may act only on Your own behalf and on Your sole
    responsibility, not on behalf of any other Contributor, and only if You agree
    to indemnify, defend, and hold each Contributor harmless for any liability
    incurred by, or claims asserted against, such Contributor by reason of your
    accepting any such warranty or additional liability.
    
    END OF TERMS AND CONDITIONS
    
  • SKILL.md 23 KB
    ---
    name: mpp
    description: Build with MPP (Machine Payments Protocol), open machine-to-machine payments over HTTP 402. Use for paid APIs, payment-gated endpoints, agent payment flows, MCP tool payments, or metered billing. Covers mppx (TS), pympp, and mpp Rust SDKs.
    metadata:
      version: "0.10.2"
      categories: "finance, development"
      topics: "payments, http-402, stablecoins, machine-payments, apis"
      upstream: "mppx@0.8.15, pympp@0.9.1, mpp@0.11.0, @buildonspark/lightning-mpp-sdk@0.1.4, @stellar/mpp@0.7.1, @solana/mpp@0.7.0, @redotpay/mpp@0.1.2, @defuse-protocol/nearintents-mpp-sdk@0.1.2, mpp-card@0.1.8"
      openclaw:
        homepage: https://github.com/tenequm/skills/tree/main/skills/mpp
        emoji: "💸"
        primaryEnv: MPP_SECRET_KEY
        envVars:
          - name: MNEMONIC
            required: false
            description: BIP-39 mnemonic for client wallet (testnet/regtest only).
          - name: MPP_SECRET_KEY
            required: false
            description: Server-side MPP signing secret (HMAC-binds challenge IDs).
          - name: MPP_REALM
            required: false
            description: Stable realm identifier for mppscan attribution.
          - name: MPPX_RPC_URL
            required: false
            description: Tempo RPC endpoint override.
          - name: STRIPE_SECRET_KEY
            required: false
            description: Stripe API secret key for the Stripe method.
          - name: STRIPE_PROFILE_ID
            required: false
            description: Stripe crypto profile ID for on-chain deposits.
    ---
    
    # MPP - Machine Payments Protocol
    
    MPP is an open protocol (co-authored by Tempo and Stripe) that standardizes HTTP `402 Payment Required` for machine-to-machine payments. Clients pay in the same HTTP request - no accounts, API keys, or checkout flows needed.
    
    The core protocol spec is submitted to the IETF as the [Payment HTTP Authentication Scheme](https://datatracker.ietf.org/doc/draft-ryan-httpauth-payment/).
    
    Code in this skill uses placeholder token names (`<USDC_TEMPO_MAINNET>`, `<PATHUSD_TESTNET>`); the real addresses live in the [Tempo documentation](https://docs.tempo.xyz) and `references/tempo-method.md`.
    
    ## Core Architecture
    
    Three primitives power every MPP payment:
    
    1. **Challenge** - server-issued payment requirement (in `WWW-Authenticate: Payment` header)
    2. **Credential** - client-submitted payment proof (in `Authorization: Payment` header)
    3. **Receipt** - server confirmation of successful payment (in `Payment-Receipt` header)
    
    ## Payment Methods & Intents
    
    MPP is payment-method agnostic. Each method defines its own settlement rail:
    
    | Method | Rail | SDK Package | Status |
    |--------|------|-------------|--------|
    | [Tempo](https://mpp.dev/payment-methods/tempo) | TIP-20 stablecoins on Tempo chain | `mppx` (built-in) | Production |
    | [Stripe](https://mpp.dev/payment-methods/stripe) | Cards/wallets (SPT) + on-chain crypto deposit | `mppx` (built-in) | Production |
    | [EVM](https://mpp.dev/payment-methods/evm) | EIP-3009 stablecoin authorizations (x402-exact compatible) | `mppx` (built-in) | Production |
    | [Lightning](https://mpp.dev/payment-methods/lightning) | Bitcoin over Lightning Network | `@buildonspark/lightning-mpp-sdk` | Production |
    | [Stellar](https://mpp.dev/payment-methods/stellar) | SEP-41 tokens on Stellar, charge + `channel` | `@stellar/mpp` | Production (`channel` wire spec still being drafted - subject to change) |
    | [Solana](https://mpp.dev/payment-methods/solana) | Solana-native charge + session (SOL, SPL, Token-2022) | `@solana/mpp` | Production |
    | [Monad](https://mpp.dev/payment-methods/monad) | Monad charge (ERC-3009, settlement modes) | `@monad-crypto/mpp` | Production |
    | [NEAR Intents](https://mpp.dev/payment-methods/nearintents) | Cross-chain charge via 1Click deposit addresses | `@defuse-protocol/nearintents-mpp-sdk` | Production (**not trustless** - routes through a settlement backend, advertised as `methodDetails.settlementBackend: "near-intents"` for per-method risk policy) |
    | [RedotPay](https://mpp.dev/payment-methods/redotpay) | RedotPay balance (`rdt`) or stablecoin proof, charge only | `@redotpay/mpp` | Production |
    | [Card](https://mpp.dev/payment-methods/card) | Encrypted network tokens (Visa) | `mpp-card` | Production |
    | Custom | Any rail | `Method.from()` + `Method.toClient/toServer` | Extensible |
    
    Per-method deep dives: `references/tempo-method.md`, `references/stripe-method.md`, `references/lightning-method.md`, `references/custom-methods.md`.
    
    | Intent | Pattern | Best For |
    |--------|---------|----------|
    | **charge** | One-time payment per request | API calls, content access, fixed-price endpoints |
    | **session** | Pay-as-you-go over payment channels | LLM streaming, metered billing, high-frequency APIs |
    | **subscription** | Recurring access via an authorized key (Tempo) - see `references/subscriptions.md` | Plans/tiers where access is separated from per-request billing |
    
    ## Quick Start: Server (TypeScript)
    
    ```typescript
    import { Mppx, tempo } from 'mppx/server'
    
    const mppx = Mppx.create({
      methods: [tempo({
        currency: '<PATHUSD_TESTNET>', // pathUSD testnet
        recipient: '0xYourAddress',
      })],
    })
    
    export async function handler(request: Request) {
      const result = await mppx.charge({ amount: '0.01' })(request)
      if (result.status === 402) return result.challenge
      return result.withReceipt(Response.json({ data: '...' }))
    }
    ```
    
    Install: `npm install mppx viem` (mppx 0.8.15 requires `viem >= 2.54.0`).
    
    Validate the finished server end-to-end with `npx mppx validate http://localhost:3000`.
    
    ## Quick Start: Client (TypeScript)
    
    ```typescript
    import { privateKeyToAccount } from 'viem/accounts'
    import { Mppx, tempo } from 'mppx/client'
    
    // Polyfills globalThis.fetch to handle 402 automatically
    Mppx.create({
      methods: [tempo({ account: privateKeyToAccount('0x...') })],
    })
    
    const res = await fetch('https://api.example.com/paid')
    // Payment happens transparently when server returns 402
    ```
    
    In browsers, mppx 0.6.0 changed the default: polyfilled `fetch` only sends `Accept-Payment` to **same-origin** endpoints, so cross-origin paid APIs need `acceptPaymentPolicy` (`'always'` / `{ origins: [...] }`). Client fetch retries incremental challenges up to `maxPaymentRetries` (default 3). For non-global alternatives (`Fetch.from/polyfill/restore`, `Mppx.restore()`), see `references/typescript-sdk.md`.
    
    ## Quick Start: Server (Python)
    
    ```python
    from fastapi import FastAPI
    from mpp import Credential, Receipt
    from mpp.server import Mpp
    from mpp.methods.tempo import tempo, ChargeIntent
    
    app = FastAPI()
    server = Mpp.create(method=tempo(
        currency="<PATHUSD_TESTNET>",
        recipient="0xYourAddress", intents={"charge": ChargeIntent()},
    ))
    
    @app.get("/resource")
    @server.pay(amount="0.50")
    async def get_resource(request, credential: Credential, receipt: Receipt):
        return {"data": "paid content", "payer": credential.source}
    ```
    
    Install: `pip install "pympp[tempo]"`. See `references/python-sdk.md` for full patterns.
    
    ## Quick Start: Server (Rust)
    
    Install: `cargo add mpp --features tempo,server`. See `references/rust-sdk.md` for full patterns.
    
    ## Framework Middleware (TypeScript)
    
    Each framework has its own import (`mppx/nextjs`, `mppx/hono`, `mppx/express`, `mppx/elysia`):
    
    ```typescript
    // Next.js
    import { Mppx, tempo } from 'mppx/nextjs'
    const mppx = Mppx.create({ methods: [tempo({ currency: '<PATHUSD_TESTNET>', recipient: '0x...' })] })
    export const GET = mppx.charge({ amount: '0.1' })(() => Response.json({ data: '...' }))
    
    // Hono
    import { Mppx, tempo } from 'mppx/hono'
    app.get('/resource', mppx.charge({ amount: '0.1' }), (c) => c.json({ data: '...' }))
    ```
    
    See `references/typescript-sdk.md` for Express and Elysia examples.
    
    ## Sessions: Pay-as-You-Go Streaming
    
    Sessions open a payment channel once, then use off-chain vouchers for each request - no blockchain transaction per request. Sub-100ms latency, near-zero per-request fees.
    
    **Sessions v2 (default since mppx 0.7.0):** `tempo.session()` is the TIP-1034 precompile channel flow; the earlier escrow-contract implementation is **Sessions v1**, still available as the deprecated `tempo.sessionLegacy`. A v2-expecting client rejects a v1 session and falls back to the charge path, so keep client and server on matching flows. Two client APIs: `tempo.session({ account, maxDeposit })` registers the method with `Mppx.create()` (transparent 402 handling via `fetch`), while `tempo.session.manager({ account, maxDeposit })` returns a managed client for direct lifecycle control (`.sse()`, `.close()`).
    
    ```typescript
    // Server - session endpoint with automatic settlement
    const mppx = Mppx.create({
      methods: [tempo.session({
        currency: '<PATHUSD_TESTNET>', recipient: '0x...',
        store: Store.redis(redis),
        settlementSchedule: { amount: '1.00', intervalMs: 300_000 },
        bootstrap: true, // let returning clients recover their channel on this route
      })],
    })
    const result = await mppx.session({ amount: '0.001', unitType: 'token' })(request)
    if (result.status === 402) return result.challenge
    return result.withReceipt(Response.json({ data: '...' }))
    ```
    
    ```typescript
    // Server - SSE streaming with per-word billing
    export const GET = mppx.session({ amount: '0.001', unitType: 'word' })(
      async () => async function* (stream) {
        for (const word of ['hello', 'world']) {
          await stream.charge()
          yield word
        }
      }
    )
    
    // Client - session with auto-managed channel
    Mppx.create({ methods: [tempo({ account, maxDeposit: '1' })] })
    const res = await fetch('http://localhost:3000/api/resource')
    // 1st request: opens channel on-chain; 2nd+: off-chain vouchers
    ```
    
    Sessions also stream over WebSocket via `Ws.serve()`. See `references/sessions.md` for the full lifecycle, settlement, stores, SSE and WebSocket patterns, and channel recovery.
    
    ## Multi-Method Support
    
    Accept Tempo stablecoins, Stripe cards, and Lightning Bitcoin on a single endpoint:
    
    ```typescript
    const mppx = Mppx.create({
      methods: [
        tempo({ currency: '<PATHUSD_TESTNET>', recipient: '0x...' }),
        stripe.charge({ client: new Stripe(key), networkId: 'profile_...', paymentMethodTypes: ['card'] }),
        spark.charge({ mnemonic: process.env.MNEMONIC! }),
      ],
    })
    ```
    
    Use `Mppx.compose()` to present multiple methods in a single 402 response with per-route pricing. Apply the same branch at the challenge site and the verification site, or the 402 advertises fewer options than the server accepts. See `references/typescript-sdk.md`.
    
    ## Payment Links (HTML)
    
    Setting `html: true` on a payment method config renders a browser-friendly payment page when a 402 endpoint is visited in a browser, with theming, multi-method compose tabs, and Solana wallet support. Service workers handle credential submission, then the page reloads with the paid response.
    
    Customize via `mppx/html` exports (`Config`, `Text`, `Theme`), and build a custom method's payment link with `Html.init(methodName)`.
    
    ## Zero-Dollar Auth (Proof Credentials)
    
    Authenticate agent identity without payment. Clients sign an EIP-712 proof over the challenge ID instead of creating a transaction - no gas burned, no funds transferred.
    
    ```typescript
    // Server - zero-dollar charge, with a store for replay protection
    const mppx = Mppx.create({
      methods: [tempo.charge({ currency: '<PATHUSD_TESTNET>', recipient: '0x...', store })],
    })
    const result = await mppx.charge({ amount: '0' })(request)
    ```
    
    Since mppx 0.8.0 these proofs are **bound to the payer wallet**: the EIP-712 `Proof` typed data (exposed as `tempo.Proof`) carries an `account` field at domain version `3`, so a proof signed for one account no longer verifies against another.
    
    Use cases: identity verification, long-running job polling, paid unlock with free subsequent access, multi-step agent pipelines. See [mpp.dev/advanced/identity](https://mpp.dev/advanced/identity).
    
    ## Payments Proxy
    
    Gate existing APIs behind MPP payments:
    
    ```typescript
    // import { openai, Proxy } from 'mppx/proxy' - a service inside Proxy.create({ services: [...] })
    openai({
      apiKey: process.env.OPENAI_API_KEY,
      routes: {
        'POST /v1/chat/completions': mppx.charge({ amount: '0.05' }),
        'GET /v1/models': true, // literal `true` marks a free route
      },
    })
    ```
    
    Built-in presets `openai()`, `anthropic()`, `stripe()`, plus `custom()` for any upstream. See `references/discovery-and-proxy.md` for `Proxy.create()`, the discovery endpoints it serves, and the `discovery()` helper for non-proxy servers.
    
    ## MCP Transport
    
    MCP tool calls can require payment using JSON-RPC error code `-32042` (servers may also issue `-32043`):
    
    ```typescript
    // Server - import tempo from mppx/server, NOT mppx/tempo
    import { McpServer } from 'mppx/mcp/server'
    import { tempo } from 'mppx/server'
    const server = McpServer.wrap(baseServer, { methods: [tempo.charge({ /* ... */ })], secretKey })
    
    // Client - payment-aware MCP client (import tempo from mppx/client)
    import { McpClient } from 'mppx/mcp/client'
    import { tempo } from 'mppx/client'
    const mcp = McpClient.wrap(client, { methods: [tempo({ account })] })
    const result = await mcp.callTool({ name: 'premium_tool', arguments: {} })
    ```
    
    MCP-over-HTTP challenges settle in the same payment-aware fetch, and transports are pluggable via `Transport.from/http/mcp/mcpSdk` on both sides. See `references/transports.md`.
    
    ## Privy Server Wallets
    
    `createViemAccount` from `@privy-io/node/viem` (needs `@privy-io/node` >= 0.20.0) returns a viem `Account` backed by a [Privy](https://docs.privy.io) server wallet, so it drops into `tempo({ account })` wherever a local account would go.
    
    Server-side signing works with **app-owned server wallets**; user-owned embedded wallets require authorization keys or key quorums. See `references/typescript-sdk.md` for the full setup and the manual `toAccount()` construction.
    
    ## Testing & CLI
    
    ```bash
    # Create an account (stored in keychain), then fund it on testnet
    npx mppx account create
    npx mppx account fund --network testnet
    
    # Make a paid request
    npx mppx http://localhost:3000/resource
    
    # Parse a challenge without signing it
    npx mppx sign --dry-run --challenge '<www-authenticate value>'
    
    # Validate a server implementation end-to-end
    npx mppx validate http://localhost:3000
    ```
    
    The CLI also covers `init`, `sessions` (list/view/close), `discover`, `services`, `mcp add`, and `skills add`. Config comes from `MPPX_CONFIG` or an explicit `--config` - there is no auto-discovery from the working directory. Full reference: `references/cli.md`.
    
    ## SDK Packages
    
    | Language | Package | Install |
    |----------|---------|---------|
    | TypeScript | [`mppx`](https://github.com/wevm/mppx) | `npm install mppx` |
    | Python | [`pympp`](https://github.com/tempoxyz/pympp) | `pip install "pympp[tempo]"` |
    | Rust | [`mpp`](https://github.com/tempoxyz/mpp-rs) | `cargo add mpp --features tempo,client,server` |
    | Ruby | [`mpp-rb`](https://github.com/stripe/mpp-rb) (official, by Stripe) | see repo for gem name |
    | Go | [`mpp-go`](https://github.com/tempoxyz/mpp-go) (official, by Tempo) | `go get github.com/tempoxyz/mpp-go` |
    | Elixir | [`mpp`](https://github.com/ZenHive/mpp) (community) | [hex.pm/packages/mpp](https://hex.pm/packages/mpp) |
    | Swift | [`mpp-swift`](https://github.com/amitach/mpp-swift) (community) | see repo |
    
    Capability notes, checked against SDK source rather than the docs matrices (upstream publishes two that disagree):
    
    - **Session** intent: TypeScript and Rust only.
    - **Proof Credentials** (zero-dollar auth): TypeScript, Rust, and Ruby. **Not** pympp - the Python Tempo method implements only `hash` and `transaction` payload types.
    - **Stripe, MCP, and event handling**: TypeScript, Python, Rust, Ruby. Not the official `mpp-go`, which ships client/server/charge/fee-sponsorship/proof with net/http, Gin, Echo, and Chi middleware. A separate community Go `mppx` (cp0x) also exists.
    
    Go and Ruby have first-class SDK doc pages at [mpp.dev/sdk/go](https://mpp.dev/sdk/go) and [mpp.dev/sdk/ruby](https://mpp.dev/sdk/ruby).
    
    Always import `Mppx` and `tempo` from the subpath matching your context (`mppx/server`, `mppx/client`, or the framework subpath). Note: `Mppx` and `tempo` are NOT exported from `mppx/tempo` - that subpath only exports `Session` and `Ws`. The authoritative subpath table is in `references/typescript-sdk.md`.
    
    ## Key Concepts
    
    - **Challenge/Credential/Receipt**: The three protocol primitives. Challenge IDs are HMAC-SHA256 bound to prevent tampering. See `references/protocol-spec.md`
    - **Split payments**: One charge across multiple recipients in a single transaction (1-10 splits, per-split memos, `expectedRecipients`). See `references/tempo-method.md`
    - **Fee sponsorship**: Server pays gas on behalf of clients, capped by `maxInFlightReservations` / `maxInFlightTotalFee`
    - **Relays**: Delegate credential validation and broadcast to Tempo API or a compatible relay via `tempo.charge({ relay })`
    - **Push/pull modes**: Client broadcasts the transaction (push) or the server does (pull)
    - **Client chain pinning**: `tempo.charge({ expectedChainId })` rejects challenges for the wrong Tempo network
    - **Reusable client channels**: pass a `channelStore` to persist and reuse payer session channels across processes
    - **x402 interop**: `evm.charge({ x402: { facilitator } })` serves native MPP and x402 "exact" challenges from one route; the client prefers Payment-auth challenges
    - **Custom methods**: Implement any payment rail with `Method.from()`. See `references/custom-methods.md`
    
    ## Payment Hooks
    
    Attach logging, metrics, or tracing without touching the handler. Register on the object returned by `Mppx.create()`; each registration returns an unsubscribe function.
    
    - **Server** (`mppx/server`): `onChallengeCreated`, `onPaymentSuccess`, `onPaymentFailed`, `onSessionSettlement`, `on('*')`
    - **Client** (`mppx/client`): `onChallengeReceived`, `onCredentialCreated`, `onPaymentResponse`, `onPaymentFailed`
    
    Server handlers are awaited inline on the request path - keep them fast. `onPaymentFailed` is the practical way to see the real error behind an opaque 402. See `references/typescript-sdk.md` and [mpp.dev/advanced/payment-hooks](https://mpp.dev/advanced/payment-hooks).
    
    ## Managing Agent Spend
    
    Bound an agent's payment authority with **Tempo access keys** - delegated signing keys with built-in spend controls, their own expiry, and a revocation path.
    
    ```typescript
    import { Expiry } from 'accounts'
    import { numberToHex, parseUnits } from 'viem'
    import { Scopes } from 'viem/tempo'
    
    const accessKey = {
      expiry: Expiry.days(7),
      limits: [{ token: usdc, limit: numberToHex(parseUnits('10', 6)), period: 86_400 }], // 10 USDC/day
      scopes: [Scopes.tip20(usdc).transfer({ recipients: [recipientAddress] })],
    }
    // Authorize: provider.request({ method: 'wallet_connect', params: [{ capabilities: { authorizeAccessKey: accessKey } }] })
    
    Mppx.create({
      methods: [tempo({
        account: provider.getAccount(),
        ...provider.getMppxParameters({ accessKey: accessKeyAddress }),
      })],
    })
    ```
    
    Spend limits are **hex-encoded** - pass `numberToHex(parseUnits(...))`, not a raw bigint. Separate keys per app/tool/deployment keep delegated runtimes isolated. See [mpp.dev/guides/managing-agent-spend](https://mpp.dev/guides/managing-agent-spend) and [Tempo access keys](https://docs.tempo.xyz/guide/use-accounts/authorize-access-keys).
    
    ## Production Gotchas
    
    The failure modes that cost the most time. Full detail in `references/production-gotchas.md`:
    
    - **Tempo has no native gas token.** Set `feeToken` or call `setUserToken`, or transactions fail with `gas_limit: 0`. "Fund with ETH" errors mean "fund with the stablecoin fee token"
    - **Sessions do not settle themselves.** Configure `settlementSchedule` or run your own `tempo.settle()` / `tempo.settleBatch()` sweep, paired with a close policy for idle channels - otherwise revenue accrues as unredeemed vouchers and channels stay open holding payer deposits
    - **Charge settles before your handler runs.** Use `validateCredential` then `broadcastCredential` when payment should depend on the work succeeding. Challenges expire after 5 minutes by default
    - **Never use `Store.memory()` in production.** Lost channel state means deposits stay reserved indefinitely
    - **Set `realm` explicitly.** Env vars outrank the per-request hostname, and Kubernetes `HOSTNAME` rotates every deploy, breaking mppscan attribution
    - **Session voucher, `close`, and `topUp` credentials are bodyless POSTs**, so a body validator running before `mppx.session()` rejects them with a spurious 400. Clone the request before reading its body, or mppx sees an empty one and returns 402
    - **Large 402 headers overflow nginx's 4k default buffer** and surface as 502
    
    ## References
    
    | File | Content |
    |------|---------|
    | `references/protocol-spec.md` | Challenge/Credential/Receipt, status codes, security |
    | `references/typescript-sdk.md` | mppx: server, client, middleware, transports, stores |
    | `references/cli.md` | mppx CLI: requests, validate, sign, accounts, config |
    | `references/production-gotchas.md` | Field-tested failure modes and their fixes |
    | `references/sessions.md` | Channels, vouchers, settlement, SSE/WS, recovery |
    | `references/subscriptions.md` | Subscription intent: activation, renewal, cancellation |
    | `references/tempo-method.md` | Tempo: fees, relays, push/pull, splits, sessions |
    | `references/stripe-method.md` | Stripe: SPT fiat flow, crypto deposit, Elements |
    | `references/discovery-and-proxy.md` | Proxy services, discovery documents, registries |
    | `references/transports.md` | HTTP, MCP, and WebSocket transport bindings |
    | `references/python-sdk.md` | pympp: `@server.pay`, async client, charge intent |
    | `references/rust-sdk.md` | mpp Rust: server/client, features, sessions |
    | `references/lightning-method.md` | Lightning: BOLT11 charge, bearer sessions, Spark |
    | `references/custom-methods.md` | `Method.from`, `toClient`, `toServer` patterns |
    
    ## Official Resources
    
    - Website: [mpp.dev](https://mpp.dev) - LLM docs: [llms-full.txt](https://mpp.dev/llms-full.txt) - Spec: [paymentauth.org](https://paymentauth.org)
    - GitHub: [wevm/mppx](https://github.com/wevm/mppx) (TypeScript SDK), [tempoxyz/mpp](https://github.com/tempoxyz/mpp) (docs), [tempoxyz/mpp-specs](https://github.com/tempoxyz/mpp-specs) (spec)
    - IETF draft: [draft-ryan-httpauth-payment-01](https://datatracker.ietf.org/doc/draft-ryan-httpauth-payment/) (Standards Track)
    - [Stripe MPP docs](https://docs.stripe.com/payments/machine/mpp) - [Tempo docs](https://docs.tempo.xyz) - [x402 interop](https://mpp.dev/guides/use-mpp-with-x402) - [mpp vs x402](https://mpp.dev/mpp-vs-x402) - [governance](https://mpp.dev/governance)
    - Agent wallets: [mpp.dev/tools/wallet](https://mpp.dev/tools/wallet) - Partner integrations: [Cloudflare Agents](https://mpp.dev/partner-integrations/cloudflare-agents), [Vercel AI SDK](https://mpp.dev/partner-integrations/vercel-ai-sdk), [MCP SDK](https://mpp.dev/partner-integrations/mcp-sdk), [OpenClaw](https://mpp.dev/partner-integrations/openclaw) - community [extensions](https://mpp.dev/extensions)
    - Docs MCP: `claude mcp add --transport http mpp https://mpp.dev/api/mcp` (8 tools: `list_pages`, `read_page`, `search_docs`, `search_source`, `list_sources`, `list_source_files`, `read_source_file`, `get_file_tree`). Services MCP: [mpp.dev/mcp/services](https://mpp.dev/mcp/services)
    - Upstream publishes its own machine-readable skill at `mpp.dev/.well-known/agent-skills/mppx/SKILL.md`; install via `npx skills add tempoxyz/mpp -g` or `mppx skills add`
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related