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
Install
npx skills add https://github.com/tenequm/skills/tree/main/skills/mpp
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install tenequm-skills@llmmart
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:
- Challenge - server-issued payment requirement (in
WWW-Authenticate: Paymentheader) - Credential - client-submitted payment proof (in
Authorization: Paymentheader) - Receipt - server confirmation of successful payment (in
Payment-Receiptheader)
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
hashandtransactionpayload 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 Gomppx(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). Seereferences/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
channelStoreto 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(). Seereferences/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
feeTokenor callsetUserToken, or transactions fail withgas_limit: 0. "Fund with ETH" errors mean "fund with the stablecoin fee token" - Sessions do not settle themselves. Configure
settlementScheduleor run your owntempo.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
validateCredentialthenbroadcastCredentialwhen 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
realmexplicitly. Env vars outrank the per-request hostname, and KubernetesHOSTNAMErotates every deploy, breaking mppscan attribution - Session voucher,
close, andtopUpcredentials are bodyless POSTs, so a body validator running beforemppx.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 - LLM docs: llms-full.txt - Spec: paymentauth.org
- GitHub: wevm/mppx (TypeScript SDK), tempoxyz/mpp (docs), tempoxyz/mpp-specs (spec)
- IETF draft: draft-ryan-httpauth-payment-01 (Standards Track)
- Stripe MPP docs - Tempo docs - x402 interop - mpp vs x402 - governance
- Agent wallets: mpp.dev/tools/wallet - Partner integrations: Cloudflare Agents, Vercel AI SDK, MCP SDK, OpenClaw - community 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 - Upstream publishes its own machine-readable skill at
mpp.dev/.well-known/agent-skills/mppx/SKILL.md; install vianpx skills add tempoxyz/mpp -gormppx skills add
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.
Reviews (0)
No reviews yet.
No comments yet.