# MPP — Machine Payments Protocol MPP (Machine Payments Protocol) is the open standard for machine-to-machine payments via HTTP 402. # Machine Payments Protocol \[The open protocol for machine-to-machine payments] The Machine Payments Protocol (MPP) lets any client—agents, apps, or humans—pay for any service in the same HTTP request. Developers use MPP to let their agents pay for services. Service operators use MPP to accept payments for their APIs. MPP is built around a simple, extensible core and is neutral to the implementation of underlying payment flows and methods. * **Open standard built for the internet**—Built on an [open specification proposed to the IETF](https://paymentauth.org), not a proprietary API * **Designed for payments**—Idempotency, security, and receipts are first-class primitives * **Works with stablecoins, cards, and bank transfers**—All payment methods can be supported through one protocol and flexible control flow * **Any currency**—Transact in USD, EUR, BRL, USDC.e, BTC, or any other asset * **Composable and designed for extension**—A flexible core allows advanced flows like disputes or additional primitives like identity to be gradually introduced ## Who is MPP for? MPP involves three parties: * **Developers** build apps and agents that consume paid services. You integrate an MPP client so your agent can discover, pay for, and use third-party APIs without manual signup or API keys. * **Agents** are the entities that take action—calling APIs, generating images, querying data. They pay for services autonomously on behalf of your users. * **Services** operate APIs that charge for access—LLM inference, image generation, web search, and more. You integrate an MPP server to accept payments with zero onboarding friction. ## The problem with payments on the internet There is no shortage of ways to pay for things on the internet. Hundreds of payment methods give users ample space for personal preference, and optimized payment forms with one-click checkout ensure that the act of paying is low-friction and highly secure. However, the very things that make these payment flows familiar and fast for human purchasers are structural headwinds for programmatic consumption. Many have tried, but it is a consistent uphill battle to fight browser automation pipelines, visual captchas, and ever changing payment forms—all of which reduce reliability, increase latency, and bear high costs. This is not the fault of any individual payment method or credential. This is a global problem which exists at the *interface* level: how buyer and seller negotiate cost, supported payment methods, and ultimately transact. The Machine Payments Protocol addresses this gap by providing a payment interface built for programmatic access that strips away the complexity of rich checkout flows, while still providing robust security and reliability. By using MPP, you can accept payments from any client—agents, apps, or humans—and across any payment method, without complex checkout flows and integrations. ## Try it out See the full payment flow in action. The terminal creates an ephemeral wallet, funds it with testnet USDC.e, and makes a paid request.
## Use cases * **[Agentic payments](/use-cases/agentic-payments)**—Your agent calls LLM providers, search APIs, and image generators through MPP, paying per request without API keys or human intervention. * **[API monetization](/use-cases/api-monetization)**—Accept payments from any client—agents, apps, or humans—without requiring signups, billing accounts, or API keys. * **[Micropayments](/use-cases/micropayments)**—Charge sub-cent amounts per token, per query, or per request using off-chain payment sessions with on-chain settlement. ## Payment flow When a client requests a paid resource, the server returns a `402` response with the payment options they support. The client chooses a payment method, fulfills the request and retries with a payment `Credential` which contains proof of payment. The server verifies the payment and returns the resource with a `Receipt` which contains proof of delivery. ## Official SDKs MPP comes with a suite of official SDKs maintained by [Tempo Labs](https://tempo.xyz) and [Wevm](https://wevm.dev). The SDKs offer high-level abstractions and low-level primitives to implement and extend the Machine Payments Protocol. ## Next steps # Frequently asked questions \[Common questions about the Machine Payments Protocol] ## Is MPP only for stablecoins? No. MPP is payment-method agnostic—the protocol works with any payment rail. Today, [Tempo](/payment-methods/tempo) stablecoin payments, [Stripe](/payment-methods/stripe) (Visa, Mastercard, and other card networks), and [Lightning](/payment-methods/lightning) (Bitcoin over the Lightning Network) are in production. Anyone can build a [custom payment method](/payment-methods/custom) by implementing the core control flow for their payment rail. See the full list of payment methods and specifications at [paymentauth.org](https://paymentauth.org). ## Do I need a stablecoin wallet? No. With Stripe, you can pay with cards without stablecoins. With Lightning, you can pay with Bitcoin. For Tempo payments, you need a stablecoin wallet to sign transactions. The SDK and the `tempo wallet` CLI handle key management for you. ## How is MPP different from x402? Both MPP and x402 use HTTP `402` to signal that a request requires payment. The key differences: See [MPP vs x402](/mpp-vs-x402) for a full side-by-side comparison, or [Running x402 servers with mppx](/guides/upgrade-x402#running-x402-servers-with-mppx) for an implementation guide. * **Payment-method agnostic.** MPP supports stablecoins, cards, wallets, and custom rails through extensible payment method specifications. x402 only supports blockchains. * **Designed for production.** MPP supports idempotency, expiration, request-body binding (digest), and request-tampering mitigations as first-class primitives. * **Performant payments.** MPP's session intent enables pay-as-you-go metering for payments as small as 0.0001 USD. Sessions achieve sub-100ms latency and near-zero per-request fees by settling off-chain vouchers, enabling high-throughput applications like token streaming or content aggregation. x402 requires an on-chain transaction per request. * **Permissionless extensibility.** Anyone can author and publish a new payment method or intent specification without approval from a foundation or intermediary. Payment methods compete on adoption and are independently maintained. * **IETF standards track.** The core [Payment HTTP Authentication Scheme](https://paymentauth.org) is submitted to the IETF for standardization. ## Is MPP compatible with x402? Yes. The core x402 "exact" flows map directly onto MPP's charge intent. `mppx` can serve x402 and MPP clients from the same route. See [Running x402 servers with mppx](/guides/upgrade-x402#running-x402-servers-with-mppx). ## Why build MPP on Tempo? MPP works with any payment rail—Stripe for cards, Lightning for Bitcoin, or any custom method. You don't have to use Tempo. That said, high-throughput, low-value transactions benefit from specific properties that Tempo provides: * **Fast, deterministic finality**—Certainty that a payment has settled, not probabilistic confirmation. * **Low, predictable cost**—Transaction fees stay stable regardless of global network congestion. * **Payment lanes**—Dedicated transaction routing for payment traffic, ensuring reliability even under heavy load. * **Stablecoin-native**—TIP-20 stablecoins (USDC.e, USDT) are first-class citizens, so payments are denominated in familiar currency. These properties make Tempo well-suited as a settlement layer for machine payments where speed, cost, and reliability matter. ## What are sessions? Sessions are a Tempo-specific payment intent that enables streaming, pay-as-you-go payments. Instead of paying per request, a client opens a session (depositing funds into an escrow contract), then makes many requests by issuing signed vouchers off-chain. The server periodically settles the accumulated vouchers on-chain. Sessions are the mechanism that makes [micropayments](/use-cases/micropayments) viable—sub-cent transactions cost nothing individually because only net settlement hits the chain. See the [session documentation](/payment-methods/tempo/session) for details. Because sessions bypass consensus for individual interactions, they achieve client-to-server latency (low double-digit milliseconds), near-zero per-request fees, and horizontally scalable throughput. The bottleneck is CPU, not blockchain TPS. ## How much does it cost? Pricing is set per service. For individual charge payments, typical prices range from $0.01 to $0.10 per request. For session-based payments, the per-request cost can go much lower because each interaction is a signed voucher rather than an on-chain transaction—only net settlement hits the chain. The protocol itself is free and open. There are no licensing fees for implementing MPP. ## Is it safe? MPP requires TLS 1.2+ for all connections. Challenge IDs are cryptographically bound to prevent replay attacks. The protocol never performs side effects on unpaid requests—your client only pays after verifying what it is paying for. Payments use the same security model as the underlying payment method. For Tempo, that means cryptographic signatures over every transaction. For Stripe, it means Stripe's existing fraud and dispute infrastructure. For operational guidance on `MPP_SECRET_KEY`, logging, and rotation, see [Security](/advanced/security). ## How do I handle `MPP_SECRET_KEY`? Treat `MPP_SECRET_KEY` as root-of-trust material for server-side Challenge binding. Store it in a secrets manager, keep it server-side, never log it, rotate it immediately if it is exposed, and use overlapping current-and-previous key verification during rollovers so in-flight Challenges keep working. See [Security](/advanced/security) for the full guidance. ## What happens if a payment fails? The service returns an error with details following [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) (Problem Details for HTTP APIs). Your client can retry with a different payment method or surface the error. No money is deducted for failed requests. ## Can I accept MPP payments for my own service? Yes. See the [server quickstart](/quickstart/server) to start accepting payments in a few lines of code, or read more about [API monetization with MPP](/use-cases/api-monetization). The TypeScript SDK includes middleware for popular frameworks including Hono, Express, Next.js, and Elysia. ## Is MPP an IETF standard? The core [Payment HTTP Authentication Scheme](https://datatracker.ietf.org/doc/draft-ryan-httpauth-payment/) is submitted to the IETF standards track. Payment method and intent specifications (for example, charge, session) are separate documents that anyone can author and publish independently—they do not require IETF approval. This mirrors how the web works: HTTP is standardized, but content types and authentication schemes evolve independently. ## Can I use MPP outside of HTTP? Yes. MPP includes an [MCP transport binding](/protocol/transports/mcp) that maps the Challenge-Credential-Receipt flow onto the Model Context Protocol. This means MCP servers can monetize tool calls directly, and agents pay autonomously without OAuth or account setup. ## Who is building MPP? MPP is co-authored by Tempo and Stripe. The core specification is developed in the open and designed to be extended by any payment network or provider. The [Payment HTTP Authentication Scheme](https://datatracker.ietf.org/doc/draft-ryan-httpauth-payment/) is submitted to the IETF. # Build with an LLM \[Give your agent MPP context] Point your coding agent at `llms-full.txt`, a single file containing the complete documentation. ## Get started Copy this URL and paste it into your agent: ```bash [terminal] https://mpp.dev/llms-full.txt ``` Your agent now has full context on MPP's client and server APIs, payment methods, and integration patterns. ## Advanced options These alternatives provide different ways to consume the docs depending on your workflow. ### Agent skills Install skills for your coding agent using the [`skills` CLI](https://github.com/vercel-labs/skills): ```bash [terminal] $ npx skills add tempoxyz/mpp -g ``` After installing, your agent knows how to integrate `mppx` with your chosen framework. ### llms.txt Each page has a [`llms.txt`](https://llmstxt.org) file for LLM consumption: * `llms.txt`: A concise index of all pages with titles and descriptions * `llms-full.txt`: Complete documentation content in a single file ### MCP server Connect the docs as an [MCP server](https://modelcontextprotocol.io) so your agent can search and read pages directly: ::::code-group ```bash [Claude] $ claude mcp add --transport http mpp https://mpp.dev/api/mcp ``` ```bash [Codex] $ codex mcp add --transport http mpp https://mpp.dev/api/mcp ``` ```bash [Amp] $ amp mcp add --transport http mpp https://mpp.dev/api/mcp ``` ```json [Manual] // Claude: .mcp.json | Cursor: ~/.cursor/mcp.json // Windsurf: ~/.codeium/windsurf/mcp_config.json { "mcpServers": { "mpp": { "url": "https://mpp.dev/api/mcp" } } } ``` :::: **Available tools:** | Tool | Description | | --- | --- | | `list_pages` | List all documentation pages with their paths | | `read_page` | Read the content of a specific documentation page | | `search_docs` | Search documentation for a query string | | `list_sources` | List available source code repositories | | `list_source_files` | List files in a directory | | `read_source_file` | Read a source code file | | `get_file_tree` | Get a recursive file tree | | `search_source` | Search source code for a pattern | # Quickstart \[Get started with MPP in minutes] MPP lets APIs charge for access. Servers request payment when you hit a paid endpoint; clients pay; servers verify and return the resource. [Learn more](/protocol). ## Start prompting Paste one of these into your coding agent to build your first MPP app or service: ## Start building Pick a starting point based on your role: # Add payments to your API \[Charge for access to protected resources] ## Overview This quickstart demonstrates how to plug MPP into any server framework to accept payments for protected resources. Pick the path that suits you: * [**Prompt mode**](#prompt-mode): paste a prompt into your coding agent and build in one prompt * [**Framework mode**](#framework-mode): use `mppx` middleware for Next.js, Hono, Elysia, or Express * [**Manual mode**](#advanced-manual-mode): call `mppx/server` directly with the Fetch API ## Prompt mode Paste this into your coding agent to set up a server with `mppx` in one prompt: :::warning\[Set `MPP_SECRET_KEY` before you start] `Mppx.create()` reads `MPP_SECRET_KEY` by default. Store it in your platform secret manager, keep it server-side, and never log it. See [Security](/advanced/security). ::: ## Framework mode Use the framework-specific middleware from `mppx` to integrate payment into your server. Each middleware handles the `402` Challenge/Credential flow and attaches receipts automatically. ::::code-group ```ts [Next.js] import { Mppx, tempo } from 'mppx/nextjs' // [!code hl:start] const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', // pathUSD on Tempo recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) // [!code hl:end] export const GET = mppx.charge({ amount: '0.1' }) // [!code hl] (() => Response.json({ data: '...' })) ``` ```ts [Hono] import { Hono } from 'hono' import { Mppx, tempo } from 'mppx/hono' const app = new Hono() // [!code hl:start] const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) // [!code hl:end] app.get( '/resource', mppx.charge({ amount: '0.1' }), // [!code hl] (c) => c.json({ data: '...' }), ) ``` ```ts [Elysia] import { Elysia } from 'elysia' import { Mppx, tempo } from 'mppx/elysia' // [!code hl:start] const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) // [!code hl:end] const app = new Elysia() .guard( { beforeHandle: mppx.charge({ amount: '0.1' }) }, // [!code hl] (app) => app.get('/resource', () => ({ data: '...' })), ) ``` ```ts [Express] import express from 'express' import { Mppx, tempo } from 'mppx/express' const app = express() // [!code hl:start] const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) // [!code hl:end] app.get( '/resource', mppx.charge({ amount: '0.1' }), // [!code hl] (req, res) => res.json({ data: '...' })) ``` :::: :::tip You can also override `currency` and `recipient` per call if different routes need different payment configurations. ```ts mppx.charge({ amount: '0.1', currency: '0x…', // [!code ++] recipient: '0x…', // [!code ++] }) ``` ::: :::note Don't see your framework? `mppx` is designed to be framework-agnostic. See [Manual mode](#advanced-manual-mode) below. ::: ## Advanced: manual mode If you prefer full control over the payment flow, use `mppx/server` directly with the Fetch API. ```ts import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) // [!code focus:start] export async function handler(request: Request) { const response = await mppx.charge({ amount: '0.1' })(request) // [!code focus:end] // Payment required: send 402 response with challenge if (response.status === 402) return response.challenge // Payment verified: attach receipt and return resource return response.withReceipt(Response.json({ data: '...' })) } ``` :::info\[Currency and recipient values] `currency` is the TIP-20 token contract address—[`0x20c0…`](https://explore.tempo.xyz/address/0x20c0000000000000000000000000000000000000?live=false) is pathUSD on Tempo. `recipient` is the address that receives payment. See [Tempo payment method](/payment-methods/tempo) for supported tokens. ::: The intent handler accepts a [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)-compatible request object, and returns a `Response` object. The Fetch API is compatible with most server frameworks, including: [Hono](https://hono.dev), [Deno](https://deno.com), [Cloudflare Workers](https://workers.dev), [Next.js](https://nextjs.org), [Bun](https://bun.sh), and other Fetch API-compatible frameworks. :::tip You can also override `currency` and `recipient` per call if different routes need different payment configurations. ```ts const response = await mppx.charge({ amount: '0.1', currency: '0x…', // [!code ++] recipient: '0x…', // [!code ++] })(request) ``` ::: ## Node.js & Express compatibility If your framework doesn't support the **Fetch API** (for example, Express or Node.js), you're likely interfacing with the [Node.js Request Listener API](https://nodejs.org/api/http.html#httpcreateserveroptions-requestlistener). Use the `Mppx.toNodeListener` helper to transform the handler into a Node.js-compatible listener. ```ts twoslash import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) type IncomingMessage = import('node:http').IncomingMessage type ServerResponse = import('node:http').ServerResponse // ---cut--- export async function handler(req: IncomingMessage, res: ServerResponse) { const response = await Mppx.toNodeListener( // [!code ++] mppx.charge({ amount: '0.1' }) )(req, res) // [!code ++] // Payment required: send 402 response with challenge if (response.status === 402) return response.challenge // Payment verified: attach receipt and return resource return response.withReceipt(Response.json({ data: '...' })) } ``` ## Push & pull modes Non-zero Tempo charges support two transaction submission modes, determined by the client. Zero-amount charges skip transaction submission entirely and use a `proof` Credential payload instead. * **`pull` mode (default)**: the client signs the transaction and sends the serialized transaction to the server. The server broadcasts it and verifies on-chain. This enables the server to sponsor gas fees via a `feePayer`. * **`push` mode**: the client builds, signs, and broadcasts the transaction itself (for example, via a browser wallet). It sends the transaction hash to the server, which verifies the payment by fetching the receipt. Your server handles all three payload types automatically—no configuration required. The server inspects the credential payload type (`proof` for zero-amount Challenges, `transaction` for pull, `hash` for push) and verifies accordingly. If you would like to force a specific mode, you can set the `mode` parameter to `'pull'` or `'push'`. ```ts import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', mode: 'push', // [!code focus] recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) ``` The `mode` parameter only affects non-zero charges. When `amount` is `0`, the client always returns a `proof` payload and `feePayer` is irrelevant. ### Fee sponsorship To sponsor gas fees for pull-mode clients, pass a `feePayer` account to `tempo()`: ```ts import { Mppx, tempo } from 'mppx/server' import { privateKeyToAccount } from 'viem/accounts' const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', feePayer: privateKeyToAccount('0x…'), // [!code focus] recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) ``` It is possible to pass a [fee payer service](https://docs.tempo.xyz/sdk/typescript/server/handler.feePayer) URL instead: ```ts import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', feePayer: 'https://sponsor.example.com', // [!code focus] recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) ``` When a pull-mode client submits a signed transaction, the server co-signs with the fee payer account (or delegates to the relay) before broadcasting. Push-mode clients pay their own gas, so `feePayer` is ignored for those requests. Zero-amount proof flows do not create a transaction at all. ### Optimistic verification By default, the server waits for onchain confirmation before returning a Receipt. For lower latency, set `waitForConfirmation: false` to return immediately after simulation: ```ts const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', waitForConfirmation: false, // [!code focus] })], }) ``` :::warning Optimistic verification simulates the transaction but does not wait for inclusion. If the transaction reverts onchain after broadcast, the Receipt does not reflect the failure. Only use this when latency matters more than guaranteed confirmation. ::: ## Discovery After your server is running, add [discovery](/advanced/discovery) so agents can find your API and its payment terms automatically. The `discovery()` helper generates a `GET /openapi.json` endpoint from your route configuration: ```ts [server.ts] import { Hono } from 'hono' import { Mppx, discovery } from 'mppx/hono' import { tempo } from 'mppx/server' const app = new Hono() const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) app.get('/resource', mppx.charge({ amount: '0.1' }), (c) => c.json({ data: '...' })) // [!code hl:start] discovery(app, mppx, { auto: true, info: { title: 'My API', version: '1.0.0' }, }) // [!code hl:end] ``` The generated document advertises each paid route with canonical `x-payment-info.offers[]` entries. See [Discovery](/advanced/discovery) for the full document shape, multi-offer examples, and flat-shorthand compatibility notes. Register your service on [MPPScan](https://mppscan.com) or the [MPP Services directory](/services) so agents and registries can discover it. ## Testing your server After your server is running, test it with the `mppx` CLI: ```bash [terminal] # Create an account funded with testnet tokens $ npx mppx account create # Make a paid request $ npx mppx /resource ``` :::tip Use `npx mppx --inspect` to debug your server's Challenge response without making any payments. ::: ## Next steps # Use with agents \[Connect your agent to MPP-enabled services] Agents can automatically interact with MPP-enabled services, paying for API calls without human intervention. Learn more about [agentic payments](/use-cases/agentic-payments) or get started below. | Tool | Best for | Setup | |------|----------|-------| | [Tempo Wallet](#tempo-wallet) | MPP services with spend controls and service discovery | `tempo wallet login` | | [Privy Agent CLI](#privy-agent-cli) | Multi-chain agent wallets with browser-based funding | `privy-agent-wallets login` | | [AgentCash](#agentcash) | Discover and use 300+ premium APIs via MPP | `npx agentcash onboard` | | [`mppx` CLI](#mppx) | Development and debugging | `mppx account create` | ## Tempo Wallet Recommended The [Tempo Wallet](https://wallet.tempo.xyz) is a managed MPP client with built in spend controls and service discovery. Agents can use `tempo wallet` to discovery and pay for MPP-enabled services on demand. Paste this into your agent to set up Tempo Wallet: ``` Read https://tempo.xyz/SKILL.md and set up tempo ``` ::::steps ### Install the CLI ```bash [terminal] $ curl -fsSL https://tempo.xyz/install | bash ``` ### Connect your wallet ```bash [terminal] $ tempo wallet login ``` ### Verify setup ```bash [terminal] $ tempo wallet whoami ``` ### List available services ```bash [terminal] $ tempo wallet services ``` ### Make a paid request ```bash [terminal] $ tempo request -X POST \ --json '{"prompt": "a sunset over the ocean"}' \ https://fal.mpp.tempo.xyz/fal-ai/flux/dev ``` :::: ## Privy Agent CLI [Privy Agent CLI](https://docs.privy.io/recipes/agent-integrations/agent-cli) gives agents a CLI-first way to create, fund, and manage wallets with no integration code. It pairs with the [Agent Sandbox](https://agents.privy.io/) where users track agent spending, manage funds, and revoke access. The agent never holds the wallet private key—each CLI session generates a P-256 keypair used to sign authorization payloads. Paste this into your agent to set up Privy Agent CLI: ``` Set up https://agents.privy.io/skill.md ``` ::::steps ### Install ```bash [terminal] $ npm install -g @privy-io/agent-wallet-cli ``` ### Log in ```bash [terminal] $ privy-agent-wallets login ``` This opens a browser flow—complete Privy auth, approve signer access, then paste the credential back into the terminal. ### Fund wallets ```bash [terminal] $ privy-agent-wallets fund ``` ### List wallets ```bash [terminal] $ privy-agent-wallets list-wallets ``` ### Send a transaction ```bash [terminal] $ privy-agent-wallets rpc --json '{"method": "eth_sendTransaction", "params": {"to": "0xRecipient", "value": "0.01"}}' ``` :::: ## AgentCash [AgentCash](https://agentcash.dev) gives agents instant access to 300+ premium APIs for data enrichment, social data, image generation, web scraping, email, and much more, all through one USDC.e balance. Paste this into your agent to set up AgentCash: ``` Set up agentcash.dev/skill.md ``` ::::steps ### Onboard and get free credits Visit [agentcash.dev/onboard](https://agentcash.dev/onboard) to claim a sign-up bonus, then redeem in your terminal: ```bash [terminal] $ npx agentcash onboard ``` ### Search for services ```bash [terminal] $ npx agentcash search "image generation" ``` ### Use a service ```bash [terminal] $ npx agentcash fetch https://stableenrich.dev/api/exa/search \ --method POST \ --body '{"query":"agentcash.dev"}' ``` ### Install as MCP server (optional) ```bash [terminal] $ claude mcp add agentcash --scope user -- npx -y agentcash@latest ``` :::: ## mppx The [`mppx`](/sdk/typescript/cli) CLI is a lightweight MPP client bundled with the `mppx` package. It is designed for simple use cases and debugging during development. ::::steps ### Install :::code-group ```bash [npm] $ npm install -g mppx ``` ```bash [pnpm] $ pnpm add -g mppx ``` ```bash [bun] $ bun add -g mppx ``` ::: ### Create an account ```bash [terminal] $ mppx account create ``` ### Make a paid request ```bash [terminal] $ mppx https://mpp.dev/api/ping/paid ``` :::: ## Next steps # Use with your app \[Handle payment-gated resources automatically] ## Overview Polyfill the global `fetch` to handle `402` responses. Your existing code works unchanged—payments happen in the background. Pick the path that suits you: * [**Prompt mode**](#prompt-mode): paste a prompt into your coding agent for fast setup * [**Manual mode**](#manual-mode): step-by-step setup with `mppx/client` ## Prompt mode Paste this into your coding agent to set up your client with `mppx` in one prompt: ## Manual mode ::::steps ### Install dependencies :::code-group ```bash [npm] $ npm install mppx viem ``` ```bash [pnpm] $ pnpm add mppx viem ``` ```bash [bun] $ bun add mppx viem ``` ::: ### Define an account ```ts twoslash import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0xabc…123') ``` :::tip With Tempo, you can also use [Passkey or WebCrypto accounts](https://viem.sh/tempo/accounts). ::: ### Create payment handler Call `Mppx.create` at startup. This polyfills the global `fetch` to automatically handle `402` payment challenges. ```ts twoslash import { privateKeyToAccount } from 'viem/accounts' import { Mppx, tempo } from 'mppx/client' // [!code hl] const account = privateKeyToAccount('0xabc…123') Mppx.create({ // [!code hl] methods: [tempo({ account })], // [!code hl] }) // [!code hl] ``` :::tip If you want to avoid polyfilling, use the bound `fetch` instead. ```ts const mppx = Mppx.create({ polyfill: false, // [!code hl] methods: [tempo({ account })] }) const response = await mppx.fetch('https://mpp.dev/api/ping/paid') // [!code hl] ``` ::: ### Request protected resources Use `fetch`. Payment happens when a server returns `402`. ```ts const response = await fetch('https://mpp.dev/api/ping/paid') ``` :::: ## Learn more ### Wagmi You can inject a [Wagmi](https://wagmi.sh) connector into Mppx by passing the `getConnectorClient` function. :::code-group ```ts twoslash [example.ts] import { createConfig, http } from 'wagmi' import { getConnectorClient } from 'wagmi/actions' import { tempoModerato } from 'viem/chains' import { Mppx, tempo } from 'mppx/client' declare const connectors: Parameters[0]['connectors'] // ---cut--- const config = createConfig({ connectors, chains: [tempoModerato], transports: { [tempoModerato.id]: http(), }, }) Mppx.create({ methods: [tempo({ getClient: (parameters) => getConnectorClient(config, parameters as any), })], }) ``` ```ts twoslash [config.ts] import { createConfig, http } from 'wagmi' import { webAuthn } from 'wagmi/tempo' import { tempoModerato } from 'viem/chains' export const config = createConfig({ chains: [tempoModerato], connectors: [ webAuthn({ authUrl: 'https://accounts.tempo.xyz', }), ], transports: { [tempoModerato.id]: http(), }, }) ``` ::: ### Per-request accounts Pass accounts on individual requests instead of at setup: ```ts twoslash import { privateKeyToAccount } from 'viem/accounts' import { Mppx, tempo } from 'mppx/client' const mppx = Mppx.create({ polyfill: false, methods: [tempo()] }) const response = await mppx.fetch('https://mpp.dev/api/ping/paid', { // [!code hl:start] context: { account: privateKeyToAccount('0xabc…123'), } // [!code hl:end] }) ``` ### Manual payment handling Use `Mppx.create` for full control over the payment flow: * Present payment UI before paying * Implement custom retry logic * Handle credentials manually ```ts twoslash import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const mppx = Mppx.create({ // [!code hl:start] polyfill: false, // [!code hl:end] methods: [tempo()], }) // [!code hl:start] const response = await fetch('https://mpp.dev/api/ping/paid') if (response.status === 402) { const credential = await mppx.createCredential(response, { account: privateKeyToAccount('0x...'), }) const paidResponse = await fetch('https://mpp.dev/api/ping/paid', { headers: { Authorization: credential }, }) } // [!code hl:end] ``` ### Payment receipts On success, the server returns a `Payment-Receipt` header: ```ts import { Receipt } from 'mppx' const response = await fetch('https://mpp.dev/api/ping/paid') const receipt = Receipt.fromResponse(response) // [!code hl] console.log(receipt.status) // @log: success console.log(receipt.reference) // @log: 0xtx789abc... console.log(receipt.timestamp) // @log: 2025-01-15T12:00:00Z ``` ## Next steps # Accept one-time payments \[Charge per request with a payment-gated API] Build a payment-gated image generation API that charges $0.01 per request using `mppx`. The server returns a random photo from [Picsum](https://picsum.photos) behind a paywall, but you could swap in an AI model like [OpenAI Image Generation](https://developers.openai.com/api/reference/resources/images) instead. ## Demo Try the payment-gated image generation API. Click **Run demo** to create a wallet, fund it, and make a paid request.
## Prompt mode Paste this into your coding agent to build the entire guide in one prompt: {`Use https://mpp.dev/guides/one-time-payments.md as reference. Add mppx to my app with a payment-gated photo endpoint that charges $0.01 per request using the Tempo payment method with PathUSD. When payment is verified, fetch a random photo from https://picsum.photos/1024/1024 and return the URL as JSON.`} ## Manual mode Select your framework to follow a step-by-step guide. If your framework isn't listed, choose **Other** for a generic [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) approach compatible with most TypeScript server frameworks. ::::steps ### Install `mppx` :::code-group ```bash [npm] $ npm install mppx viem ``` ```bash [pnpm] $ pnpm add mppx viem ``` ```bash [bun] $ bun add mppx viem ``` ::: ### Set up `Mppx` instance Set up an `Mppx` instance with the `tempo` method. * `recipient` is the address where you receive payments. * `currency` is the token address for payments (in this case, `pathUSD`). ```ts [app/api/photo/route.ts] import { Mppx, tempo } from 'mppx/nextjs' export const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) ``` ### Create the `/api/photo` route Create the photo route. This route is **currently unpaid**. ```ts [app/api/photo/route.ts] import crypto from 'crypto' import { Mppx, tempo } from 'mppx/nextjs' export const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) // [!code focus:start] export const GET = async () => { const res = await fetch('https://picsum.photos/1024/1024') return Response.json({ url: res.url }) } // [!code focus:end] ``` ### Add `.charge` to the route handler Add payment verification using `mppx.charge` as route middleware. The handler runs only after payment is verified. ```ts [app/api/photo/route.ts] import crypto from 'crypto' import { Mppx, tempo } from 'mppx/nextjs' export const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) // [!code focus:start] export const GET = mppx.charge({ amount: '0.01', description: 'Random stock photo' }) // [!code ++] (async () => { const res = await fetch('https://picsum.photos/1024/1024') return Response.json({ url: res.url }) }) // [!code focus:end] ``` ### Test via the `mppx` CLI ```bash [terminal] # Create account funded with testnet tokens $ npx mppx account create # Make a paid request $ npx mppx http://localhost:3000/api/photo ``` :::: ::::steps ### Install `mppx` and `hono` :::code-group ```bash [npm] $ npm install mppx hono viem ``` ```bash [pnpm] $ pnpm add mppx hono viem ``` ```bash [bun] $ bun add mppx hono viem ``` ::: ### Set up `Mppx` instance Set up an `Mppx` instance with the `tempo` method. * `recipient` is the address where you receive payments. * `currency` is the token address for payments (in this case, `pathUSD`). ```ts [server.ts] import { Hono } from 'hono' import { Mppx, tempo } from 'mppx/hono' const app = new Hono() const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) ``` ### Create the `/api/photo` route Create the photo route. This route is **currently unpaid**. ```ts [server.ts] import crypto from 'crypto' import { Hono } from 'hono' import { Mppx, tempo } from 'mppx/hono' const app = new Hono() const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) // [!code focus:start] app.get('/api/photo', async (c) => { const res = await fetch('https://picsum.photos/1024/1024') return c.json({ url: res.url }) }) // [!code focus:end] ``` ### Add `.charge` to the route handler Add payment verification using `mppx.charge` as route middleware. The handler runs only after payment is verified. ```ts [server.ts] import crypto from 'crypto' import { Hono } from 'hono' import { Mppx, tempo } from 'mppx/hono' const app = new Hono() const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) // [!code focus:start] app.get( '/api/photo', mppx.charge({ amount: '0.01', description: 'Random stock photo' }), // [!code ++] async (c) => { const res = await fetch('https://picsum.photos/1024/1024') return c.json({ url: res.url }) }, ) // [!code focus:end] ``` ### Test via the `mppx` CLI ```bash [terminal] # Create account funded with testnet tokens $ npx mppx account create # Make a paid request $ npx mppx http://localhost:3000/api/photo ``` :::: ::::steps ### Install `mppx` :::code-group ```bash [npm] $ npm install mppx viem ``` ```bash [pnpm] $ pnpm add mppx viem ``` ```bash [bun] $ bun add mppx viem ``` ::: ### Set up `Mppx` instance Set up an `Mppx` instance with the `tempo` method. * `recipient` is the address where you receive payments. * `currency` is the token address for payments (in this case, `pathUSD`). ```ts [src/index.ts] import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) ``` ### Create the `/api/photo` route Create the photo route. This route is **currently unpaid**. ```ts [src/index.ts] import crypto from 'crypto' import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) // [!code focus:start] export default { async fetch(request: Request) { const res = await fetch('https://picsum.photos/1024/1024') return Response.json({ url: res.url }) }, } // [!code focus:end] ``` ### Add `.charge` to the route handler Add payment verification using `mppx.charge`. If the status is `402`, return the Challenge. Otherwise, fetch the photo and attach a Receipt to the response. ```ts [src/index.ts] import crypto from 'crypto' import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) // [!code focus:start] export default { async fetch(request: Request) { const result = await mppx.charge({ // [!code ++] amount: '0.01', // [!code ++] description: 'Random stock photo', // [!code ++] })(request) // [!code ++] if (result.status === 402) return result.challenge // [!code ++] const res = await fetch('https://picsum.photos/1024/1024') return result.withReceipt(Response.json({ url: res.url })) // [!code ++] }, } // [!code focus:end] ``` ### Test via the `mppx` CLI ```bash [terminal] # Create account funded with testnet tokens $ npx mppx account create # Make a paid request $ npx mppx http://localhost:8787 ``` :::: ::::steps ### Install `mppx` and `express` :::code-group ```bash [npm] $ npm install mppx express viem ``` ```bash [pnpm] $ pnpm add mppx express viem ``` ```bash [bun] $ bun add mppx express viem ``` ::: ### Set up `Mppx` instance Set up an `Mppx` instance with the `tempo` method. * `recipient` is the address where you receive payments. * `currency` is the token address for payments (in this case, `pathUSD`). ```ts [server.ts] import express from 'express' import { Mppx, tempo } from 'mppx/express' const app = express() const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) ``` ### Create the `/api/photo` route Create the photo route. This route is **currently unpaid**. ```ts [server.ts] import crypto from 'crypto' import express from 'express' import { Mppx, tempo } from 'mppx/express' const app = express() const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) // [!code focus:start] app.get('/api/photo', async (req, res) => { const response = await fetch('https://picsum.photos/1024/1024') res.json({ url: response.url }) }) // [!code focus:end] ``` ### Add `.charge` to the route handler Add payment verification using `mppx.charge` as route middleware. The handler runs only after payment is verified. ```ts [server.ts] import crypto from 'crypto' import express from 'express' import { Mppx, tempo } from 'mppx/express' const app = express() const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) // [!code focus:start] app.get( '/api/photo', mppx.charge({ amount: '0.01', description: 'Random stock photo' }), // [!code ++] async (req, res) => { const response = await fetch('https://picsum.photos/1024/1024') res.json({ url: response.url }) }, ) // [!code focus:end] ``` ### Test via the `mppx` CLI ```bash [terminal] # Create account funded with testnet tokens $ npx mppx account create # Make a paid request $ npx mppx http://localhost:3000/api/photo ``` :::: This guide walks through using `mppx/server` directly with any [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)-compatible framework: [Bun](https://bun.sh), [Deno](https://deno.com), [Cloudflare Workers](https://workers.dev), and others.
::::steps ### Install `mppx` :::code-group ```bash [npm] $ npm install mppx viem ``` ```bash [pnpm] $ pnpm add mppx viem ``` ```bash [bun] $ bun add mppx viem ``` ::: ### Set up `Mppx` instance Set up an `Mppx` instance with the `tempo` method. * `recipient` is the address where you receive payments. * `currency` is the token address for payments (in this case, `pathUSD`). ```ts [server.ts] import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) ``` ### Create the `/api/photo` route Create the photo route. This route is **currently unpaid**. ```ts [server.ts] import crypto from 'crypto' import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) // [!code focus:start] Bun.serve({ async fetch(request) { const res = await fetch('https://picsum.photos/1024/1024') return Response.json({ url: res.url }) }, }) // [!code focus:end] ``` ### Add `.charge` to the route handler Add payment verification using `mppx.charge`. If the status is `402`, return the Challenge. Otherwise, fetch the photo and attach a Receipt to the response. ```ts [server.ts] import crypto from 'crypto' import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) // [!code focus:start] Bun.serve({ async fetch(request) { const result = await mppx.charge({ // [!code ++] amount: '0.01', // [!code ++] description: 'Random stock photo', // [!code ++] })(request) // [!code ++] if (result.status === 402) return result.challenge // [!code ++] const res = await fetch('https://picsum.photos/1024/1024') return result.withReceipt(Response.json({ url: res.url })) // [!code ++] }, }) // [!code focus:end] ``` ### Test via the `mppx` CLI ```bash [terminal] # Create account funded with testnet tokens $ npx mppx account create # Make a paid request $ npx mppx http://localhost:3000 ``` :::: ## With Stripe Accept MPP payments through Stripe for refunds, reporting, and multi-currency payouts. Read the [Stripe documentation](https://docs.stripe.com/payments/machine/mpp) for the full integration walkthrough. ## Next steps # Accept pay-as-you-go payments \[Session-based billing with payment channels] Build a payment-gated photo gallery API that charges $0.01 per photo using `mppx` sessions. The server returns random photos from [Picsum](https://picsum.photos) behind a paywall, but you could imagine generating images with an AI model instead like [OpenAI Image Generation](https://developers.openai.com/api/reference/resources/images). :::info Unlike [one-time payments](/guides/one-time-payments), sessions open a payment channel once and use off-chain vouchers for each subsequent request—vouchers are **not bottlenecked by blockchain throughput**, they are processed in pure CPU-bound signature checks. ::: ## Demo Try the payment-gated photo gallery API. Click **Run demo** to create a wallet, fund it, and generate a gallery of paid photos.
## Prompt mode Paste this into your coding agent to build the entire guide in one prompt: {`Use https://mpp.dev/guides/pay-as-you-go.md as reference. Add mppx to my app with a payment-gated gallery endpoint that charges $0.01 per photo using the Tempo session payment method with PathUSD. When payment is verified, fetch a random photo from https://picsum.photos/200/200 and return the URL as JSON.`} ## Manual mode Select your framework to follow a step-by-step guide. If your framework isn't listed, choose **Other** for a generic [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) approach compatible with most TypeScript server frameworks. ::::steps ### Install `mppx` :::code-group ```bash [npm] $ npm install mppx viem ``` ```bash [pnpm] $ pnpm add mppx viem ``` ```bash [bun] $ bun add mppx viem ``` ::: ### Set up `Mppx` instance Set up an `Mppx` instance with the `tempo` method. * `recipient` is the address where you receive payments. * `currency` is the token address for payments (in this case, `pathUSD`). ```ts [app/api/sessions/photo/route.ts] import crypto from 'crypto' import { Mppx, tempo } from 'mppx/nextjs' export const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) ``` ### Create the `/api/sessions/photo` route Create the gallery route. This route is **currently unpaid**. ```ts [app/api/sessions/photo/route.ts] import crypto from 'crypto' import { Mppx, tempo } from 'mppx/nextjs' export const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) // [!code focus:start] export const GET = async () => { const res = await fetch('https://picsum.photos/200/200') return Response.json({ url: res.url }) } // [!code focus:end] ``` ### Add `.session` to the route handler Add payment verification using `mppx.session` as route middleware. The handler runs only after payment is verified. ```ts [app/api/sessions/photo/route.ts] import crypto from 'crypto' import { Mppx, tempo } from 'mppx/nextjs' export const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) // [!code focus:start] export const GET = mppx.session({ amount: '0.01', unitType: 'photo' }) // [!code ++] (async () => { const res = await fetch('https://picsum.photos/200/200') return Response.json({ url: res.url }) }) // [!code focus:end] ``` ### Test via the `mppx` CLI ```bash [terminal] # Create account funded with testnet tokens $ npx mppx account create # Make a paid request $ npx mppx http://localhost:3000/api/sessions/photo ``` :::: ::::steps ### Install `mppx` and `hono` :::code-group ```bash [npm] $ npm install mppx hono viem ``` ```bash [pnpm] $ pnpm add mppx hono viem ``` ```bash [bun] $ bun add mppx hono viem ``` ::: ### Set up `Mppx` instance Set up an `Mppx` instance with the `tempo` method. * `recipient` is the address where you receive payments. * `currency` is the token address for payments (in this case, `pathUSD`). ```ts [server.ts] import crypto from 'crypto' import { Hono } from 'hono' import { Mppx, tempo } from 'mppx/hono' const app = new Hono() const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) ``` ### Create the `/api/sessions/photo` route Create the gallery route. This route is **currently unpaid**. ```ts [server.ts] import crypto from 'crypto' import { Hono } from 'hono' import { Mppx, tempo } from 'mppx/hono' const app = new Hono() const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) // [!code focus:start] app.get('/api/sessions/photo', async (c) => { const res = await fetch('https://picsum.photos/200/200') return c.json({ url: res.url }) }) // [!code focus:end] ``` ### Add `.session` to the route handler Add payment verification using `mppx.session` as route middleware. The handler runs only after payment is verified. ```ts [server.ts] import crypto from 'crypto' import { Hono } from 'hono' import { Mppx, tempo } from 'mppx/hono' const app = new Hono() const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) // [!code focus:start] app.get( '/api/sessions/photo', mppx.session({ amount: '0.01', unitType: 'photo' }), // [!code ++] async (c) => { const res = await fetch('https://picsum.photos/200/200') return c.json({ url: res.url }) }, ) // [!code focus:end] ``` ### Test via the `mppx` CLI ```bash [terminal] # Create account funded with testnet tokens $ npx mppx account create # Make a paid request $ npx mppx http://localhost:3000/api/sessions/photo ``` :::: ::::steps ### Install `mppx` :::code-group ```bash [npm] $ npm install mppx viem ``` ```bash [pnpm] $ pnpm add mppx viem ``` ```bash [bun] $ bun add mppx viem ``` ::: ### Set up `Mppx` instance Set up an `Mppx` instance with the `tempo` method. * `recipient` is the address where you receive payments. * `currency` is the token address for payments (in this case, `pathUSD`). ```ts [src/index.ts] import crypto from 'crypto' import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) ``` ### Create the gallery route Create the gallery route. This route is **currently unpaid**. ```ts [src/index.ts] import crypto from 'crypto' import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) // [!code focus:start] export default { async fetch(request: Request) { const res = await fetch('https://picsum.photos/200/200') return Response.json({ url: res.url }) }, } // [!code focus:end] ``` ### Add `.session` to the route handler Add payment verification using `mppx.session`. If the status is `402`, return the Challenge. Otherwise, fetch the photo and attach a Receipt to the response. ```ts [src/index.ts] import crypto from 'crypto' import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) // [!code focus:start] export default { async fetch(request: Request) { const result = await mppx.session({ // [!code ++] amount: '0.01', // [!code ++] unitType: 'photo', // [!code ++] })(request) // [!code ++] if (result.status === 402) return result.challenge // [!code ++] const res = await fetch('https://picsum.photos/200/200') return result.withReceipt(Response.json({ url: res.url })) // [!code ++] }, } // [!code focus:end] ``` ### Test via the `mppx` CLI ```bash [terminal] # Create account funded with testnet tokens $ npx mppx account create # Make a paid request $ npx mppx http://localhost:8787 ``` :::: ::::steps ### Install `mppx` and `express` :::code-group ```bash [npm] $ npm install mppx express viem ``` ```bash [pnpm] $ pnpm add mppx express viem ``` ```bash [bun] $ bun add mppx express viem ``` ::: ### Set up `Mppx` instance Set up an `Mppx` instance with the `tempo` method. * `recipient` is the address where you receive payments. * `currency` is the token address for payments (in this case, `pathUSD`). ```ts [server.ts] import crypto from 'crypto' import express from 'express' import { Mppx, tempo } from 'mppx/express' const app = express() const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) ``` ### Create the `/api/sessions/photo` route Create the gallery route. This route is **currently unpaid**. ```ts [server.ts] import crypto from 'crypto' import express from 'express' import { Mppx, tempo } from 'mppx/express' const app = express() const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) // [!code focus:start] app.get('/api/sessions/photo', async (req, res) => { const response = await fetch('https://picsum.photos/200/200') res.json({ url: response.url }) }) // [!code focus:end] ``` ### Add `.session` to the route handler Add payment verification using `mppx.session` as route middleware. The handler runs only after payment is verified. ```ts [server.ts] import crypto from 'crypto' import express from 'express' import { Mppx, tempo } from 'mppx/express' const app = express() const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) // [!code focus:start] app.get( '/api/sessions/photo', mppx.session({ amount: '0.01', unitType: 'photo' }), // [!code ++] async (req, res) => { const response = await fetch('https://picsum.photos/200/200') res.json({ url: response.url }) }, ) // [!code focus:end] ``` ### Test via the `mppx` CLI ```bash [terminal] # Create account funded with testnet tokens $ npx mppx account create # Make a paid request $ npx mppx http://localhost:3000/api/sessions/photo ``` :::: This guide walks through using `mppx/server` directly with any [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)-compatible framework: [Bun](https://bun.sh), [Deno](https://deno.com), [Cloudflare Workers](https://workers.dev), and others.
::::steps ### Install `mppx` :::code-group ```bash [npm] $ npm install mppx viem ``` ```bash [pnpm] $ pnpm add mppx viem ``` ```bash [bun] $ bun add mppx viem ``` ::: ### Set up `Mppx` instance Set up an `Mppx` instance with the `tempo` method. * `recipient` is the address where you receive payments. * `currency` is the token address for payments (in this case, `pathUSD`). ```ts [server.ts] import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) ``` ### Create the `/api/sessions/photo` route Create the gallery route. This route is **currently unpaid**. ```ts [server.ts] import crypto from 'crypto' import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) // [!code focus:start] Bun.serve({ async fetch(request) { const res = await fetch('https://picsum.photos/200/200') return Response.json({ url: res.url }) }, }) // [!code focus:end] ``` ### Add `.session` to the route handler Add payment verification using `mppx.session`. If the status is `402`, return the Challenge. Otherwise, fetch the photo and attach a Receipt to the response. ```ts [server.ts] import crypto from 'crypto' import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) // [!code focus:start] Bun.serve({ async fetch(request) { const result = await mppx.session({ // [!code ++] amount: '0.01', // [!code ++] unitType: 'photo', // [!code ++] })(request) // [!code ++] if (result.status === 402) return result.challenge // [!code ++] const res = await fetch('https://picsum.photos/200/200') return result.withReceipt(Response.json({ url: res.url })) // [!code ++] }, }) // [!code focus:end] ``` ### Test via the `mppx` CLI ```bash [terminal] # Create account funded with testnet tokens $ npx mppx account create # Make a paid request $ npx mppx http://localhost:3000 ``` :::: ## Client setup When using sessions from a client, set `maxDeposit` to enable automatic channel management. This is the maximum amount of tokens the client locks into the payment channel's escrow contract. Any unspent deposit is refunded when the channel closes. ```ts [client.ts] import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const mppx = Mppx.create({ methods: [tempo({ account: privateKeyToAccount('0x...'), maxDeposit: '1', // Lock up to 1 pathUSD per channel })], }) // Each fetch automatically manages the session lifecycle: // 1st request: opens channel on-chain, sends initial voucher // 2nd+ requests: sends off-chain vouchers (no on-chain tx) const res = await fetch('http://localhost:3000/api/sessions/photo') ``` * **`maxDeposit: '1'`**: Locks up to 1 pathUSD into the payment channel. At $0.01/photo, this covers up to 100 requests before the channel runs out. * The client handles the full session lifecycle automatically: channel open, voucher signing, and retry after `402` responses. * If the server sets `suggestedDeposit`, the client uses `min(suggestedDeposit, maxDeposit)`. ### Closing the channel After you're done making requests, close the channel to settle on-chain and reclaim unspent deposit: ```ts twoslash [client.ts] import { tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const session = tempo.session({ account: privateKeyToAccount('0x...'), maxDeposit: '1', }) const res = await session.fetch('http://localhost:3000/api/sessions/photo') // Settle on-chain and reclaim unspent deposit const receipt = await session.close() ``` :::info Channels remain open for reuse. Closing is not required between individual requests—only when you're done with the session entirely. ::: ## Next steps # Accept streamed payments \[Per-token billing over Server-Sent Events] Build a payment-gated poetry API that streams poems word-by-word and charges $0.001 per word using `mppx` sessions with Server-Sent Events (SSE). :::info Streamed payments extend [pay-as-you-go sessions](/guides/pay-as-you-go) with SSE. The server charges per token as content streams—if the channel balance runs out mid-stream, the client automatically sends a new voucher and the stream resumes. ::: ## Demo Try the payment-gated poetry API. Click **Run demo** to create a wallet, fund it, and stream a paid poem.
## Prompt mode Paste this into your coding agent to build the entire guide in one prompt: {`Use https://mpp.dev/guides/streamed-payments.md as reference. Add mppx to my app with a payment-gated SSE endpoint that streams text word-by-word and charges $0.001 per word using the Tempo session payment method with PathUSD and sse: true.`} ## Manual mode Select your framework to follow a step-by-step guide. If your framework isn't listed, choose **Other** for a generic [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) approach compatible with most TypeScript server frameworks. ::::steps ### Install `mppx` :::code-group ```bash [npm] $ npm install mppx viem ``` ```bash [pnpm] $ pnpm add mppx viem ``` ```bash [bun] $ bun add mppx viem ``` ::: ### Set up `Mppx` instance with streaming Set up an `Mppx` instance with `sse: true` to enable SSE support on the session method. ```ts [app/api/sessions/poem/route.ts] import crypto from 'crypto' import { Mppx, tempo } from "mppx/nextjs"; export const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [ tempo({ testnet: true, currency: "0x20c0000000000000000000000000000000000000", recipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", sse: true, }), ], }); ``` ### Create the `/api/sessions/poem` route Create the poem route. The `withReceipt` method accepts an async generator—each yielded value is one SSE event and one charged word. ```ts [app/api/sessions/poem/route.ts] import crypto from 'crypto' import { Mppx, tempo } from "mppx/nextjs"; export const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [ tempo({ testnet: true, currency: "0x20c0000000000000000000000000000000000000", recipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", sse: true, }), ], }); // [!code focus:start] const poem = { title: "The Road Not Taken", author: "Robert Frost", lines: [ "Two roads diverged in a yellow wood,", "And sorry I could not travel both", "And be one traveler, long I stood", "And looked down one as far as I could", "To where it bent in the undergrowth;", ], }; export const GET = mppx.session({ amount: "0.001", unitType: "word" })( async () => { const words = poem.lines.flatMap((line) => [...line.split(" "), "\\n"]); return async function* (stream) { yield JSON.stringify({ title: poem.title, author: poem.author }); for (const word of words) { await stream.charge(); yield word; } }; }, ); // [!code focus:end] ``` ### Test via the `mppx` CLI ```bash [terminal] # Create account funded with testnet tokens $ npx mppx account create # Stream a paid poem $ npx mppx http://localhost:3000/api/sessions/poem ``` :::: ::::steps ### Install `mppx` and `hono` :::code-group ```bash [npm] $ npm install mppx hono viem ``` ```bash [pnpm] $ pnpm add mppx hono viem ``` ```bash [bun] $ bun add mppx hono viem ``` ::: ### Set up `Mppx` instance with streaming Set up an `Mppx` instance with `sse: true` to enable SSE support on the session method. ```ts [server.ts] import crypto from 'crypto' import { Hono } from "hono"; import { Mppx, tempo } from "mppx/hono"; const app = new Hono(); const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [ tempo({ testnet: true, currency: "0x20c0000000000000000000000000000000000000", recipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", sse: true, }), ], }); ``` ### Create the `/api/sessions/poem` route Create the poem route with the session middleware. The handler returns an async generator—each yielded value is one SSE event and one charged word. ```ts [server.ts] import crypto from 'crypto' import { Hono } from "hono"; import { Mppx, tempo } from "mppx/hono"; const app = new Hono(); const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [ tempo({ testnet: true, currency: "0x20c0000000000000000000000000000000000000", recipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", sse: true, }), ], }); // [!code focus:start] const poem = { title: "The Road Not Taken", author: "Robert Frost", lines: [ "Two roads diverged in a yellow wood,", "And sorry I could not travel both", "And be one traveler, long I stood", "And looked down one as far as I could", "To where it bent in the undergrowth;", ], }; app.get( "/api/sessions/poem", mppx.session({ amount: "0.001", unitType: "word" }), async (c) => { const words = poem.lines.flatMap((line) => [...line.split(" "), "\\n"]); return async function* (stream) { yield JSON.stringify({ title: poem.title, author: poem.author }); for (const word of words) { await stream.charge(); yield word; } }; }, ); // [!code focus:end] ``` ### Test via the `mppx` CLI ```bash [terminal] # Create account funded with testnet tokens $ npx mppx account create # Stream a paid poem $ npx mppx http://localhost:3000/api/sessions/poem ``` :::: ::::steps ### Install `mppx` :::code-group ```bash [npm] $ npm install mppx viem ``` ```bash [pnpm] $ pnpm add mppx viem ``` ```bash [bun] $ bun add mppx viem ``` ::: ### Set up `Mppx` instance with streaming Set up an `Mppx` instance with `sse: true` to enable SSE support on the session method. ```ts [src/index.ts] import crypto from 'crypto' import { Mppx, tempo } from "mppx/server"; const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [ tempo({ testnet: true, currency: "0x20c0000000000000000000000000000000000000", recipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", sse: true, }), ], }); ``` ### Create the `/api/sessions/poem` route Create the poem route. The `withReceipt` method accepts an async generator—each yielded value is one SSE event and one charged word. ```ts [src/index.ts] import crypto from 'crypto' import { Mppx, tempo } from "mppx/server"; const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [ tempo({ testnet: true, currency: "0x20c0000000000000000000000000000000000000", recipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", sse: true, }), ], }); // [!code focus:start] const poem = { title: "The Road Not Taken", author: "Robert Frost", lines: [ "Two roads diverged in a yellow wood,", "And sorry I could not travel both", "And be one traveler, long I stood", "And looked down one as far as I could", "To where it bent in the undergrowth;", ], }; export default { async fetch(request: Request) { const result = await mppx.session({ amount: "0.001", unitType: "word", })(request); if (result.status === 402) return result.challenge; const words = poem.lines.flatMap((line) => [...line.split(" "), "\\n"]); return result.withReceipt(async function* (stream) { yield JSON.stringify({ title: poem.title, author: poem.author }); for (const word of words) { await stream.charge(); yield word; } }); }, }; // [!code focus:end] ``` ### Test via the `mppx` CLI ```bash [terminal] # Create account funded with testnet tokens $ npx mppx account create # Stream a paid poem $ npx mppx http://localhost:8787 ``` :::: ::::steps ### Install `mppx` and `express` :::code-group ```bash [npm] $ npm install mppx express viem ``` ```bash [pnpm] $ pnpm add mppx express viem ``` ```bash [bun] $ bun add mppx express viem ``` ::: ### Set up `Mppx` instance with streaming Set up an `Mppx` instance with `sse: true` to enable SSE support on the session method. ```ts [server.ts] import crypto from 'crypto' import express from "express"; import { Mppx, tempo } from "mppx/express"; const app = express(); const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [ tempo({ testnet: true, currency: "0x20c0000000000000000000000000000000000000", recipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", sse: true, }), ], }); ``` ### Create the `/api/sessions/poem` route Create the poem route with the session middleware. The handler returns an async generator—each yielded value is one SSE event and one charged word. ```ts [server.ts] import crypto from 'crypto' import express from "express"; import { Mppx, tempo } from "mppx/express"; const app = express(); const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [ tempo({ testnet: true, currency: "0x20c0000000000000000000000000000000000000", recipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", sse: true, }), ], }); // [!code focus:start] const poem = { title: "The Road Not Taken", author: "Robert Frost", lines: [ "Two roads diverged in a yellow wood,", "And sorry I could not travel both", "And be one traveler, long I stood", "And looked down one as far as I could", "To where it bent in the undergrowth;", ], }; app.get( "/api/sessions/poem", mppx.session({ amount: "0.001", unitType: "word" }), async (req, res) => { const words = poem.lines.flatMap((line) => [...line.split(" "), "\\n"]); return async function* (stream) { yield JSON.stringify({ title: poem.title, author: poem.author }); for (const word of words) { await stream.charge(); yield word; } }; }, ); // [!code focus:end] ``` ### Test via the `mppx` CLI ```bash [terminal] # Create account funded with testnet tokens $ npx mppx account create # Stream a paid poem $ npx mppx http://localhost:3000/api/sessions/poem ``` :::: This guide walks through using `mppx/server` directly with any [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)-compatible framework: [Bun](https://bun.sh), [Deno](https://deno.com), [Cloudflare Workers](https://workers.dev), and others.
::::steps ### Install `mppx` :::code-group ```bash [npm] $ npm install mppx viem ``` ```bash [pnpm] $ pnpm add mppx viem ``` ```bash [bun] $ bun add mppx viem ``` ::: ### Set up `Mppx` instance with streaming Set up an `Mppx` instance with `sse: true` to enable SSE support on the session method. ```ts [server.ts] import crypto from 'crypto' import { Mppx, tempo } from "mppx/server"; const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [ tempo({ testnet: true, currency: "0x20c0000000000000000000000000000000000000", recipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", sse: true, }), ], }); ``` ### Create the streaming poem route Create the route handler. `withReceipt` accepts an async generator—each yielded value becomes one SSE `event: message` and is charged one tick (`$0.001`). If the channel balance runs out mid-stream, the server emits `event: payment-need-voucher` and pauses until the client sends a new voucher. ```ts [server.ts] import crypto from 'crypto' import { Mppx, tempo } from "mppx/server"; const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [ tempo({ testnet: true, currency: "0x20c0000000000000000000000000000000000000", recipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", sse: true, }), ], }); // [!code focus:start] const poem = { title: "The Road Not Taken", author: "Robert Frost", lines: [ "Two roads diverged in a yellow wood,", "And sorry I could not travel both", "And be one traveler, long I stood", "And looked down one as far as I could", "To where it bent in the undergrowth;", ], }; Bun.serve({ async fetch(request) { const result = await mppx.session({ amount: "0.001", unitType: "word", })(request); if (result.status === 402) return result.challenge; const words = poem.lines.flatMap((line) => [...line.split(" "), "\\n"]); return result.withReceipt(async function* (stream) { yield JSON.stringify({ title: poem.title, author: poem.author }); for (const word of words) { await stream.charge(); yield word; } }); }, }); // [!code focus:end] ``` ### Test via the `mppx` CLI ```bash [terminal] # Create account funded with testnet tokens $ npx mppx account create # Stream a paid poem $ npx mppx http://localhost:3000 ``` :::: ## Client setup Use `tempo.session()` from `mppx/client` to create a session manager. The `.sse()` method connects to the SSE endpoint and handles voucher renewal automatically—if the server requests a new voucher mid-stream, the client signs and sends one without interrupting the stream. ```ts [client.ts] import { tempo } from "mppx/client"; import { privateKeyToAccount } from "viem/accounts"; const session = tempo.session({ account: privateKeyToAccount("0x..."), maxDeposit: "1", // Lock up to 1 pathUSD per channel }); // .sse() returns an async iterable of SSE data payloads const stream = await session.sse("http://localhost:3000/api/sessions/poem"); for await (const word of stream) { process.stdout.write(word + " "); } ``` * **`tempo.session()`** — Creates a session manager that handles the full channel lifecycle: open, voucher signing, and close. * **`.sse()`** — Connects to an SSE endpoint. Automatically sends new vouchers when the server emits `payment-need-voucher` events. * **`maxDeposit: '1'`** — Locks up to 1 pathUSD. At $0.001/word, this covers ~1,000 words before the channel needs a top-up. ### Closing the channel After streaming completes, close the channel to settle and reclaim unspent deposit: ```ts twoslash [client.ts] import { tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const session = tempo.session({ account: privateKeyToAccount('0x...'), maxDeposit: '1', }) const stream = await session.sse('http://localhost:3000/api/sessions/poem') for await (const word of stream) { process.stdout.write(word + ' ') } // Settle on-chain and reclaim unspent deposit const receipt = await session.close() ``` ## WebSocket alternative The examples above use Server-Sent Events (SSE) for streaming. If your use case benefits from a persistent bidirectional connection — for example, interactive chat or real-time sessions — you can stream over WebSocket instead. The session payment flow is the same (channel open, voucher signing, close), but vouchers and content travel over a single socket rather than separate HTTP requests. On the server, use [`tempo.Ws.serve()`](/sdk/typescript/server/Ws.serve) to bridge a WebSocket to the session payment flow. On the client, use [`session.ws()`](/sdk/typescript/client/Method.tempo.session-manager#sessionwsinput-init) instead of `session.sse()`. ## Next steps # Create and manage subscriptions \[Recurring access for paid APIs] Build a subscription-gated API that charges $1 for access using `mppx` and Tempo subscriptions. ## Overview Subscriptions separate access from billing. The client authorizes recurring access once, then keeps using the paid API without paying again on every request. Your server stores the subscription after the first payment succeeds. Later requests check that stored subscription and return the protected response immediately when access is active. Billing happens on its own schedule: the next request after a billing period ends can renew the subscription, or a background job can renew subscriptions before clients return. This keeps normal API requests simple while recurring payments happen asynchronously. >Server: GET /api/pro Server->>Store: Resolve subscription key Server-->>Client: 402 Challenge Client->>Server: Retry with keyAuthorization Credential Server->>Tempo: Charge first billing period Server->>Store: Store subscription record Server-->>Client: 200 OK + Receipt Client->>Server: GET /api/pro Server->>Store: Find active subscription Server-->>Client: 200 OK + Receipt `} /> ## Install `mppx` :::code-group ```bash [npm] $ npm install mppx viem ``` ```bash [pnpm] $ pnpm add mppx viem ``` ```bash [bun] $ bun add mppx viem ``` ::: ## Create the subscription method Create one `Mppx` instance and register `tempo.subscription()`. ```ts twoslash [mppx.server.ts] import { Mppx, Store, tempo } from 'mppx/server' const store = Store.memory() export const mppx = Mppx.create({ methods: [ tempo.subscription({ amount: '1.00', currency: '0x20c0000000000000000000000000000000000000', periodCount: '1', periodUnit: 'week', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', resolve: async ({ input }) => { const userId = input.headers.get('X-User-Id') return userId ? { key: `user:${userId}:plan:pro` } : null }, store, subscriptionExpires: new Date('2027-01-01T00:00:00.000Z'), testnet: true, }), ], }) ``` The `resolve` function maps each request to one subscription. Use the same key for every route that belongs to the same paid plan. :::warning Use a durable atomic store in production. `Store.memory()` loses subscription state when the process restarts. ::: ## Gate the API route Call `mppx.tempo.subscription({})` before returning paid data. ```ts twoslash [route.ts] import { Mppx, Store, tempo } from 'mppx/server' const store = Store.memory() const mppx = Mppx.create({ methods: [ tempo.subscription({ amount: '1.00', currency: '0x20c0000000000000000000000000000000000000', periodCount: '1', periodUnit: 'week', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', resolve: async ({ input }) => { const userId = input.headers.get('X-User-Id') return userId ? { key: `user:${userId}:plan:pro` } : null }, store, subscriptionExpires: new Date('2027-01-01T00:00:00.000Z'), testnet: true, }), ], }) // ---cut--- export async function GET(request: Request) { if (!request.headers.get('X-User-Id')) { return Response.json({ error: 'Unauthorized' }, { status: 401 }) } const result = await mppx.tempo.subscription({})(request) if (result.status === 402) return result.challenge return result.withReceipt( Response.json({ limits: { requests: 100_000 }, plan: 'pro', }), ) } ``` The first unpaid request returns `402`. After the client activates the subscription, the same route returns `200` with a `Payment-Receipt` header. ## Configure the client Register `tempo.subscription()` on the client. The SDK handles the `402` response, signs the key authorization, and retries the request. ```ts twoslash [client.ts] import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0xabc…123') Mppx.create({ methods: [tempo.subscription({ account })], }) const response = await fetch('https://api.example.com/api/pro', { headers: { 'X-User-Id': 'user_123' }, }) console.log(response.status) // @log: 200 ``` ## Advanced options ### Cancel a subscription Cancellation is a server-side state change. Have the client call your cancellation endpoint, then mark the active subscription record with `canceledAt`; the next paid request returns `402` and requires a new subscription activation. ```ts [client.ts] await fetch('/api/subscription/cancel', { method: 'POST', }) ``` ```ts twoslash [cancel.ts] import { Store } from 'mppx/server' import { Subscription } from 'mppx/tempo' const store = Store.memory() const subscriptions = Subscription.fromStore(store) export async function cancelSubscription(userId: string) { const subscription = await subscriptions.getByKey(`user:${userId}:plan:pro`) if (!subscription) return false await subscriptions.put({ ...subscription, canceledAt: new Date().toISOString(), }) return true } ``` Keep the canceled record instead of deleting it. That preserves receipts and prevents in-flight renewals from clearing the cancellation marker. ### Revoke the access key On Tempo, clients can also revoke the authorized access key. Do this after server cancellation when you want a wallet-level backstop against future renewals. ```ts twoslash [revoke.ts] import { createClient, http } from 'viem' import { tempo } from 'viem/chains' import { privateKeyToAccount } from 'viem/accounts' import { Actions } from 'viem/tempo' const client = createClient({ account: privateKeyToAccount( '0x0000000000000000000000000000000000000000000000000000000000000001', // your account ), chain: tempo, transport: http(), }) await Actions.accessKey.revokeSync(client, { accessKey: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', }) ``` Revoking the access key doesn't cancel the merchant-side subscription record. If the client only revokes the key, the server can still reuse an already-paid period until it tries to renew. ### Renew in the background Subscriptions renew when a request arrives after the next billing period starts. For proactive billing, run `tempo.renewSubscription()` from a background job. ```ts twoslash [renew.ts] import type { Store } from 'mppx/server' import { tempo } from 'mppx/server' declare const store: Store.AtomicStore> const result = await tempo.renewSubscription({ store, subscriptionId: 'sub_abc123', }) if (result) console.log(result.receipt.status) ``` ## Production checklist * Store subscription records in a durable atomic store. * Mark canceled subscriptions with `canceledAt` and keep their records for audit. * Use authenticated user or organization IDs in `resolve`. * Set `subscriptionExpires` to the maximum authorization lifetime you accept. * Add webhook or cron coverage for background renewals if access must stay warm. * Persist `subscriptionId` and `externalId` in your app database for support and reconciliation. ## Next steps * Read the [Tempo subscription overview](/payment-methods/tempo/subscription). * Review [`tempo.subscription` server API](/sdk/typescript/server/Method.tempo.subscription). * Review [`tempo.subscription` client API](/sdk/typescript/client/Method.tempo.subscription). # Accept card payments \[Charge Visa and Mastercard via Stripe] Accept card payments on your MPP-enabled API using [Stripe](/payment-methods/stripe). Clients pay with Visa, Mastercard, and other card networks—no stablecoin wallet required. The server uses Stripe's [Shared Payment Tokens (SPTs)](https://docs.stripe.com/agentic-commerce/concepts/shared-payment-tokens) to process payments through Stripe's existing rails. ::::info\[Stripe account setup] Machine payments must be enabled on your Stripe account before you can accept MPP payments. [Request access](https://docs.stripe.com/payments/machine#sign-up) through the Stripe Dashboard. :::: ## How it works >Server: (1) GET /resource Server-->>Client: (2) 402 + Challenge (Stripe method) Client->>Stripe: (3) Create SPT from Challenge Stripe-->>Client: (4) spt_... Client->>Server: (5) GET /resource + Credential (SPT) Server->>Stripe: (6) Create PaymentIntent Stripe-->>Server: (7) pi_... Server-->>Client: (8) 200 OK + Receipt `} /> 1. **Client** requests a paid resource. 2. **Server** responds with `402` and a Challenge containing the price, currency, and Stripe method details. 3. **Client** creates a Shared Payment Token (SPT) through the Stripe API. 4. **Client** retries the request with a Credential containing the SPT. 5. **Server** creates a Stripe `PaymentIntent` using the SPT, verifies payment, and returns the resource with a Receipt. Settlement, refunds, and reporting all happen through your Stripe Dashboard—the same tools you use for any other Stripe payment. ## Prompt mode Paste this into your coding agent to build the entire guide in one prompt: {`Use https://mpp.dev/guides/accept-card-payments.md as reference. Add mppx to my app with a payment-gated endpoint that accepts card payments via Stripe. Charge $1.00 per request using the Stripe payment method. When payment is verified, return a JSON response.`} ## Server setup ::::steps ### Install dependencies :::code-group ```bash [npm] $ npm install mppx stripe viem ``` ```bash [pnpm] $ pnpm add mppx stripe viem ``` ```bash [bun] $ bun add mppx stripe viem ``` ::: ### Set environment variables Set your Stripe secret key and [Business Network](https://docs.stripe.com/get-started/account/profile) profile ID. ```bash [.env] STRIPE_SECRET_KEY=sk_test_... STRIPE_NETWORK_ID=your_network_id ``` ### Create the server with Stripe payments Set up an `Mppx` instance with the `stripe.charge` method. The `networkId` is your Stripe Business Network profile ID, and `paymentMethodTypes` controls which card types you accept. ```ts twoslash import Stripe from 'stripe' import { Mppx, stripe } from 'mppx/server' const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!) const mppx = Mppx.create({ methods: [ stripe.charge({ client: stripeClient, networkId: process.env.STRIPE_NETWORK_ID!, paymentMethodTypes: ['card'], }), ], }) ``` ### Add a paid endpoint Use `mppx.charge` to gate your endpoint. Set the `amount` in the smallest currency unit (cents for USD), and specify `currency` and `decimals`. ```ts twoslash import Stripe from 'stripe' import { Mppx, stripe } from 'mppx/server' const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!) const mppx = Mppx.create({ methods: [ stripe.charge({ client: stripeClient, networkId: process.env.STRIPE_NETWORK_ID!, paymentMethodTypes: ['card'], }), ], }) // ---cut--- export async function handler(request: Request) { const result = await mppx.charge({ amount: '100', currency: 'usd', decimals: 2, description: 'Premium API access', })(request) if (result.status === 402) return result.challenge return result.withReceipt(Response.json({ data: 'your response here' })) } ``` :::: ## Client setup Use `stripe` with `Mppx.create` to handle `402` responses automatically. The client parses the Challenge, creates an SPT through the `createToken` callback, and retries with the Credential. ```ts twoslash import { Mppx, stripe } from 'mppx/client' Mppx.create({ methods: [ stripe({ createToken: async (params) => { const res = await fetch('/api/create-spt', { body: JSON.stringify(params), headers: { 'Content-Type': 'application/json' }, method: 'POST', }) if (!res.ok) throw new Error('Failed to create SPT') return (await res.json()).spt }, paymentMethod: 'pm_card_visa', }), ], }) const response = await fetch('https://api.example.com/resource') // @log: Response { status: 200, ... } ``` :::warning\[Security: server-side authorization] The `createToken` callback proxies through your own server because SPT creation requires a Stripe secret key. The server **must** derive SPT parameters (amount, currency, expiry) itself—never accept them from the client. See the [SPT creation proxy endpoint](/payment-methods/stripe/charge#spt-creation-proxy-endpoint) for a secure implementation. ::: ## Accept cards and stablecoins together Stripe works alongside other payment methods. Add `tempo` to accept both cards and stablecoins on the same endpoint—clients pay with whichever rail they support. ```ts twoslash import Stripe from 'stripe' import { Mppx, stripe, tempo } from 'mppx/server' const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!) const mppx = Mppx.create({ methods: [ stripe.charge({ client: stripeClient, networkId: process.env.STRIPE_NETWORK_ID!, paymentMethodTypes: ['card'], }), tempo.charge({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', testnet: true, }), ], }) ``` The server returns both methods in the `402` Challenge. See [Accept multiple payment methods](/guides/multiple-payment-methods) for a full walkthrough. ## Add a browser payment page Set `html` on the Stripe method to render a Stripe Elements card form when a browser visits the endpoint. Programmatic clients with `Authorization` headers are unaffected. ```ts twoslash import Stripe from 'stripe' import { Mppx, stripe } from 'mppx/server' const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!) const mppx = Mppx.create({ methods: [ stripe.charge({ client: stripeClient, // [!code hl:start] html: { createTokenUrl: '/api/create-spt', publishableKey: process.env.STRIPE_PUBLISHABLE_KEY!, }, // [!code hl:end] networkId: process.env.STRIPE_NETWORK_ID!, paymentMethodTypes: ['card'], }), ], }) ``` See [Create a payment link](/guides/payment-links) for a full walkthrough and live demo. ## Next steps # Accept split payments \[Distribute a charge across multiple recipients] Split a single charge across multiple recipients in one atomic transaction. The primary recipient receives the remainder after all splits are deducted. Split payments are useful for: * **Marketplaces** — route a platform fee to yourself and the rest to the seller * **Referral programs** — pay a bounty to the referrer on every purchase * **Revenue sharing** — distribute earnings across partners or contributors ## How it works When you add `splits` to a charge, the SDK constructs multiple on-chain transfers in a single transaction: 1. Each split recipient receives their declared amount 2. The primary `recipient` receives `amount - sum(splits)` 3. The server verifies all transfers atomically :::info Split amounts are in human-readable units, the same as the top-level `amount`. The primary recipient's share is always implicit — you only declare the splits. ::: ## Server Add a `splits` array to any `mppx.charge` call. Each entry specifies a `recipient` and `amount`. ```ts twoslash import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo()] }) // ---cut--- export async function handler(request: Request) { const result = await mppx.charge({ amount: '1.00', currency: '0x20c0000000000000000000000000000000000000', // pathUSD recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', // seller splits: [ // [!code hl] { // [!code hl] amount: '0.10', // [!code hl] recipient: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', // platform fee // [!code hl] }, // [!code hl] ], // [!code hl] })(request) // seller receives $0.90, platform receives $0.10 if (result.status === 402) return result.challenge return result.withReceipt(Response.json({ data: '...' })) } ``` ### With per-split memos Each split can carry its own on-chain memo for reconciliation: ```ts twoslash import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo()] }) declare const request: Request // ---cut--- const result = await mppx.charge({ amount: '1.00', currency: '0x20c0000000000000000000000000000000000000', // pathUSD memo: '0x6f726465722d313233', // order-123 recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', // seller splits: [ { amount: '0.10', memo: '0x706c6174666f726d2d666565', // platform-fee // [!code hl] recipient: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', // platform }, ], })(request) ``` ### With fee sponsorship Split payments work with [fee sponsorship](/payment-methods/tempo#fee-sponsorship). The server co-signs the multi-transfer transaction so the client doesn't need gas tokens. ```ts twoslash import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo()] }) declare const request: Request // ---cut--- const result = await mppx.charge({ amount: '1.00', currency: '0x20c0000000000000000000000000000000000000', // pathUSD feePayer: true, // [!code hl] recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', // seller splits: [ { amount: '0.05', recipient: '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC' }, // referrer { amount: '0.10', recipient: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8' }, // platform ], })(request) ``` ## Client The client SDK handles split payments automatically — no client-side configuration is needed. When the server includes `splits` in the Challenge, the client constructs the matching multi-transfer transaction. ### Validating split recipients Use `expectedRecipients` to restrict which split recipients the client signs for. This prevents a compromised server from redirecting funds to unexpected addresses. ```ts twoslash import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0xabc…123') // ---cut--- Mppx.create({ methods: [ tempo.charge({ account, expectedRecipients: [ // [!code hl] '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', // platform // [!code hl] '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC', // referrer // [!code hl] ], // [!code hl] }), ], }) ``` If the server sends a Challenge with a split recipient not in the allowlist, the client throws an error instead of signing. ## Constraints | Rule | Limit | |------|-------| | Splits per charge | 1–10 | | Each split amount | Must be > 0 | | Sum of all splits | Must be strictly less than `amount` | | Split memo | Optional, 32-byte hex hash | ## Next steps # Accept multiple payment methods \[Stablecoins, cards, and Bitcoin on a single endpoint] Build a payment-gated API that accepts [Tempo](/payment-methods/tempo) stablecoins, [Stripe](/payment-methods/stripe) cards, and [Lightning](/payment-methods/lightning) Bitcoin—all on the same endpoint. The server returns a `402` Challenge advertising every available method, and the client pays with whichever rail it supports. :::info MPP's multi-method support is additive. Each payment method is independent—you can start with one and add more at any time without changing your route handlers. ::: ## Prompt mode Paste this into your coding agent to build the entire guide in one prompt: {`Use https://mpp.dev/guides/multiple-payment-methods.md as reference. Add mppx to my app with a payment-gated endpoint that accepts three payment methods: Tempo, Stripe, and Lightning. Charge $0.01 per request. When payment is verified via any method, return a JSON response.`} ## How it works When multiple methods are registered, the `402` response includes a `WWW-Authenticate` header for each one. The client picks the method it supports and sends the appropriate Credential. ```http HTTP/1.1 402 Payment Required WWW-Authenticate: Payment method="tempo", intent="charge", ... WWW-Authenticate: Payment method="stripe", intent="charge", ... WWW-Authenticate: Payment method="lightning", intent="charge", ... ``` The server verifies whichever Credential it receives. Your route handler stays the same regardless of which method the client chose. ## Server setup ::::steps ### Install dependencies :::code-group ```bash [npm] $ npm install mppx stripe @buildonspark/lightning-mpp-sdk viem ``` ```bash [pnpm] $ pnpm add mppx stripe @buildonspark/lightning-mpp-sdk viem ``` ```bash [bun] $ bun add mppx stripe @buildonspark/lightning-mpp-sdk viem ``` ::: ### Configure payment methods Register all three methods in a single `Mppx.create` call. Each method has its own configuration—Tempo needs a recipient address and currency, Stripe needs API credentials, and Lightning needs a wallet mnemonic. ```ts [server.ts] import Stripe from 'stripe' import { Mppx, tempo, stripe } from 'mppx/server' import { spark } from '@buildonspark/lightning-mpp-sdk/server' const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!) const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [ tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', // pathUSD on Tempo recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', }), stripe.charge({ client: stripeClient, networkId: 'internal', paymentMethodTypes: ['card'], }), spark.charge({ mnemonic: process.env.MNEMONIC!, }), ], }) ``` ### Create a payment-gated route The route handler is identical to a single-method setup. `mppx.charge` advertises all registered methods in the Challenge and verifies whichever Credential the client presents. ```ts [server.ts] import crypto from 'crypto' import Stripe from 'stripe' import { Mppx, tempo, stripe } from 'mppx/server' import { spark } from '@buildonspark/lightning-mpp-sdk/server' const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!) const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [ tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', // pathUSD on Tempo recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', }), stripe.charge({ client: stripeClient, networkId: 'internal', paymentMethodTypes: ['card'], }), spark.charge({ mnemonic: process.env.MNEMONIC!, }), ], }) // [!code focus:start] Bun.serve({ async fetch(request) { const result = await mppx.charge({ amount: '0.01', currency: 'usd', decimals: 2, description: 'Premium API access', })(request) if (result.status === 402) return result.challenge return result.withReceipt(Response.json({ message: 'Paid content' })) }, }) // [!code focus:end] ``` ### Test via the `mppx` CLI The `mppx` CLI uses Tempo by default. Each payment method has its own client SDK—see the individual method docs for client setup. ```bash [terminal] # Create account funded with testnet tokens $ npx mppx account create # Make a paid request (pays with Tempo) $ npx mppx http://localhost:3000 ``` :::: ## Framework examples The `Mppx.create` configuration is the same across frameworks—only the route handler syntax changes. ### Hono ```ts [server.ts] import crypto from 'crypto' import { Hono } from 'hono' import Stripe from 'stripe' import { Mppx, tempo, stripe } from 'mppx/hono' import { spark } from '@buildonspark/lightning-mpp-sdk/server' const app = new Hono() const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!) const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [ tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', // pathUSD on Tempo recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', }), stripe.charge({ client: stripeClient, networkId: 'internal', paymentMethodTypes: ['card'], }), spark.charge({ mnemonic: process.env.MNEMONIC!, }), ], }) app.get( '/api/resource', mppx.charge({ amount: '0.01', currency: 'usd', decimals: 2, description: 'Premium API access' }), async (c) => c.json({ message: 'Paid content' }), ) ``` ### Express ```ts [server.ts] import crypto from 'crypto' import express from 'express' import Stripe from 'stripe' import { Mppx, tempo, stripe } from 'mppx/express' import { spark } from '@buildonspark/lightning-mpp-sdk/server' const app = express() const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!) const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [ tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', // pathUSD on Tempo recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', }), stripe.charge({ client: stripeClient, networkId: 'internal', paymentMethodTypes: ['card'], }), spark.charge({ mnemonic: process.env.MNEMONIC!, }), ], }) app.get( '/api/resource', mppx.charge({ amount: '0.01', currency: 'usd', decimals: 2, description: 'Premium API access' }), async (req, res) => res.json({ message: 'Paid content' }), ) ``` ### Next.js ```ts [app/api/resource/route.ts] import crypto from 'crypto' import Stripe from 'stripe' import { Mppx, tempo, stripe } from 'mppx/nextjs' import { spark } from '@buildonspark/lightning-mpp-sdk/server' const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!) const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [ tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', // pathUSD on Tempo recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', }), stripe.charge({ client: stripeClient, networkId: 'internal', paymentMethodTypes: ['card'], }), spark.charge({ mnemonic: process.env.MNEMONIC!, }), ], }) export const GET = mppx.charge({ amount: '0.01', currency: 'usd', decimals: 2, description: 'Premium API access' }) (async () => Response.json({ message: 'Paid content' })) ``` ## Method-specific configuration Each payment method has its own parameters. Refer to the individual method docs for the full configuration reference: ## Client preferences Clients can declare which payment methods they prefer by passing `paymentPreferences` to `Mppx.create`. This sends an `Accept-Payment` header on every request, and the server uses it to filter Challenges down to the methods the client supports. ```ts twoslash import { Mppx, stripe, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0x...') Mppx.create({ methods: [ tempo({ account }), stripe.charge({ createToken: async (opts) => { const res = await fetch('/api/create-spt', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(opts), }) const { spt } = await res.json() as { spt: string } return spt }, }), ], // [!code hl:start] paymentPreferences: ({ tempo, stripe }) => ({ [tempo.charge]: 1, [stripe.charge]: 0.5, [tempo.session]: 0.2, }), // [!code hl:end] }) ``` See the [`paymentPreferences` parameter reference](/sdk/typescript/client/Mppx.create#paymentpreferences-optional) for the full configuration options. ## Next steps # Create a payment link \[Share a link. Get paid.] Create a full payment page for any API endpoint—no frontend required. Share the link anywhere and users pay directly from the page. ## Supported methods | Method | `html` type | What renders | | --- | --- | --- | | [`tempo`](/sdk/typescript/server/Method.tempo) | `boolean` or config object | "Continue with Tempo" wallet UI | | [`stripe`](/sdk/typescript/server/Method.stripe) | Config object | Full Stripe Elements card form | | [`solana.charge`](/payment-methods/solana/charge#payment-links) | `boolean` | "Continue with Solana" wallet UI | For Tempo and Stripe, `html` also accepts an object so you can customize page text and theme without building a frontend. ## Demo This is a live payment link. Click "Continue with Tempo" to pay $0.01 and receive a random photo from [Picsum](https://picsum.photos). ## Prompt mode Paste this into your coding agent to create a payment link in one prompt: {`Use https://mpp.dev/guides/payment-links.md as reference. Add mppx to my app with a payment-gated photo endpoint that charges $0.01 per request using the Tempo payment method with PathUSD. Enable payment links so browsers can pay directly from the page by setting html: true on the tempo() method config. When payment is verified, fetch a random photo from https://picsum.photos/1024/1024 and return the URL as JSON.`} ## Manual mode Select your framework to follow a step-by-step guide. If your framework isn't listed, choose **Other** for a generic [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) approach compatible with most TypeScript server frameworks. ::::steps ### Install `mppx` :::code-group ```bash [npm] $ npm install mppx viem ``` ```bash [pnpm] $ pnpm add mppx viem ``` ```bash [bun] $ bun add mppx viem ``` ::: ### Set up `Mppx` instance with `html: true` ```ts [app/api/photo/route.ts] import crypto from 'crypto' import { Mppx, tempo } from 'mppx/nextjs' export const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', html: true, // [!code hl] recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', testnet: true, })], secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), }) ``` ### Create the `/api/photo` route Create the photo route with `mppx.charge`. Browsers see a payment page; programmatic clients get the standard `402` flow. ```ts [app/api/photo/route.ts] import crypto from 'crypto' import { Mppx, tempo } from 'mppx/nextjs' export const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', html: true, recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', testnet: true, })], secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), }) // [!code focus:start] export const GET = mppx.charge({ amount: '0.01', description: 'Random stock photo' }) (async () => { const res = await fetch('https://picsum.photos/1024/1024') return Response.json({ url: res.url }) }) // [!code focus:end] ``` ### Test in the browser Open `http://localhost:3000/api/photo` in your browser. The server returns a payment page with a "Continue with Tempo" button. After payment, the page reloads with the photo URL. Programmatic clients work the same way: ```bash [terminal] $ npx mppx http://localhost:3000/api/photo ``` :::: ::::steps ### Install `mppx` and `hono` :::code-group ```bash [npm] $ npm install mppx hono viem ``` ```bash [pnpm] $ pnpm add mppx hono viem ``` ```bash [bun] $ bun add mppx hono viem ``` ::: ### Set up `Mppx` instance with `html: true` ```ts [server.ts] import crypto from 'crypto' import { Hono } from 'hono' import { Mppx, tempo } from 'mppx/hono' const app = new Hono() const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', html: true, // [!code hl] recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', testnet: true, })], secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), }) ``` ### Create the `/api/photo` route ```ts [server.ts] import crypto from 'crypto' import { Hono } from 'hono' import { Mppx, tempo } from 'mppx/hono' const app = new Hono() const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', html: true, recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', testnet: true, })], secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), }) // [!code focus:start] app.get( '/api/photo', mppx.charge({ amount: '0.01', description: 'Random stock photo' }), async (c) => { const res = await fetch('https://picsum.photos/1024/1024') return c.json({ url: res.url }) }, ) // [!code focus:end] ``` ### Test in the browser Open `http://localhost:3000/api/photo` in your browser. ```bash [terminal] $ npx mppx http://localhost:3000/api/photo ``` :::: ::::steps ### Install `mppx` and `express` :::code-group ```bash [npm] $ npm install mppx express viem ``` ```bash [pnpm] $ pnpm add mppx express viem ``` ```bash [bun] $ bun add mppx express viem ``` ::: ### Set up `Mppx` instance with `html: true` ```ts [server.ts] import crypto from 'crypto' import express from 'express' import { Mppx, tempo } from 'mppx/express' const app = express() const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', html: true, // [!code hl] recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', testnet: true, })], secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), }) ``` ### Create the `/api/photo` route ```ts [server.ts] import crypto from 'crypto' import express from 'express' import { Mppx, tempo } from 'mppx/express' const app = express() const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', html: true, recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', testnet: true, })], secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), }) // [!code focus:start] app.get( '/api/photo', mppx.charge({ amount: '0.01', description: 'Random stock photo' }), async (req, res) => { const response = await fetch('https://picsum.photos/1024/1024') res.json({ url: response.url }) }, ) // [!code focus:end] ``` ### Test in the browser Open `http://localhost:3000/api/photo` in your browser. ```bash [terminal] $ npx mppx http://localhost:3000/api/photo ``` :::: This guide walks through using `mppx/server` directly with any [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)-compatible framework: [Bun](https://bun.sh), [Deno](https://deno.com), [Cloudflare Workers](https://workers.dev), and others.
::::steps ### Install `mppx` :::code-group ```bash [npm] $ npm install mppx viem ``` ```bash [pnpm] $ pnpm add mppx viem ``` ```bash [bun] $ bun add mppx viem ``` ::: ### Set up `Mppx` instance with `html: true` ```ts [server.ts] import crypto from 'crypto' import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', html: true, // [!code hl] recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', testnet: true, })], secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), }) ``` ### Create the handler The server detects the `Accept` header and returns a payment page for browsers. Programmatic clients get the standard `402` flow. ```ts [server.ts] import crypto from 'crypto' import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', html: true, recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', testnet: true, })], secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), }) // [!code focus:start] Bun.serve({ async fetch(request) { const result = await mppx.charge({ amount: '0.01', description: 'Random stock photo', })(request) if (result.status === 402) return result.challenge const res = await fetch('https://picsum.photos/1024/1024') return result.withReceipt(Response.json({ url: res.url })) }, }) // [!code focus:end] ``` ### Test in the browser Open `http://localhost:3000` in your browser. ```bash [terminal] $ npx mppx http://localhost:3000 ``` :::: ## Next steps # Monetize your MCP server \[Charge for tool calls with MPP] Add per-call payments to any [MCP](https://modelcontextprotocol.io) server. When an agent calls a paid tool, the server issues a Challenge, the agent pays, and the tool executes—all within the MCP protocol. No API keys or billing portals required. ## How it works >S: (1) tools/call S-->>A: (2) {"error":{"code":-32042}} + Challenge Note over A: (3) Create Credential A->>S: (4) tools/call + {"_meta":{"credential"}} S->>N: (5) Settle payment N-->>S: (6) Confirmed S-->>A: (7) {"result":{}} + Receipt `} /> 1. **Agent** calls a tool on the MCP server 2. **Server** responds with JSON-RPC error code `-32042` and a Challenge specifying the price 3. **Agent** creates a Credential (payment proof) from the Challenge 4. **Agent** retries the tool call with the Credential in `_meta` 5. **Server** verifies the Credential and settles the payment on-chain 6. **Network** confirms the payment 7. **Server** returns the tool result with a Receipt in `_meta` This maps directly to the standard MPP challenge → credential → receipt flow, encoded as JSON-RPC instead of HTTP headers. See the [MCP transport spec](/protocol/transports/mcp) for the full encoding. :::info This guide uses the MCP transport, so Challenge, Credential, and Receipt data travel as native JSON in `error.data` and `_meta`. The base64url-encoded `request` and `opaque` auth-params apply to the HTTP transport. ::: ## Prompt mode Paste this into your coding agent to build a paid MCP server in one prompt: {`Use https://mpp.dev/guides/monetize-mcp-server.md as reference. Build an MCP server with mppx that charges $0.01 per tool call using the Tempo payment method. Use @modelcontextprotocol/sdk for the MCP server and Transport.mcpSdk() for the payment transport.`} ## Manual mode ::::steps ### Install dependencies :::code-group ```bash [npm] $ npm install mppx @modelcontextprotocol/sdk viem ``` ```bash [pnpm] $ pnpm add mppx @modelcontextprotocol/sdk viem ``` ```bash [bun] $ bun add mppx @modelcontextprotocol/sdk viem ``` ::: ### Create the MCP server Set up a standard MCP server with a tool. This tool is **currently free**. ```ts [server.ts] import { McpServer } from '@modelcontextprotocol/sdk/server/mcp' import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio' const server = new McpServer({ name: 'my-service', version: '1.0.0' }) // [!code focus:start] server.registerTool( 'search', { description: 'Search the web' }, async ({ query }) => ({ content: [{ type: 'text', text: `Results for: ${query}` }], }), ) // [!code focus:end] const transport = new StdioServerTransport() await server.connect(transport) ``` ### Add `mppx` with the MCP transport Create an `Mppx` instance with `Transport.mcpSdk()`. This tells `mppx` to encode Challenges and Receipts as JSON-RPC messages instead of HTTP headers. ```ts [server.ts] import { McpServer } from '@modelcontextprotocol/sdk/server/mcp' import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio' import { Mppx, tempo, Transport } from 'mppx/server' // [!code ++] // [!code focus:start] const mppx = Mppx.create({ methods: [tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], transport: Transport.mcpSdk(), // [!code hl] }) // [!code focus:end] const server = new McpServer({ name: 'my-service', version: '1.0.0' }) server.registerTool( 'search', { description: 'Search the web' }, async ({ query }) => ({ content: [{ type: 'text', text: `Results for: ${query}` }], }), ) const transport = new StdioServerTransport() await server.connect(transport) ``` ### Add `.charge` to the tool handler Call `mppx.charge` inside the tool handler. If the agent hasn't paid, throw the Challenge. Otherwise, attach a Receipt to the result. ```ts [server.ts] import { McpServer } from '@modelcontextprotocol/sdk/server/mcp' import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio' import { Mppx, tempo, Transport } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], transport: Transport.mcpSdk(), }) const server = new McpServer({ name: 'my-service', version: '1.0.0' }) // [!code focus:start] server.registerTool( 'search', { description: 'Search the web' }, async ({ query }, extra) => { const result = await mppx.charge({ // [!code ++] amount: '0.01', // [!code ++] description: 'Web search query', // [!code ++] })(extra) // [!code ++] if (result.status === 402) throw result.challenge // [!code ++] return result.withReceipt({ // [!code ++] content: [{ type: 'text', text: `Results for: ${query}` }], }) }, ) // [!code focus:end] const mcpTransport = new StdioServerTransport() await server.connect(mcpTransport) ``` Three lines turn a free tool into a paid one: * **`mppx.charge()`** checks for a valid Credential in the tool call's `_meta` * **`throw result.challenge`** sends a `-32042` error with the payment requirements * **`result.withReceipt()`** attaches a Receipt to the tool result ### Test with the `mppx` CLI ```bash [terminal] # Create account funded with testnet tokens $ npx mppx account create # Start the server and call the tool $ echo '{"method":"tools/call","params":{"name":"search","arguments":{"query":"hello"}}}' | node server.ts ``` :::: ## Multiple tools with different prices Set different prices per tool. Free tools don't need `mppx.charge` at all. ```ts [server.ts] import { McpServer } from '@modelcontextprotocol/sdk/server/mcp' import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio' import { Mppx, tempo, Transport } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo({ testnet: true, currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], transport: Transport.mcpSdk(), }) const server = new McpServer({ name: 'my-service', version: '1.0.0' }) // [!code focus:start] // Free tool — no payment required server.registerTool( 'status', { description: 'Check service status' }, async () => ({ content: [{ type: 'text', text: 'OK' }], }), ) // $0.01 per call server.registerTool( 'search', { description: 'Search the web' }, async ({ query }, extra) => { const result = await mppx.charge({ amount: '0.01' })(extra) if (result.status === 402) throw result.challenge return result.withReceipt({ content: [{ type: 'text', text: `Results for: ${query}` }], }) }, ) // $0.10 per call server.registerTool( 'generate-image', { description: 'Generate an image from a prompt' }, async ({ prompt }, extra) => { const result = await mppx.charge({ amount: '0.10' })(extra) if (result.status === 402) throw result.challenge return result.withReceipt({ content: [{ type: 'text', text: `Image generated for: ${prompt}` }], }) }, ) // [!code focus:end] const transport = new StdioServerTransport() await server.connect(transport) ``` ## What the agent sees Under the hood, the MCP transport encodes MPP Challenges, Credentials, and Receipts as JSON-RPC fields: | MPP concept | MCP encoding | |---|---| | Challenge | Error code `-32042` with challenges in `error.data` | | Credential | `_meta["org.paymentauth/credential"]` on the tool call | | Receipt | `_meta["org.paymentauth/receipt"]` on the tool result | A payment-aware MCP client like [`McpClient.wrap`](/sdk/typescript/client/McpClient.wrap) handles this automatically—the agent doesn't need to know the encoding details. ## Specification ## Next steps # Proxy an existing service \[Add payments to any API without changing its code] Put a payment gate in front of an existing API without modifying it. The proxy sits between clients and the origin, handles the `402` flow, injects upstream credentials, and forwards paid requests. ## How it works >P: GET /my-api/v1/data P-->>C: 402 + WWW-Authenticate: Payment C->>P: GET /my-api/v1/data + Authorization: Payment P->>P: Verify Credential P->>O: GET /v1/data + Authorization: Bearer sk-... O-->>P: 200 + response body P-->>C: 200 + Payment-Receipt + response body `} /> The proxy verifies the client's payment Credential, then forwards the request to the origin with the real API key injected. The client never sees the upstream credentials. ## Prompt mode Paste this into your coding agent to build the entire guide in one prompt: {`Use https://mpp.dev/guides/proxy-existing-service.md as reference. Create an mppx proxy server that gates an upstream REST API behind MPP payments. Use Service.from with a bearer token for upstream auth. Charge $0.01 for the forecast endpoint and allow the status endpoint for free. Use the Tempo payment method.`} ## Manual mode ::::steps ### Install `mppx` :::code-group ```bash [npm] $ npm install mppx viem ``` ```bash [pnpm] $ pnpm add mppx viem ``` ```bash [bun] $ bun add mppx viem ``` ::: ### Define the upstream service Use `Service.from` to describe the API you want to proxy. The proxy injects upstream credentials so clients never see them. ```ts [server.ts] import { Proxy, Service } from 'mppx/proxy' import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo()] }) const proxy = Proxy.create({ services: [ // [!code focus:start] Service.from('weather', { baseUrl: 'https://api.weather.example.com', bearer: process.env.WEATHER_API_KEY!, // [!code hl] description: 'Weather forecast API', routes: { 'GET /v1/forecast': mppx.charge({ amount: '0.01' }), 'GET /v1/status': true, // [!code hl] }, title: 'Weather API', }), // [!code focus:end] ], }) ``` * **`bearer`** injects `Authorization: Bearer` on upstream requests. Use `headers` for APIs that expect a custom header like `X-API-Key`. * **`true`** marks a route as free passthrough—no payment required, but upstream credentials are still injected. ### Set pricing per route Each route maps a `"METHOD /pattern"` to a payment intent or `true` for free passthrough. Set different prices per endpoint. ```ts [server.ts] import { Proxy, Service } from 'mppx/proxy' import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo()] }) const proxy = Proxy.create({ services: [ Service.from('weather', { baseUrl: 'https://api.weather.example.com', bearer: process.env.WEATHER_API_KEY!, description: 'Weather forecast API', routes: { 'GET /v1/forecast': mppx.charge({ amount: '0.01' }), // [!code focus] 'GET /v1/historical/:date': mppx.charge({ amount: '0.05' }), // [!code focus] 'GET /v1/status': true, // [!code focus] }, title: 'Weather API', }), ], }) ``` Requests to routes not listed in the map receive `404`. ### Start the proxy The proxy returns two handlers for different runtimes. ```ts [server.ts] import { Proxy, Service } from 'mppx/proxy' import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo()] }) const proxy = Proxy.create({ description: 'Payment-gated weather data', // [!code hl] services: [ Service.from('weather', { baseUrl: 'https://api.weather.example.com', bearer: process.env.WEATHER_API_KEY!, description: 'Weather forecast API', routes: { 'GET /v1/forecast': mppx.charge({ amount: '0.01' }), 'GET /v1/historical/:date': mppx.charge({ amount: '0.05' }), 'GET /v1/status': true, }, title: 'Weather API', }), ], title: 'Weather Proxy', // [!code hl] }) // [!code focus:start] // Bun / Deno export default { fetch: proxy.fetch } // Node.js // import { createServer } from 'node:http' // createServer(proxy.listener).listen(3000) // [!code focus:end] ``` ### Test via the `mppx` CLI ```bash [terminal] # Create account funded with testnet tokens $ npx mppx account create # Free route — no payment required $ npx mppx http://localhost:3000/weather/v1/status # Paid route — handles 402 automatically $ npx mppx http://localhost:3000/weather/v1/forecast ``` Requests route to `/{serviceId}/`—for example, `/weather/v1/forecast` proxies to `https://api.weather.example.com/v1/forecast`. :::: ## Multiple upstream services Gate several APIs behind a single proxy by passing multiple services. Each mounts at its own path prefix. ```ts [server.ts] import { Proxy, Service, openai } from 'mppx/proxy' import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo()] }) const proxy = Proxy.create({ description: 'Multi-service paid API proxy', services: [ // [!code focus:start] openai({ // [!code hl] apiKey: process.env.OPENAI_API_KEY!, routes: { 'GET /v1/models': true, 'POST /v1/chat/completions': mppx.charge({ amount: '0.05' }), }, }), Service.from('internal', { // [!code hl] baseUrl: 'https://api.internal.example.com', headers: { 'X-API-Key': process.env.INTERNAL_API_KEY! }, // [!code hl] routes: { 'POST /v1/analyze': mppx.charge({ amount: '0.10' }), }, }), // [!code focus:end] ], title: 'My Proxy', }) ``` * `/openai/v1/chat/completions` → `https://api.openai.com/v1/chat/completions` * `/internal/v1/analyze` → `https://api.internal.example.com/v1/analyze` Built-in service presets (`openai`, `anthropic`, `stripe`) handle upstream auth conventions automatically. Use `Service.from` for everything else. ## Discovery The proxy auto-generates discovery endpoints so coding agents and CLI tools can find available services and pricing. ```bash [terminal] # Human-readable overview $ curl http://localhost:3000/llms.txt # JSON service list $ curl http://localhost:3000/discover # Single service details $ curl http://localhost:3000/discover/weather ``` See the [Proxy reference](/sdk/typescript/proxy#discovery-endpoints) for the full list of discovery endpoints. ## Next steps # Upgrade your x402 server to MPP \[Migrate from x402 in minutes] MPP plugs into your existing x402 server. Both protocols use HTTP `402` to gate API access behind payment, and x402's "exact" charge flow maps directly onto MPP's `charge` intent. Adding MPP gives you standard `WWW-Authenticate` / `Authorization` headers, multi-method support (stablecoins, cards, Lightning), sessions for streaming payments, and an [IETF standards track](https://paymentauth.org) foundation—all without changing your business logic. ## What MPP adds MPP builds on the same `402` pattern that x402 established, and extends it with standardized headers and new capabilities. | | x402 | MPP | |---|---|---| | **Challenge** | `PAYMENT-REQUIRED` header | `WWW-Authenticate: Payment` header | | **Credential** | `PAYMENT-SIGNATURE` header | `Authorization: Payment` header | | **Receipt** | `PAYMENT-RESPONSE` header | `Payment-Receipt` header | | **Payment methods** | Stablecoins only | Stablecoins, cards, digital wallets, and many other methods | | **Sessions** | No | Yes—off-chain vouchers for sub-cent streaming | | **Error format** | Custom | [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) Problem Details | | **Idempotency** | No | Built-in Challenge binding | :::info MPP is compatible with x402. The x402 "exact" charge flow maps directly onto MPP's `charge` intent. MPP extends beyond x402 with idempotency, Receipts, request binding, multiple payment methods, and efficient payment sessions. ::: ## Protocol flow The request lifecycle is the same—only the header names and encoding change. **x402** >Server: GET /resource Server-->>Client: 402 + PAYMENT-REQUIRED Client->>Server: GET /resource + PAYMENT-SIGNATURE Server-->>Client: 200 + PAYMENT-RESPONSE `} /> **MPP** >Server: GET /resource Server-->>Client: 402 + WWW-Authenticate: Payment Client->>Server: GET /resource + Authorization: Payment Server-->>Client: 200 + Payment-Receipt `} /> ## Key concepts MPP uses three protocol objects in every payment flow—the same lifecycle x402 uses, with standardized names: * **Challenge** — The server's `402` response advertising what payment is required. Sent in the `WWW-Authenticate: Payment` header. Equivalent to x402's `PAYMENT-REQUIRED`. * **Credential** — The client's signed payment authorization. Sent in the `Authorization: Payment` header. Equivalent to x402's `PAYMENT-SIGNATURE`. * **Receipt** — The server's confirmation that payment settled. Sent in the `Payment-Receipt` header. Equivalent to x402's `PAYMENT-RESPONSE`. ## Running x402 servers with mppx Use `evm.charge` when you want existing x402 clients to keep paying while you move payment logic into the MPP SDK. x402 clients read `PAYMENT-REQUIRED` and send `PAYMENT-SIGNATURE`. MPP clients read `WWW-Authenticate` and send `Authorization`. :::info For exact x402 flows, `evm.charge` replaces custom Challenge builders, `PAYMENT-*` header parsing, facilitator verification calls, settlement response handling, and route binding. Configure the asset, recipient, and facilitator once; each route supplies a display-unit `amount`. ::: ### `evm.charge` `evm.charge` creates an EVM charge payment method. On the server, pass `currency`, `recipient`, and either `x402.facilitator` or `settle`. On the route, call `mppx.evm.charge({ amount })`. See the [`evm.charge` server reference](/sdk/typescript/server/Method.evm.charge) and [`evm.charge` client reference](/sdk/typescript/client/Method.evm.charge) for full parameters. ### Run an x402-compatible endpoint Configure `evm.charge` with a known x402 asset and a facilitator. The route returns x402-compatible Challenges and accepts x402 exact Credentials on retry. ```ts twoslash [server.ts] import { evm, Mppx } from 'mppx/server' const mppx = Mppx.create({ methods: [ evm.charge({ currency: evm.assets.baseSepolia.USDC, recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', x402: { facilitator: 'https://x402.org/facilitator', }, }), ], secretKey: process.env.MPP_SECRET_KEY ?? 'local-dev-secret', }) export async function GET(request: Request) { const result = await mppx.evm.charge({ amount: '0.01', description: 'Premium data access', })(request) if (result.status === 402) return result.challenge return result.withReceipt(Response.json({ ok: true })) } ``` ### Add MPP to the same endpoint Use `mppx.compose` when one endpoint should support x402 EVM charges and another MPP method. The same route verifies either wire format. ```ts twoslash [server.ts] import { evm, Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [ evm.charge({ currency: evm.assets.baseSepolia.USDC, recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', x402: { facilitator: 'https://x402.org/facilitator', }, }), tempo.charge({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', testnet: true, }), ], secretKey: process.env.MPP_SECRET_KEY ?? 'local-dev-secret', }) const paid = mppx.compose( [mppx.evm.charge, { amount: '0.01', description: 'Premium data access' }], [mppx.tempo.charge, { amount: '0.01', description: 'Premium data access' }], ) export async function GET(request: Request) { const result = await paid(request) if (result.status === 402) return result.challenge return result.withReceipt(Response.json({ ok: true })) } ``` ### Use Hono middleware Import from `mppx/hono` when your x402 server already uses Hono. The middleware handles Challenge creation, Credential verification, settlement, and Receipt headers. ```ts twoslash [server.ts] import { Hono } from 'hono' import { evm, Mppx } from 'mppx/hono' const app = new Hono() const mppx = Mppx.create({ methods: [ evm.charge({ currency: evm.assets.baseSepolia.USDC, recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', x402: { facilitator: 'https://x402.org/facilitator', }, }), ], secretKey: process.env.MPP_SECRET_KEY ?? 'local-dev-secret', }) app.get('/paid', mppx.evm.charge({ amount: '0.01' }), (c) => c.json({ ok: true }), ) ``` ### Call with the mppx client `evm.charge` on the client signs both native MPP EVM charge Challenges and x402 exact Challenges. ```ts twoslash [client.ts] import { Fetch, evm } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const fetch = Fetch.from({ methods: [ evm.charge({ account: privateKeyToAccount( '0x0123456789012345678901234567890123456789012345678901234567890123', ), currencies: [evm.assets.baseSepolia.USDC], maxAmount: '1.00', }), ], }) const response = await fetch('https://api.example.com/paid') console.log(response.status) // @log: 200 ``` :::tip Because MPP uses the standard `WWW-Authenticate` scheme, it works with HTTP intermediaries (proxies, CDNs, API gateways) that understand [RFC 7235](https://www.rfc-editor.org/rfc/rfc7235) authentication. ::: ## Prompt mode Paste this into your coding agent to add MPP in one prompt: {`Use https://mpp.dev/guides/upgrade-x402.md#running-x402-servers-with-mppx as reference. Add mppx to my existing x402 server. Use the Tempo payment method with USDC.e. Keep the same pricing and route structure. Add mppx.charge to each paid endpoint. Point out areas where I could benefit from adding sessions.`} ## Add MPP to your server ::::steps ### Install `mppx` :::code-group ```bash [npm] $ npm install mppx viem ``` ```bash [pnpm] $ pnpm add mppx viem ``` ```bash [bun] $ bun add mppx viem ``` ::: ### Add `mppx` alongside your existing routes Here's a typical x402 server with MPP applied. Your business logic stays the same—you swap the payment middleware. ```ts [server.ts] import express from 'express' import { Mppx, tempo } from 'mppx/express' // [!code hl] const app = express() const mppx = Mppx.create({ // [!code hl] methods: [ // [!code hl] tempo({ // [!code hl] currency: '0x20c0000000000000000000000000000000000000', // [!code hl] recipient: '0xYourAddress', // [!code hl] }), // [!code hl] ], // [!code hl] secretKey: process.env.MPP_SECRET_KEY ?? 'local-dev-secret', // [!code hl] }) // [!code hl] app.get( '/api/data', mppx.charge({ amount: '0.01', description: 'Premium data access' }), // [!code hl] (req, res) => { res.json({ data: 'premium content' }) }, ) ``` Key differences: * **Per-route pricing.** `mppx.charge` is route middleware instead of a global config map. * **Amount in dollars, not atomic units.** `'0.01'` instead of `'10000'`. * **No header parsing.** The SDK handles `WWW-Authenticate`, `Authorization`, and `Payment-Receipt` headers automatically. ### Simplify verification and settlement x402 servers typically call a facilitator service for `POST /verify` and `POST /settle`. The `mppx` SDK handles verification and settlement internally, so you don't need a separate facilitator client. ```ts [server.ts] // x402 — external facilitator import { createFacilitator } from '@x402/server' const facilitator = createFacilitator('https://facilitator.x402.org') // MPP — built into the SDK const mppx = Mppx.create({ methods: [tempo({ /* ... */ })], }) ``` ### Add more payment methods This is where MPP goes beyond x402. You can accept stablecoins, cards, and Lightning on the same endpoint by registering additional methods. The `402` response advertises all of them, and the client picks one. ```ts [server.ts] import express from 'express' import Stripe from 'stripe' import { Mppx, tempo, stripe } from 'mppx/express' const app = express() const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!) const mppx = Mppx.create({ methods: [ tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xYourAddress', testnet: true, }), stripe.charge({ // [!code hl] client: stripeClient, // [!code hl] networkId: 'internal', // [!code hl] paymentMethodTypes: ['card'], // [!code hl] }), // [!code hl] ], secretKey: process.env.MPP_SECRET_KEY ?? 'local-dev-secret', }) app.get( '/api/data', mppx.charge({ amount: '0.01', description: 'Premium data access' }), (req, res) => { res.json({ data: 'premium content' }) }, ) ``` No changes to the route handler—`mppx.charge` advertises both methods and verifies whichever Credential the client presents. ### Test via the `mppx` CLI ```bash [terminal] # Create account funded with testnet tokens $ npx mppx account create # Make a paid request $ npx mppx http://localhost:3000/api/data ``` :::: ## What you gain * **Multi-method payments.** Accept stablecoins, cards, and Lightning on the same endpoint—your x402 stablecoin flow continues to work, and new rails are additive. * **Sessions.** Stream sub-cent payments with off-chain vouchers instead of one on-chain transaction per request. * **IETF standards track.** MPP's [Payment HTTP Authentication Scheme](https://paymentauth.org) follows the IETF standards process—building on a standard that browsers, proxies, and CDNs already understand. * **Production primitives.** Idempotency, expiration, request-body binding, and RFC 9457 error responses are built into the protocol. * **Permissionless extensibility.** Anyone can author and publish a new payment method without approval from a foundation. ## Next steps # Protocol overview \[Standardizing HTTP 402 for machine-to-machine payments] The Machine Payments Protocol (MPP) is a protocol for machine-to-machine payments. It standardizes HTTP `402` "Payment Required" with an extensible framework that works with any payment network. These docs provide a developer-friendly overview. For the full specification, see the full [IETF Specification](https://paymentauth.org). ## Flow ## Core concepts ## Status codes MPP uses HTTP status codes consistently to signal payment-related conditions: :::info\[Consistent 402 usage] MPP uses `402` for all payment-related challenges, including failed credential validation. This differs from other HTTP authentication schemes that use `401` for failed credentials. The distinction: * **`402`** = Payment barrier (initial challenge or retry needed) * **`401`** = Authentication failure unrelated to payment * **`403`** = Payment succeeded but access denied by policy ::: | Condition | Status | Response | | -------------------------------------------------- | ------------------------------------ | ------------------------------------------------ | | Resource requires payment, no credential provided | 402 | Fresh challenge in `WWW-Authenticate` | | Malformed credential (invalid base64url, bad JSON) | 402 | Fresh challenge + `malformed-credential` problem | | Unknown, expired, or already-used challenge id | 402 | Fresh challenge + `invalid-challenge` problem | | Payment proof invalid or verification failed | 402 | Fresh challenge + `verification-failed` problem | | Payment verified, access granted | 200 | Resource + optional `Payment-Receipt` | | Payment verified, but policy denies access | 403 | No challenge (payment was valid) | See [HTTP 402](/protocol/http-402) for details on when to return each status code. ## Payment method agnostic MPP works with any payment network or currency. The core protocol defines the framework, while **payment methods** define how specific networks integrate: :::info\[Extensible by design] Anyone can define new payment methods. The protocol requires that methods define their `request` schema (what the server asks for) and `payload` schema (what the client provides as proof). ::: | Method | Description | Status | | --------------------------------- | ----------------------------------------------- | ------------------------------------------- | | [Tempo](/payment-methods/tempo) | Native stablecoin payments on Tempo Network | Production | | [Stripe](/payment-methods/stripe) | Traditional card payment methods through Stripe | Production | Each payment method specifies its own `request` and `payload` schemas while sharing the common Challenge/Credential flow. ### Payment method requirements Payment method specifications must define: 1. **Method identifier**—Unique lowercase ASCII string (for example, `tempo` or `stripe`) 2. **Request schema**—JSON structure for the `request` parameter in challenges 3. **Payload schema**—JSON structure for credential `payload` fields 4. **Verification procedure**—How servers validate payment proofs 5. **Settlement procedure**—How payment is finalized ## Payment intents Payment intents describe the type of payment being requested. Common intents include: * **`charge`**—One-time payment that settles immediately * **`session`**—Streaming payment over a payment channel * **`subscription`**—Recurring fixed payment for paid access across billing periods Intent Specifications define: * Required and optional `request` fields * `payload` requirements * Verification and settlement semantics Servers can offer multiple intents in separate challenges, allowing clients to choose: ```http WWW-Authenticate: Payment id="abc", method="tempo", intent="charge", ... WWW-Authenticate: Payment id="def", method="tempo", intent="session", ... WWW-Authenticate: Payment id="ghi", method="tempo", intent="subscription", ... ``` ## Request body binding For requests with bodies (`POST`, `PUT`, `PATCH`), servers can bind the challenge to the request body using a `digest` parameter: ```http WWW-Authenticate: Payment id="...", method="tempo", intent="charge", digest="sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:", request="..." ``` When a `digest` is present, clients must submit the credential with a request body whose digest matches. This prevents clients from modifying the request body after receiving the challenge. The digest is computed per [RFC 9530](https://www.rfc-editor.org/rfc/rfc9530) Content-Digest header format. ## Error handling Failed payment attempts return `402` with a fresh challenge and a Problem Details [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) body: ```json { "type": "https://paymentauth.org/problems/verification-failed", "title": "Payment Verification Failed", "status": 402, "detail": "Invalid payment proof." } ``` Common error codes (full type URI: `https://paymentauth.org/problems/{code}`): | Code | Description | | ---------------------- | ---------------------------------------------- | | `payment-required` | Resource requires payment | | `payment-insufficient` | Amount too low | | `payment-expired` | Challenge or authorization expired | | `verification-failed` | Proof invalid | | `method-unsupported` | Method not accepted | | `malformed-credential` | Invalid credential format | | `invalid-challenge` | Challenge ID unknown, expired, or already used | Use the `Retry-After` header to indicate when clients can retry failed payments. ## Security considerations ### Transport security **TLS 1.2 or later is REQUIRED** for all Payment authentication flows. Use TLS 1.3 where possible. Payment Credentials contain sensitive authorization data that could result in financial loss if intercepted. ### Replay protection Payment methods must provide single-use proof semantics. A payment proof can be used exactly once; subsequent attempts to use the same proof must be rejected. ### Idempotency Servers must not perform side effects (database writes, external API calls) for requests that have not been paid. The unpaid request that triggers a `402` challenge must not modify server state beyond recording the challenge itself. For non-idempotent methods (`POST`, `PUT`, `DELETE`), accept an `Idempotency-Key` header to enable safe client retries. ### Amount verification Clients must verify before authorizing payment: 1. Requested amount is reasonable for the resource 2. Recipient/address is expected 3. Currency/asset is as expected 4. Validity window is appropriate :::warning\[Don't trust descriptions] Clients must not rely on the `description` parameter for payment verification. Malicious servers could provide a misleading description while the actual `request` payload requests a different amount. ::: ### Credential handling Payment credentials are bearer tokens that authorize financial transactions. Servers and intermediaries must not log Payment credentials or include them in error messages, debugging output, or analytics. ### Caching Payment challenges contain unique identifiers and time-sensitive payment data that must not be cached. Servers must send `Cache-Control: no-store` with `402` responses. Responses containing `Payment-Receipt` headers must include `Cache-Control: private`. ## Extensibility The protocol is designed for extensibility, with simple constraints where required for security or a consistent developer experience: ### Custom parameters Implementations may define additional parameters in challenges: * Parameters must use lowercase names * Unknown parameters must be ignored by clients * This allows payment methods to add method-specific fields ### Size considerations * Keep challenges under 8 KB * Clients must handle challenges of at least 4 KB * Servers must handle credentials of at least 4 KB ### Internationalization * All string values use UTF-8 encoding * Payment method identifiers are restricted to ASCII lowercase * Use ASCII-only values for the `realm` parameter * The `description` parameter can contain localized text; use `Accept-Language` to determine appropriate language ## Full specification These docs provide a practical overview. For the full specification: The full specification includes detailed ABNF grammar, security analysis, IANA considerations, and complete examples for various payment scenarios. # HTTP 402 Payment Required \[The status code for payments on the web] MPP services return HTTP `402` Payment Required to indicate that a resource requires payment for access. This is the foundation that enables [API monetization](/use-cases/api-monetization) and [micropayments](/use-cases/micropayments) at the protocol level. ## Overview Respond with `402` when: * A resource requires payment as a precondition for access * The server can provide a `Payment` challenge the client can fulfill * Payment is the primary barrier (not authentication or authorization, which would result in a `401` and then potentially an incremental `402`) ```http HTTP/1.1 402 Payment Required WWW-Authenticate: Payment id="abc123", realm="mpp.dev", method="tempo", intent="charge", request="eyJ..." ``` ## Status code comparison | Condition | Status Code | |-----------|-------------| | Resource requires payment | **`402`** | | Client lacks authentication | `401` | | Client authenticated but unauthorized | `403` | | Resource doesn't exist | `404` | MPP uses `402` consistently for all payment-related challenges, including when a credential fails validation. This differs from other HTTP authentication schemes that use `401` for failed credentials. ## Token authentication When a resource requires both **token** and **payment** authentication: 1. Verify authentication credentials 2. Return `401` if token authentication fails 3. Return `402` with a `Payment` challenge only after successful token authentication This ordering prevents leaking payment requirements to unauthenticated clients. :::info Store authentication tokens in an [HTTP-only cookie](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#httponly-attribute) to prevent them from conflicting with the payment `Authorization` header and to avoid exposing the token to the client. ::: ## Error responses Failed payment attempts return `402` with a fresh challenge and a Problem Details body: ```http HTTP/1.1 402 Payment Required Cache-Control: no-store Content-Type: application/problem+json WWW-Authenticate: Payment id="new456", ... { "type": "https://paymentauth.org/problems/verification-failed", "title": "Payment Verification Failed", "status": 402, "detail": "Invalid payment proof." } ``` Error types include: * `invalid-challenge`—Unknown, expired, or already-used challenge * `malformed-credential`—Invalid base64url or bad JSON * `method-unsupported`—Method not accepted (400) * `payment-expired`—Challenge or authorization expired * `payment-insufficient`—Amount too low * `payment-required`—Resource requires payment * `verification-failed`—Payment proof invalid ## Learn more # Challenges \[Server-issued payment requirements] Your server issues a **challenge** to describe the payment required for a resource. Send challenges in the `WWW-Authenticate` header using the `Payment` authentication scheme. ## Structure ```http WWW-Authenticate: Payment id="qB3wErTyU7iOpAsD9fGhJk", realm="mpp.dev", method="tempo", intent="charge", expires="2025-01-15T12:05:00Z", opaque="eyJyb3V0ZSI6Ii92MS9zZWFyY2gifQ", request="eyJhbW91bnQiOiIxMDAwIiwiY3VycmVuY3kiOiJ1c2QifQ" ``` ### Required parameters | Parameter | Description | |-----------|-------------| | `id` | Unique challenge identifier, cryptographically bound to challenge parameters | | `realm` | Protection space identifier (typically the API domain) | | `method` | Payment method identifier (such as `tempo` or `stripe`) | | `intent` | Payment intent type (such as `charge` or `session`) | | `request` | Base64url-encoded JCS JSON with payment details | ### Optional parameters | Parameter | Description | |-----------|-------------| | `expires` | ISO 8601 timestamp when the challenge expires | | `description` | Human-readable description of what's being paid for | | `opaque` | Base64url-encoded JCS JSON for server-defined correlation data | ## `request` and `opaque` encoding In HTTP headers, both `request` and `opaque` use base64url-encoded [JCS](https://www.rfc-editor.org/rfc/rfc8785) JSON strings. Parse them after header processing to recover the structured values. ## `request` object The `request` parameter contains method-specific payment details encoded as base64url JCS JSON: ```json [Decoded request object] { "amount": "1000", "currency": "usd", "recipient": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" } ``` Servers can also attach correlation data in `opaque`: ```json [Decoded opaque object] { "route": "/v1/search" } ``` Clients must echo `opaque` back unchanged when they submit a Credential. Common fields across payment methods: | Field | Description | |-------|-------------| | `amount` | Payment amount in base units (for example, cents for USD) | | `currency` | Currency code (`usd`) or token address (`0x20c0...`) | | `recipient` | Payment destination in method-native format | ## Multiple challenges Servers can offer multiple payment options in a single response: ```http HTTP/1.1 402 Payment Required WWW-Authenticate: Payment id="abc", method="tempo", ... WWW-Authenticate: Payment id="def", method="stripe", ... ``` Clients select one based on their capabilities and submit a single credential. ## Challenge binding :::warning\[Security requirement] Challenges must be cryptographically bound to their parameters through the `id` field. This prevents clients from reusing a challenge ID with modified payment terms. ::: Typical binding includes: * `realm`, `method`, `intent` * `request` * `expires` * `digest` * `opaque` For HMAC-bound IDs, the canonical input sequence is `realm | method | intent | request | expires | digest | opaque`. When `expires`, `digest`, or `opaque` is absent, use an empty string for that slot. Use an [HMAC-bound](https://en.wikipedia.org/wiki/HMAC) challenge ID to prevent clients from reusing a challenge ID with modified payment terms. ## Learn more # Credentials \[Client-submitted payment proofs] A **Credential** is your response to a [Challenge](/protocol/challenges), proving that you paid or authorized the payment. Send credentials in the `Authorization` header. ## Structure ```http Authorization: Payment eyJjaGFsbGVuZ2UiOnsiaWQiOiJxQjN3RXJUeVU3aU9wQXNEOWZHaEprIiwi... ``` The credential is a base64url-encoded JSON object: ```json { "challenge": { "expires": "2025-01-15T12:05:00Z", "id": "qB3wErTyU7iOpAsD9fGhJk", "intent": "charge", "method": "tempo", "opaque": "eyJyb3V0ZSI6Ii92MS9zZWFyY2gifQ", "realm": "mpp.dev", "request": "eyJhbW91bnQiOiIxMDAwIi4uLn0", }, "payload": { "signature": "0xabc123...", "type": "transaction" }, "source": "did:pkh:eip155:4217:0x1234567890abcdef..." } ``` The echoed Challenge keeps the original HTTP wire values. In a Credential, `challenge.request` and `challenge.opaque` remain the same base64url-encoded JCS JSON strings from the `WWW-Authenticate` header. ### Fields | Field | Description | |-------|-------------| | `challenge` | The [Challenge](/protocol/challenges) being responded to | | `source` | Identity of the payer (address, DID, account ID) | | `payload` | Method-specific payment proof | ## Single-use credentials Each credential is valid for exactly one request. When processing a credential: 1. Verify the `challenge.id` matches an outstanding challenge 2. Verify the challenge has not expired 3. Verify the payment or proof using method-specific procedures 4. Reject any replayed credentials ## Tempo charge payload types Tempo charge currently uses three Credential payload shapes: | `payload.type` | When the client uses it | What the server verifies | |----------------|-------------------------|--------------------------| | `transaction` | Non-zero charge in pull mode | Signed Tempo transaction before broadcast | | `hash` | Non-zero charge in push mode | On-chain receipt for the submitted transaction | | `proof` | Zero-amount identity flow | Signed proof message over the Challenge ID with no on-chain transfer | ## Example ### Tempo charge payment ```json { "challenge": { "id": "zL4xCvBnM6kJhGfD8sAaWe", "intent": "charge", "method": "tempo", "opaque": "eyJyb3V0ZSI6Ii92MS9zZWFyY2gifQ", "realm": "mpp.dev", "request": "eyJhbW91bnQiOiI1MDAwIiwiY3VycmVuY3kiOiJ1c2QiLCJyZWNpcGllbnQiOiIweDc0MmQzNUNjNjYzNEMwNTMyOTI1YTNiODQ0QmM5ZTc1OTVmOGZFMDAifQ" }, "payload": { "signature": "0x1b2c3d4e5f6a7b8c9d0e...", "type": "transaction" }, "source": "did:pkh:eip155:4217:0x1234567890abcdef1234567890abcdef12345678" } ``` The server verifies the signature authorizes a transfer matching the challenge parameters, then submits the payment on-chain. For zero-amount Tempo Challenges, the payload becomes `{"type":"proof","signature":"0x..."}`. The server verifies the proof against the `source` identity instead of broadcasting a transfer. ## Learn more # Receipts \[Server acknowledgment of successful payment] A **Receipt** is the server's acknowledgment of successful payment. Return receipts in the `Payment-Receipt` header on successful responses. :::info The `Payment-Receipt` header is optional. Servers typically include it for auditability, but clients don't need it for correct operation. ::: ## Header ```http HTTP/1.1 200 OK Payment-Receipt: eyJzdGF0dXMiOiJzdWNjZXNzIiwibWV0aG9kIjoidGVtcG8iLCJ0aW1lc3RhbXAiOiIyMDI1LTAxLTE1VDEyOjAwOjAwWiJ9 Content-Type: application/json { "data": "Payment received." } ``` The receipt is a base64url-encoded JSON object. ## Structure ```json { "challengeId": "qB3wErTyU7iOpAsD9fGhJk", "method": "tempo", "reference": "0xtx789abc...", "settlement": { "amount": "1000", "currency": "usd" }, "status": "success", "timestamp": "2025-01-15T12:00:00Z" } ``` ### Fields | Field | Description | |-------|-------------| | `challengeId` | The challenge this receipt responds to | | `method` | Payment method used | | `reference` | Method-specific payment reference (for example, transaction hash or invoice ID) | | `settlement` | Actual amount and currency settled | | `status` | Payment outcome (`success`) | | `timestamp` | When the payment was processed | ## Use cases Receipts enable: * **Auditing**—Clients can log payment confirmations * **Dispute resolution**—Reference IDs link to payment network records * **Reconciliation**—Match payments to requests ## Payment method references | Payment Method | Reference Format | |----------------|------------------| | Tempo | Transaction hash (`0xtx789...`) | | Stripe | PaymentIntent ID (`pi_1234...`) | ## Learn more # Transports \[HTTP, MCP, and WebSocket bindings for payment flows] MPP defines how the Payment authentication scheme operates over different transport protocols. The core protocol targets HTTP, with extensions for other transports like MCP and JSON-RPC. ## Available transports | Transport | Use Case | Spec | |-----------|----------|------| | [HTTP](/protocol/transports/http) | REST APIs, web resources | [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110#section-11) | | [MCP](/protocol/transports/mcp) | AI tool calls using Model Context Protocol | [MCP Transport Spec](https://paymentauth.org) | | [WebSocket](/protocol/transports/websocket) | Bidirectional payment streams | [RFC 6455](https://www.rfc-editor.org/rfc/rfc6455) | | JSON-RPC | Non-MCP JSON-RPC services | [JSON-RPC 2.0](https://www.jsonrpc.org/specification) | ## Transport-agnostic design MPP's core concepts—Challenges, Credentials, and Receipts—remain the same across transports. The encoding and delivery mechanism changes: * **HTTP** uses standard headers (`WWW-Authenticate`, `Authorization`, `Payment-Receipt`) * **MCP** uses JSON-RPC error codes and `_meta` fields * **WebSocket** uses JSON message framing with an `mpp` discriminator Choose the transport that matches your protocol. HTTP for REST APIs, MCP for AI agent tool calls, WebSocket for bidirectional payment streams, or JSON-RPC for non-MCP JSON-RPC services. # HTTP transport \[Payment flows using standard HTTP headers] The HTTP transport is the primary binding for MPP, using standard HTTP headers from [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110#section-11). ## Headers | Direction | Header | Purpose | |-----------|--------|---------| | Server → Client | `WWW-Authenticate: Payment ...` | Challenge | | Client → Server | `Authorization: Payment ...` | Credential | | Server → Client | `Payment-Receipt: ...` | Receipt | ## Example flow :::steps ###
Client
Request a resource ```http GET /api/data HTTP/1.1 Host: api.example.com ``` ###
Server
Send a payment challenge Return the `WWW-Authenticate` header with a `Payment` challenge. ```http HTTP/1.1 402 Payment Required WWW-Authenticate: Payment id="abc123", method="tempo", intent="charge", request="..." ``` ###
Client
Retry with a credential Send the `Authorization` header. ```http GET /api/data HTTP/1.1 Host: api.example.com Authorization: Payment eyJjaGFsbGVuZ2UiOnsiaWQiOiJhYmMxMjMi... ``` ###
Server
Return a receipt Return the `Payment-Receipt` header with a success receipt. ```http HTTP/1.1 200 OK Payment-Receipt: eyJzdGF0dXMiOiJzdWNjZXNzIi4uLn0 Content-Type: application/json {"data": "..."} ``` ::: ## Header encoding Challenges are encoded as [auth-params](https://www.rfc-editor.org/rfc/rfc9110#section-11.2) in `WWW-Authenticate`. The `request` and `opaque` auth-params use base64url-encoded JCS JSON strings. Credentials echo those same challenge values inside the base64url-encoded `Authorization` payload, and receipts use base64url-encoded JSON in `Payment-Receipt`. ## Full specification # MCP and JSON-RPC transport \[Payment flows for AI tool calls] The [Model Context Protocol](https://modelcontextprotocol.io) (MCP) transport enables payments for AI tool calls using JSON-RPC. ## Overview AI agents use MCP to call tools on remote servers. The MCP transport allows these tool calls to require payment, enabling autonomous agent-to-service payments. | MPP Concept | MCP Encoding | |-------------|--------------| | Challenge | JSON-RPC error code `-32042` | | Credential | `_meta.org.paymentauth/credential` | | Receipt | `_meta.org.paymentauth/receipt` | ## Challenge Payment requirements are signaled using JSON-RPC error code `-32042`: ```json { "jsonrpc": "2.0", "id": 1, "error": { "code": -32042, // [!code highlight] "message": "Payment Required", "data": { "httpStatus": 402, "challenges": [{ // [!code highlight] "id": "ch_abc123", "realm": "search.example.com", "method": "tempo", "intent": "charge", "request": { "amount": "10", "currency": "usd", "recipient": "0xa726a1..." } }] } } } ``` ## Credential Credentials are passed in the `_meta` field of the tool call: ```json { "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "web-search", "arguments": {"query": "MCP protocol"}, "_meta": { // [!code highlight] "org.paymentauth/credential": { // [!code highlight] "challenge": { ... }, "source": "0x1234...", "payload": { "signature": "0xabc..." } } } } } ``` ## Receipt Receipts are returned in the result `_meta`: ```json { "jsonrpc": "2.0", "id": 2, "result": { "content": [{ "type": "text", "text": "Search results..." }], "_meta": { // [!code highlight] "org.paymentauth/receipt": { // [!code highlight] "status": "success", "challengeId": "ch_abc123", "method": "tempo" } } } } ``` ## Comparison with HTTP | Aspect | HTTP | MCP | |--------|------|-----| | Challenge delivery | `WWW-Authenticate` header | JSON-RPC error `-32042` | | Credential delivery | `Authorization` header | `_meta.org.paymentauth/credential` | | Receipt delivery | `Payment-Receipt` header | `_meta.org.paymentauth/receipt` | | Encoding | Base64url in headers | Native JSON in body | ## Example flow :::steps ###
Agent
Call a tool ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "web-search", "arguments": {"query": "MCP payments"} } } ``` ###
Server
Return payment challenge Respond with error code `-32042`: ```json { "jsonrpc": "2.0", "id": 1, "error": { "code": -32042, "message": "Payment Required", "data": { "challenges": [{ "id": "ch_abc", "method": "tempo", ... }] } } } ``` ###
Agent
Retry with credential Include the credential in `_meta`: ```json { "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "web-search", "arguments": {"query": "MCP payments"}, "_meta": { "org.paymentauth/credential": { ... } } } } ``` ###
Server
Return result with receipt ```json { "jsonrpc": "2.0", "id": 2, "result": { "content": [{ "type": "text", "text": "Results..." }], "_meta": { "org.paymentauth/receipt": { "status": "success", ... } } } } ``` ::: ## Full specification # WebSocket transport \[Bidirectional payment streams over WebSocket] The WebSocket transport binds MPP payment flows to a persistent WebSocket connection using JSON message framing. Unlike the HTTP transport, the client and server exchange payment messages in-band—no separate requests needed for voucher top-ups. ## Message protocol All messages are JSON objects with an `mpp` field as a discriminator: | Direction | `mpp` value | Payload | Purpose | |-----------|-------------|---------|---------| | Client → Server | `authorization` | `authorization: string` | Send Credential or top-up voucher | | Server → Client | `message` | `data: string` | Application data | | Client → Server | `payment-close-request` | — | Request session close | | Server → Client | `payment-close-ready` | `data: SessionReceipt` | Acknowledge close with final Receipt | | Server → Client | `payment-error` | `status: number, message: string` | Report error | | Server → Client | `payment-need-voucher` | `data: NeedVoucherEvent` | Request more funds | | Server → Client | `payment-receipt` | `data: SessionReceipt` | Confirm Credential | ### Example messages ```json // Client sends Credential { "mpp": "authorization", "authorization": "eyJjaGFsbGVuZ2UiOi..." } // Server confirms payment { "mpp": "payment-receipt", "data": { "status": "success", ... } } // Server streams data { "mpp": "message", "data": "chunk of application data" } // Server requests top-up { "mpp": "payment-need-voucher", "data": { "remaining": "0", ... } } // Client requests close { "mpp": "payment-close-request" } // Server acknowledges close { "mpp": "payment-close-ready", "data": { "status": "success", ... } } ``` ## Connection flow >S: WebSocket connect C->>S: authorization (Credential) S->>C: payment-receipt S-->>C: message (data) S-->>C: message (data) S->>C: payment-need-voucher C->>S: authorization (top-up voucher) S->>C: payment-receipt S-->>C: message (data) C->>S: payment-close-request S->>C: payment-close-ready (final Receipt) `} messageColors={{ 'authorization': { light: '#16a34a', dark: '#4ade80' }, 'payment-receipt': { light: '#16a34a', dark: '#4ade80' }, 'payment-close-ready': { light: '#16a34a', dark: '#4ade80' }, 'payment-need-voucher': { light: '#dc2626', dark: '#f87171' }, }} /> > **Green** arrows represent payment flow (`authorization`, `payment-receipt`). **Red** arrows indicate a payment is required (`payment-need-voucher`). Black arrows are data messages. 1. The client opens a WebSocket connection (`ws://` or `wss://`) 2. The client sends an `authorization` message containing the session Credential 3. The server verifies the Credential and responds with a `payment-receipt` 4. The server streams `message` events with application data 5. When the channel balance depletes, the server sends `payment-need-voucher` 6. The client tops up by sending a new `authorization` with a voucher 7. When the stream ends, the server sends `payment-close-ready` with the final Receipt 8. The client can also initiate close at any time via `payment-close-request` ## When to use WebSocket vs SSE | Aspect | WebSocket | Server-Sent Events | |--------|-----------|---------------------| | Direction | Bidirectional | Server → Client only | | Voucher top-ups | In-band `authorization` message | Separate HTTP request | | Overhead | Single persistent connection | HTTP connection + side-channel | | High-frequency metering | Lower overhead per message | Higher overhead per top-up | | Environment support | Broad (browsers, agents, servers) | Limited in some runtimes | Use the WebSocket transport when you need bidirectional communication—for example, streaming sessions where the client tops up vouchers frequently. Use SSE when the server only needs to push data and top-ups are infrequent. # Discovery \[Let clients automatically discover your API's pricing] ## Overview MPP's discovery system lets clients and agents learn what your endpoints cost before making a request. You serve a standard [OpenAPI 3.1](https://spec.openapis.org/oas/v3.1.0) document at `/openapi.json` with `x-payment-info` extensions that advertise one or more payment offers for each paid operation. Registries aggregate these documents so agents can find paid APIs automatically, and provide value-added services like reputation, search, and analytics. :::info\[Discovery is advisory] Discovery documents are informational hints. The runtime `402` Challenge remains the authoritative source of payment terms. Clients use discovery for display and planning, but defer to the Challenge for actual payment. ::: ## Registries Registries aggregate discovery documents from multiple services, making it easy for clients and agents to find paid APIs. | Registry | Description | How to add | |----------|-------------|------------| | [MPPScan](https://mppscan.com) | Public registry of MPP-enabled services with search and analytics | [Manually register](https://www.mppscan.com/register) in one click | | [MPP Services directory](https://mpp.dev/services) | Curated list of live services on mpp.dev | [Submit a PR](https://github.com/tempoxyz/mpp) to add your service | ## Quick start The `mppx` SDK generates discovery documents from your route configuration. Add `discovery()` to your server and it serves `/openapi.json` automatically. ```ts [server.ts] import { Hono } from 'hono' import { Mppx, discovery } from 'mppx/hono' import { tempo } from 'mppx/server' const app = new Hono() const mppx = Mppx.create({ methods: [ tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0x...', testnet: true, }), ], secretKey: process.env.MPP_SECRET_KEY, }) app.get('/v1/fortune', mppx.charge({ amount: '0.01' }), (c) => c.json({ fortune: 'You will be rich' })) // [!code hl:start] discovery(app, mppx, { auto: true, info: { title: 'Fortune API', version: '1.0.0' }, }) // [!code hl:end] ``` This generates a `GET /openapi.json` endpoint with canonical `x-payment-info.offers[]` entries on each paid route. ### Express ```ts [server.ts] import express from 'express' import { Mppx, discovery } from 'mppx/express' import { tempo } from 'mppx/server' const app = express() const mppx = Mppx.create({ methods: [ tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0x...', testnet: true, }), ], secretKey: process.env.MPP_SECRET_KEY, }) const pay = mppx.charge({ amount: '0.01' }) app.get('/v1/fortune', pay, (req, res) => res.json({ fortune: 'You will be rich' })) // [!code hl:start] discovery(app, mppx, { info: { title: 'Fortune API', version: '1.0.0' }, routes: [{ handler: pay, method: 'get', path: '/v1/fortune' }], }) // [!code hl:end] ``` ### Next.js In Next.js, `discovery()` returns a route handler you export from an API route. ```ts [app/openapi.json/route.ts] import { discovery } from 'mppx/nextjs' import { mppx, pay } from '../fortune/route' // [!code hl:start] export const GET = discovery(mppx, { info: { title: 'Fortune API', version: '1.0.0' }, routes: [{ handler: pay, method: 'get', path: '/api/fortune' }], }) // [!code hl:end] ``` ## How it works Your server exposes a `GET /openapi.json` endpoint that returns an OpenAPI document. Paid operations include an `x-payment-info` extension with one or more payment offers, and the document root can include `x-service-info` for service-level metadata. ```json [/openapi.json] { "openapi": "3.1.0", "info": { "title": "My API", "version": "1.0.0" }, "x-service-info": { "categories": ["ai"], "docs": { "homepage": "https://example.com", "apiReference": "https://example.com/docs", "llms": "/llms.txt" } }, "paths": { "/v1/generate": { "post": { // [!code hl:start] "x-payment-info": { "offers": [ { "amount": "1000000", "currency": "0x20c0000000000000000000000000000000000001", "description": "Generate text with Tempo", "intent": "charge", "method": "tempo" }, { "amount": "100", "currency": "usd", "description": "Generate text with Stripe", "intent": "charge", "method": "stripe" } ] }, // [!code hl:end] "responses": { "200": { "description": "Successful response" }, "402": { "description": "Payment Required" } } } }, "/v1/models": { "get": { "responses": { "200": { "description": "Successful response" } } } } } } ``` ### `x-payment-info` Add this extension to any operation that requires payment. Prefer the canonical multi-offer shape: | Field | Type | Description | |-------|------|-------------| | `offers` | `Offer[]` | Ordered list of payment offers the client can choose from | ### `offers[]` | Field | Type | Description | |-------|------|-------------| | `amount` | `string \| null` | Payment amount in base units | | `currency` | `string` | Currency code or token address | | `description` | `string` | Human-readable description of the charge | | `intent` | `string` | Payment intent (`charge` or `session`) | | `method` | `string` | Payment method identifier (`tempo`, `stripe`) | :::note\[Compatibility shorthand] The flat single-offer form is still valid as shorthand for older emitters, but use it only for backward compatibility. New documents write the same data under `offers[]`. ```json [openapi.json] { // [!code hl:start] "x-payment-info": { "amount": "1000000", "currency": "0x20c0000000000000000000000000000000000001", "description": "Generate text with Tempo", "intent": "charge", "method": "tempo" } // [!code hl:end] } ``` ::: ### `x-service-info` Optional root-level metadata about the service: | Field | Type | Description | |-------|------|-------------| | `categories` | `string[]` | Free-form service categories (for example, `ai`, `payments`) | | `docs.homepage` | `string` | Link to the service homepage | | `docs.apiReference` | `string` | Link to API documentation | | `docs.llms` | `string` | Link to an `llms.txt` file for AI consumption | ## Build manually You can author a discovery document by hand following the [discovery specification](https://paymentauth.org/draft-payment-discovery-00.html). The document is a standard OpenAPI 3.1 file with two extensions: ::::steps ### Create the OpenAPI skeleton Start with a standard OpenAPI 3.1 document: ```json [openapi.json] { "openapi": "3.1.0", "info": { "title": "My API", "version": "1.0.0" }, "paths": {} } ``` ### Add `x-payment-info` to paid operations For each endpoint that requires payment, add the `x-payment-info` extension with an `offers` array. Add more objects to `offers[]` when the client can choose between alternative payment methods or currencies. Amounts are in base units (for example, `1000000` for $1.00 with 6 decimals). ```json [openapi.json] { "paths": { "/v1/generate": { "post": { "summary": "Generate text", // [!code hl:start] "x-payment-info": { "offers": [ { "amount": "1000000", "currency": "0x20c0000000000000000000000000000000000001", "intent": "charge", "method": "tempo" } ] }, // [!code hl:end] "responses": { "200": { "description": "Successful response" }, "402": { "description": "Payment Required" } } } } } } ``` :::warning\[Include a 402 response] Operations with `x-payment-info` must include a `402` response in the `responses` object. Validators flag this as an error if missing. ::: ### Add `x-service-info` (optional) Add service-level metadata to the document root: ```json [openapi.json] { // [!code hl:start] "x-service-info": { "categories": ["ai", "text-generation"], "docs": { "homepage": "https://example.com", "apiReference": "https://example.com/docs/api", "llms": "https://example.com/llms.txt" } } // [!code hl:end] } ``` ### Serve at `/openapi.json` Serve the document at `GET /openapi.json` with appropriate caching: ```http HTTP/1.1 200 OK Content-Type: application/json Cache-Control: public, max-age=300 ``` :::: ## CLI Generate a static discovery document from a config module: ```bash [terminal] $ npx mppx discover generate ./discovery.config.ts ``` Validate an existing discovery document from a file or URL: ```bash [terminal] $ npx mppx discover validate https://example.com/openapi.json ``` ## Validation Common validation issues: | Issue | Severity | Description | |-------|----------|-------------| | Missing `402` response | Error | Operations with `x-payment-info` must include a `402` response | | Invalid amount format | Error | Each `offers[].amount` value must be a non-negative integer string | | Missing `requestBody` | Warning | `POST`/`PUT`/`PATCH` operations without a `requestBody` definition | | Invalid URI in docs | Error | `docs` links must be valid URIs or absolute paths | ## Specification # Identity \[Verify clients without payment] Every MPP Credential carries a cryptographic identity—the client's public key in the `source` field. Your server gets a verified identity on every request, whether the client pays or not. This page covers how to use that identity for access control, gating, and multi-step workflows. ## Overview The Credential already proves the client controls a specific public key. The `source` field is the same regardless of the payment amount, which means you can use the MPP flow purely for identity when payment isn't needed. Extract the client's identity from any verified request using `Credential.fromRequest`: ```ts twoslash [server.ts] // @noEmit declare const request: Request // ---cut--- import { Credential } from 'mppx' const credential = Credential.fromRequest(request) const clientIdentity = credential.source // @log: "did:pkh:eip155:4217:0x1234..." ``` Backends key workloads, sessions, and access control on this public key. The payment flow is orthogonal to identity, and can be used in combination with other methods. ### Methods | Method | Payment methods | Description | |--------|-----------------|-------------| | [**Zero-dollar auth**](#zero-dollar-auth) | Tempo, Lightning, Solana, Custom | Proves key ownership with no funds transfer | :::info\[More methods coming] Additional identity methods are in development. Check back for updates. ::: ## Zero-dollar auth Zero-dollar auth uses the standard Challenge → Credential flow with the amount set to `0`. The client signs the Challenge to prove key ownership. No funds move on-chain, and no additional protocol extensions are required. For Tempo charge, zero-dollar auth now uses a `proof` Credential payload instead of a real transaction. The client signs a proof message over the Challenge ID, and the server verifies that signature against the `source` DID. :::warning\[Replay protection] By default, a valid zero-dollar proof remains reusable until the Challenge expires. Pass a `store` to `tempo.charge()` when you want single-use proof auth. In a multi-instance deployment, use a shared store so every instance sees consumed proofs. ::: >Server: (1) GET /resource Server-->>Client: (2) 402 + Challenge (amount: 0) Note over Client: (3) Sign proof message Client->>Server: (4) GET /resource + Credential Note over Server: (5) Verify proof Server-->>Client: (6) 200 OK `} /> The Credential contains the client's public key and a valid signature, giving the server a verified identity to associate with the request. For Tempo, the server rejects `transaction` and `hash` payloads for zero-amount Challenges and requires `proof`. ### Case study: long-running jobs A service accepts a paid request to start work, then lets the client poll for results using zero-dollar auth. The server keys workloads on the client's public key. ::::steps ### Client submits a job (paid) The client sends a request with payment to create a new job. The Credential includes both payment proof and the client's public key. ```ts twoslash [client.ts] // @noEmit declare const fetch: (url: string) => Promise<{ json(): Promise }> // ---cut--- const response = await fetch('https://api.example.com/v1/jobs') const { jobId } = await response.json() // @log: { jobId: "abc123" } ``` The server records the job and associates it with the client's public key from the Credential. ### Server stores the public key After the payment middleware verifies the Credential, extract the client's identity from the request: ```ts [server.ts] import { Credential } from 'mppx' export async function handler(request: Request) { const result = await mppx.charge({ amount: '1.00' })(request) if (result.status === 402) return result.challenge const credential = Credential.fromRequest(request) const pubkey = credential.source // [!code hl] const jobId = createJob({ owner: pubkey }) // [!code hl] return result.withReceipt(Response.json({ jobId })) } ``` ### Client polls for status (zero-dollar auth) The client polls the job endpoint. The server issues a zero-dollar Challenge—the client signs it to prove they own the same key. ```ts [server.ts] export async function statusHandler(request: Request) { const result = await mppx.charge({ amount: '0' })(request) // [!code hl] if (result.status === 402) return result.challenge const credential = Credential.fromRequest(request) const job = getJob(jobIdFromUrl(request)) if (job.owner !== credential.source) { return Response.json({ error: 'Not your job' }, { status: 403 }) } return result.withReceipt(Response.json({ result: job.result, status: job.status })) } ``` :::: ### Case study: paid unlock with free access A service charges once to unlock a resource, then grants repeated free access tied to the client's identity. This replaces API keys with cryptographic ownership. ::::steps ### Client pays to unlock The client pays once to gain access. The server records the public key as an authorized user. ```ts [server.ts] export async function unlockHandler(request: Request) { const result = await mppx.charge({ amount: '50.00' })(request) if (result.status === 402) return result.challenge const credential = Credential.fromRequest(request) grantAccess({ dataset: 'premium', owner: credential.source }) // [!code hl] return result.withReceipt(Response.json({ status: 'unlocked' })) } ``` ### Client accesses the resource (zero-dollar auth) Subsequent requests use zero-dollar auth. The server checks the client's identity against the access list. ```ts [server.ts] export async function accessHandler(request: Request) { const result = await mppx.charge({ amount: '0' })(request) // [!code hl] if (result.status === 402) return result.challenge const credential = Credential.fromRequest(request) if (!hasAccess({ dataset: 'premium', owner: credential.source })) { return Response.json({ error: 'Not unlocked' }, { status: 403 }) } return result.withReceipt(Response.json({ data: getDataset('premium') })) } ``` :::: ### Case study: multi-step agent workflow An agent orchestrates a pipeline where one paid step kicks off several follow-up steps that only need identity. Each step verifies the same public key to maintain continuity across the workflow. ::::steps ### Agent starts the pipeline (paid) The agent pays to kick off generation. The server returns a pipeline ID tied to the agent's public key. ```ts [server.ts] export async function createPipelineHandler(request: Request) { const result = await mppx.charge({ amount: '5.00' })(request) if (result.status === 402) return result.challenge const credential = Credential.fromRequest(request) const pipelineId = createPipeline({ owner: credential.source }) // [!code hl] return result.withReceipt(Response.json({ pipelineId })) } ``` ### Agent retrieves intermediate results (zero-dollar auth) The agent polls each stage of the pipeline. Every request proves the same identity without additional payment. ```ts [server.ts] export async function stageHandler(request: Request) { const result = await mppx.charge({ amount: '0' })(request) // [!code hl] if (result.status === 402) return result.challenge const credential = Credential.fromRequest(request) const pipeline = getPipeline(pipelineIdFromUrl(request)) if (pipeline.owner !== credential.source) { return Response.json({ error: 'Not your pipeline' }, { status: 403 }) } const stage = pipeline.stages[stageFromUrl(request)] return result.withReceipt(Response.json({ output: stage.output, status: stage.status })) } ``` ### Agent downloads the final artifact (zero-dollar auth) The final download also uses zero-dollar auth—the server already collected payment at the start. ```ts [server.ts] export async function resultHandler(request: Request) { const result = await mppx.charge({ amount: '0' })(request) // [!code hl] if (result.status === 402) return result.challenge const credential = Credential.fromRequest(request) const pipeline = getPipeline(pipelineIdFromUrl(request)) if (pipeline.owner !== credential.source) { return Response.json({ error: 'Not your pipeline' }, { status: 403 }) } return result.withReceipt(Response.json({ result: pipeline.finalResult })) } ``` :::: # Refunds \[Return funds to clients] ## Overview MPP does not define a dedicated refund protocol. How refunds work depends on the payment flow your service uses. ## Charge flow In the charge flow, funds transfer to the server immediately when the client pays. Refunds are **out-of-protocol**—the server sends funds back to the client's address directly. >Server: Request + Credential Note over Server: Funds transfer Server-->>Client: 200 OK + Receipt Note over Server: Refund triggered Server->>Client: Direct transfer back `} /> The server knows the client's address from the Credential's `source` field and can return funds at any time using a standard on-chain transfer. ### Implementation To refund a charge, send the refund amount back to the client's public key: ```ts twoslash [server.ts] // @noEmit declare const credential: { source: string } declare function sendTransfer(params: { amount: string; to: string }): Promise // ---cut--- async function refundCharge(credential: { source: string }, amount: string) { const clientAddress = credential.source await sendTransfer({ amount, to: clientAddress }) } ``` Refund decisions are up to your service. Common triggers include failed processing, service errors, or customer support requests. ## Session flow In the session flow, funds are locked but not immediately claimed. If the server never claims the locked funds, they automatically return to the client after the session expires. This gives sessions a **built-in refund mechanism**—unclaimed funds are refunded by default. >Tempo: Deposit tokens Tempo-->>Client: Channel created Client->>Server: Open credential Server-->>Client: 200 OK (session established) loop Per request Client->>Server: Request + voucher Server-->>Client: 200 OK + Receipt end Note over Server: Close channel with last voucher Server->>Tempo: close(channelId, voucher) Note over Tempo: Settle claimed amount to server Tempo-->>Client: Refund unclaimed deposit `} /> | Scenario | Outcome | |----------|---------| | Server claims funds | Payment completes, no refund | | Server never claims | Funds return to client after session expiry | | Server claims partial amount | Remaining funds return to client | ### Refunding via channel close To refund a session, close the channel with the last settled voucher. Any unclaimed deposit returns to the client automatically. ```ts twoslash [server.ts] // @noEmit declare const session: { close(): Promise<{ txHash: string; refundedToPayer: string }> } // ---cut--- const receipt = await session.close() // @log: { txHash: "0xabc...", refundedToPayer: "8500000" } ``` On the client side, calling `requestClose` followed by `withdraw` after the grace period achieves the same result if the server is unresponsive: ```ts twoslash [client.ts] // @noEmit declare const escrow: { write: { requestClose(args: [`0x${string}`]): Promise; withdraw(args: [`0x${string}`]): Promise } } declare const channelId: `0x${string}` // ---cut--- await escrow.write.requestClose([channelId]) await escrow.write.withdraw([channelId]) ``` ## Best practices * **Track refunds by Credential** — Use the Challenge `id` and Credential `source` to associate refunds with the original payment. * **Refund to the same address** — Always return funds to the `source` address from the Credential. Don't ask the client for a separate refund address. * **Treat refunds as a feedback mechanism** — Like traditional payment systems, refunds avoid costly disputes. Handle them promptly through your service's support process. * **Log refund transactions** — Keep a record of the original payment and the refund transaction for reconciliation. # Security \[Protect server secrets and payment credentials] The core Payment HTTP Authentication Scheme already requires TLS and treats payment Credentials and Receipts as sensitive data. This page covers the operational practices around `MPP_SECRET_KEY` and server deployments. ## Treat `MPP_SECRET_KEY` as root-of-trust material `MPP_SECRET_KEY` binds HMAC-backed Challenge IDs to your server configuration. If an attacker gets the key, they can mint Challenges that appear server-issued for your `realm`. * Keep it on trusted servers only. * Never ship it to browsers, mobile apps, MCP clients, or frontend bundles. * Use a different key for each environment. * Never commit it to git or bake it into container images. ## Store it in a secrets manager Use your platform's secret store as the system of record—AWS Secrets Manager, GCP Secret Manager, Azure Key Vault, HashiCorp Vault, or an equivalent service. Environment variables are a good delivery mechanism at runtime, but they are not a secrets management strategy by themselves. Inject `MPP_SECRET_KEY` into your process from a managed secret store instead of treating `.env` files or deployment manifests as the source of truth. ## Never log secrets or payment credentials Do not log: * `MPP_SECRET_KEY` * `Authorization: Payment` headers * `Payment-Receipt` headers Keep them out of error messages, debugging output, analytics, traces, and support logs. If you need observability, log stable metadata such as request IDs, challenge IDs, status codes, or payment method names instead. ## Handle proxies and caches safely Treat reverse proxies, CDNs, API gateways, and observability pipelines as part of your threat surface. * Send `Cache-Control: no-store` with `402` responses so intermediaries do not cache Challenges. * Send `Cache-Control: private` on successful responses that include `Payment-Receipt`. * Redact `Authorization: Payment` and `Payment-Receipt` headers in proxy logs, trace exporters, and edge analytics. * Do not rely on intermediary-specific `402` handling—verify that your deployment forwards `WWW-Authenticate` headers correctly. ## Bind paid requests to the actual request Use Challenge binding to make sure the paid request matches what your server intended to charge for. * Include a `digest` parameter for `POST`, `PUT`, and `PATCH` requests so clients cannot change the request body after receiving a Challenge. * Verify the expected amount, currency, recipient, and route-level business context when checking a Credential. * Do not use `description` as an authorization input. It is display text, not a security control. ## Rotate with overlap When you rotate `MPP_SECRET_KEY`, use a staged rollout so in-flight Challenges keep working: 1. Start issuing new Challenges with the new key. 2. Continue verifying the previous key during a short overlap window. 3. Remove the old key after outstanding Challenges have expired. If your deployment does not support current-and-previous-key verification yet, do a coordinated rollout and wait for the old Challenge TTL window to pass before invalidating the previous key. ## Respond to exposure immediately If `MPP_SECRET_KEY` is exposed: 1. Rotate it immediately. 2. Remove the old key after your overlap window ends. 3. Scrub logs, traces, and crash reports if the secret landed there. 4. Review issuance and verification telemetry for suspicious activity. 5. Replace the key in every environment where it was reused. ## Prevent replay in production Replay protection must survive concurrency and multi-instance deployments. * Use a shared atomic store when your server runs on more than one instance. * Do not rely on process-local memory for replay protection in distributed deployments. * Check that zero-amount proof flows have explicit replay protection before you use them for production identity or access control. ## Keep local development separate A local `.env` file is fine for development if it stays local and out of git. Commit only `.env.example` with placeholders, use a separate development key, and never reuse production secrets in staging or local environments. ## Related security topics * [Protocol overview](/protocol) * [HTTP 402](/protocol/http-402) * [Tempo charge replay protection](/sdk/typescript/server/Method.tempo.charge) ## Read the underlying guidance * [Payment HTTP Authentication Scheme](/protocol/http-402) * [Frequently asked questions](/faq) * [OWASP Secrets Management Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html) # Payment methods \[Available methods and how to choose one] Payment methods define how clients pay for resources protected by the Machine Payments Protocol. Each method specifies its payment rails, credential format, and verification logic. ## Overview When a server responds with `402` Payment Required, the `WWW-Authenticate` header includes a `method` parameter indicating which payment method to use. If supported, the client can then use the corresponding payment method to generate a Credential and retry the request. ```http HTTP/1.1 402 Payment Required WWW-Authenticate: Payment method="tempo" intent="charge", ... ``` ```http HTTP/1.1 402 Payment Required WWW-Authenticate: Payment method="stripe", intent="charge", ... ``` ```http HTTP/1.1 402 Payment Required WWW-Authenticate: Payment method="card", intent="charge", ... ``` ```http HTTP/1.1 402 Payment Required WWW-Authenticate: Payment method="lightning", intent="charge", ... ``` ```http HTTP/1.1 402 Payment Required WWW-Authenticate: Payment method="solana", intent="charge", ... ``` ```http HTTP/1.1 402 Payment Required WWW-Authenticate: Payment method="stellar", intent="charge", ... ``` ```http HTTP/1.1 402 Payment Required WWW-Authenticate: Payment method="monad", intent="charge", ... ``` ```http HTTP/1.1 402 Payment Required WWW-Authenticate: Payment method="redotpay", intent="charge", ... ``` ## Available methods # Charge \[Immediate one-time payments] The `charge` intent requests an immediate one-time payment. The client pays a fixed amount and the server settles the transaction before returning the response. This is the simplest MPP payment pattern—one request, one payment, one receipt. ## How it works >Server: (1) GET /resource Server-->>Client: (2) 402 + Challenge Note over Client: (3) Fulfill payment Client->>Server: (4) GET /resource + Credential Server->>Network: (5) Settle payment Network-->>Server: (6) Confirmed Server-->>Client: (7) 200 OK + Receipt `} /> 1. **Client:** requests a paid resource 2. **Server:** responds with `402` and a Challenge specifying the payment requirements—`amount`, `currency`, and `recipient` 3. **Client:** fulfills the payment using the method specified in the Challenge 4. **Client:** retries the request with the payment proof as a Credential 5. **Server:** verifies the Credential and settles the payment on the underlying network 6. **Network:** confirms the payment 7. **Server:** returns the resource with a Receipt ## When to use charge Charge is the best intent when each request maps to a single payment with a known cost: * **Paid API endpoints**—Charge per request for data, compute, or content * **Content access**—Pay-per-article, pay-per-query, or pay-per-download * **Tool calls**—MCP tool invocations where each call has a fixed price * **Simple integrations**—No channel setup, no state management, no storage backend For metered billing, high volume flows such as scraping, or usage-based billing where the total cost isn't known upfront, use the session intent instead. ## Request schema The charge intent defines the following request fields: | Field | Type | Required | Description | |---|---|---|---| | `amount` | string | Required | Payment amount in base units | | `currency` | string | Required | Currency identifier (token address, currency code) | | `description` | string | Optional | Human-readable description of the payment | | `expires` | string | Optional | ISO 8601 expiry timestamp | | `externalId` | string | Optional | Server-defined idempotency key | | `recipient` | string | Optional | Recipient identifier (address, account ID) | Payment methods extend this schema with method-specific fields through `methodDetails`. For example, Tempo adds `chainId` and `feePayer`. ## Method integrations Each payment method defines how charge is fulfilled, verified, and settled on its underlying network. ## Specification # Subscription \[Recurring paid access] The `subscription` intent mediates recurring paid access. The client authorizes a fixed payment amount once, and the server reuses that authorization to collect at most one charge per billing period. ## How it works >Server: (1) GET /resource Server->>Store: (2) Resolve subscription Server-->>Client: (3) 402 + subscription Challenge Note over Client: (4) Authorize recurring access Client->>Server: (5) GET /resource + Credential Server->>Network: (6) Activate subscription and charge first period Network-->>Server: (7) Confirmed Server->>Store: (8) Store subscription state Server-->>Client: (9) 200 OK + Receipt Client->>Server: (10) Later request Server->>Store: (11) Find active subscription Server-->>Client: (12) 200 OK + Receipt `} /> 1. **Client:** requests a protected resource 2. **Server:** resolves whether the request already has an active subscription 3. **Server:** responds with `402` and a subscription Challenge when no usable subscription exists 4. **Client:** authorizes the recurring payment terms 5. **Client:** retries the request with a Credential 6. **Server:** verifies the Credential, activates the subscription, and charges the first billing period 7. **Server:** stores durable subscription state and returns a Receipt 8. **Server:** reuses the active subscription for later requests while the current period is paid ## When to use subscription Subscription is the best intent when access renews on a fixed schedule: * **API plans**—Charge a fixed amount per day, week, or month * **Memberships**—Keep access active across many requests * **Recurring MCP access**—Bill for tool access that renews by period * **Usage bundles**—Renew a fixed bundle on a schedule Use `charge` when each request maps to one payment. Use `session` when usage is metered and the final cost isn't known upfront. ## Request schema The subscription intent defines the following request fields: | Field | Type | Required | Description | |---|---|---|---| | `amount` | string | Required | Fixed payment amount per billing period in base units | | `currency` | string | Required | Currency identifier (token address, currency code) | | `description` | string | Optional | Human-readable subscription description | | `externalId` | string | Optional | Server-defined subscription reference | | `methodDetails` | object | Optional | Method-specific extension data | | `periodCount` | string | Required | Positive integer count of `periodUnit` values per billing period | | `periodUnit` | string | Required | Billing period unit: `day`, `week`, or `month` | | `recipient` | string | Optional | Recipient identifier (address, account ID) | | `subscriptionExpires` | string | Optional | RFC 3339 timestamp that bounds the recurring authorization | Payment methods extend this schema through `methodDetails`. They must reject subscription requests they can't represent exactly. ## Lifecycle Activation starts the first billing period and collects the first charge. The server returns a Receipt with a `subscriptionId` only after activation succeeds. Renewal collects at most one charge for each later billing period. Before granting access in an unpaid period, the server must collect the renewal charge or fail the request with `402`. Reuse is application-defined. Servers use authenticated session state, account identity, resource scope, or another local selector to associate later requests with an active subscription. A `subscriptionId` alone doesn't grant access. ## Method integrations Each payment method defines how subscription authorization, activation, renewal, and cancellation map to its underlying network. ## Specification # Tempo \[Stablecoin payments on the Tempo blockchain] The [Tempo](https://docs.tempo.xyz) payment method enables payments using TIP-20 stablecoins on the Tempo blockchain. Tempo supports **charge** for one-time payments, **session** for pay-as-you-go payment channels, and **subscription** for recurring access. ## Payments on Tempo Tempo is purpose-built for the payment patterns MPP enables: * **Instant finality**—Transactions settle in ~500ms with deterministic confirmation, no probabilistic waiting * **Sub-cent fees**—Transaction costs low enough for micropayments and per-request billing * **Fee sponsorship**—Servers can pay gas fees on behalf of clients, removing wallet UX friction entirely * **2D nonces**—Parallel nonce lanes let clients submit payment transactions without blocking other account activity * **Payment lane**—Dedicated transaction ordering for payment channel operations, providing reliable channel management UX * **High throughput**—Tempo's throughput handles the on-chain settlement and channel management volume that payment sessions generate at scale ## Choosing a payment method | | **Charge** | **Session** Recommended | **Subscription** New | |---|---|---|---| | **Pattern** | One-time payment per request | Continuous pay-as-you-go | Recurring access | | **Latency overhead** | ~500ms (on-chain confirmation) | Near-zero | Near-zero after activation | | **Throughput** | One transaction per request | Hundreds of vouchers per second per channel | One renewal per period | | **Best for** | Single API calls, content access, one-off purchases | LLM APIs, metered services, usage-based billing | Plans, memberships, recurring API access | | **On-chain cost** | Per request (0.001 USD per request) | Amortized across many requests (0.001 USD total) | Per billing period | | **Settlement** | Immediate on-chain transaction | Off-chain vouchers, periodic on-chain settlement | Key-authorized recurring transfers | ## Intents ## Fee sponsorship Tempo supports server-paid transaction fees for charge, session, and subscription intents. When enabled, the client signs only the payment authorization and the server covers gas costs. The client doesn't need to hold gas tokens or understand fee mechanics. Pass a `feePayer` account to `tempo()` to enable this: ```ts twoslash import { Mppx, tempo } from 'mppx/server' import { privateKeyToAccount } from 'viem/accounts' const mppx = Mppx.create({ methods: [tempo({ feePayer: privateKeyToAccount('0x…'), // [!code hl] })], }) ``` It is also possible to point the `feePayer` to a fee service that supports the [`Handler.feePayer`](https://docs.tempo.xyz/sdk/typescript/server/handler.feePayer) endpoint: ```ts import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo({ feePayer: 'https://sponsor.example.com', // [!code hl] })], }) ``` # Tempo charge \[One-time TIP-20 token transfers] The Tempo implementation of the [charge](/intents/charge) intent. For non-zero charges, the client signs a TIP-20 `transfer` transaction, the server broadcasts it to Tempo, and settlement completes in ~500ms with deterministic finality. For zero-amount identity flows, the client sends a `proof` Credential payload instead of a real transaction. The server verifies the signed proof message against the client's `source` identity and returns a Receipt without broadcasting anything on-chain. This method is best for single API calls, content access, or one-off purchases. ## Server Use [`mppx.charge`](/sdk/typescript/server/Method.tempo.charge) to gate any endpoint behind a one-time payment. The method handles Challenge generation, Credential verification, transaction broadcast for paid requests, and Receipt creation. ```ts twoslash import { Mppx, tempo } from "mppx/server"; const mppx = Mppx.create({ methods: [tempo()], }); export async function handler(request: Request) { const result = await mppx.charge({ amount: "0.1", currency: "0x20c0000000000000000000000000000000000000", recipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", })(request); if (result.status === 402) return result.challenge; return result.withReceipt(Response.json({ data: "..." })); } ``` ### With expiry ```ts twoslash import { Mppx, tempo } from "mppx/server"; const mppx = Mppx.create({ methods: [tempo()] }); // ---cut--- import { Expires } from "mppx"; export async function handler(request: Request) { const result = await mppx.charge({ amount: "0.1", currency: "0x20c0000000000000000000000000000000000000", expires: Expires.minutes(10), // [!code hl] recipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", })(request); if (result.status === 402) return result.challenge; return result.withReceipt(Response.json({ data: "..." })); } ``` ### With fee sponsorship ```ts twoslash import { Mppx, tempo } from "mppx/server"; const mppx = Mppx.create({ methods: [tempo()] }); declare const request: Request; // ---cut--- const result = await mppx.charge({ amount: "0.1", currency: "0x20c0000000000000000000000000000000000000", feePayer: true, // [!code hl] recipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", })(request); ``` When `feePayer` is `true`, the server adds a fee payer signature (domain `0x78`) before broadcasting. The client doesn't need gas tokens. See [fee sponsorship](/payment-methods/tempo#fee-sponsorship) for details. ### Zero-dollar auth Set `amount: "0"` to issue an identity-only Challenge. The client returns a `proof` payload instead of `transaction` or `hash`, and the server verifies the signature against the `source` DID. ```ts twoslash import { Mppx, tempo } from "mppx/server"; const mppx = Mppx.create({ methods: [tempo()] }); declare const request: Request; // ---cut--- const result = await mppx.charge({ amount: "0", // [!code hl] currency: "0x20c0000000000000000000000000000000000000", recipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", })(request); ``` Use this for polling, free follow-up requests, and unlock flows after an initial payment. Pass `store` to `tempo.charge()` when you want single-use proofs. ### With split payments Split a charge across multiple recipients in a single transaction. The primary `recipient` receives `amount` minus the sum of all splits. ```ts twoslash import { Mppx, tempo } from "mppx/server"; const mppx = Mppx.create({ methods: [tempo()] }); declare const request: Request; // ---cut--- const result = await mppx.charge({ amount: "1.00", currency: "0x20c0000000000000000000000000000000000000", // pathUSD recipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", // seller splits: [ // [!code hl] { // [!code hl] amount: "0.10", // [!code hl] recipient: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", // platform fee // [!code hl] }, // [!code hl] ], // [!code hl] })(request); ``` Up to 10 splits per charge. Each split must have a positive amount, and the sum of all splits must be less than the total `amount`. See the [split payments guide](/guides/split-payments) for more details. ### Payment links Set `html: true` on the method to render a payment page when a browser navigates to the endpoint. The page shows a "Continue with Tempo" button—after the user pays, the page reloads with the paid resource. Programmatic clients with `Authorization` headers are unaffected. ```ts twoslash import { Mppx, tempo } from "mppx/server"; const mppx = Mppx.create({ methods: [ tempo.charge({ html: true, // [!code hl] }), ], }); declare const request: Request; // ---cut--- const result = await mppx.charge({ amount: "0.1", currency: "0x20c0000000000000000000000000000000000000", recipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", })(request); ``` See the [payment links guide](/guides/payment-links) for a full walkthrough with framework examples and a live demo. ### With Stripe ```ts twoslash import { Mppx, tempo } from "mppx/server"; async function createPayToAddress(request: Request): Promise<`0x${string}`> { void request; return "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; } export async function handler(request: Request) { const recipientAddress = await createPayToAddress(request); const mppx = Mppx.create({ methods: [ tempo.charge({ currency: "0x20c0000000000000000000000000000000000000", recipient: recipientAddress, }), ], }); const result = await mppx.charge({ amount: "0.01" })(request); if (result.status === 402) return result.challenge; return result.withReceipt(Response.json({ data: "..." })); } ``` Use Stripe to create a dynamic recipient address per payment, each backed by a PaymentIntent. To learn more, read the [Stripe documentation](https://docs.stripe.com/payments/machine/mpp) on accepting MPP. :::info See [`tempo.charge` server reference](/sdk/typescript/server/Method.tempo.charge) for the full parameter list. ::: ## Client Use [`tempo.charge`](/sdk/typescript/client/Method.tempo.charge) with `Mppx.create` to automatically handle `402` responses. For non-zero amounts, the client signs a TIP-20 transfer and retries with the Credential. For zero-amount Challenges, it signs a proof message and retries with a `proof` payload instead. ```ts twoslash import { Mppx, tempo } from "mppx/client"; import { privateKeyToAccount } from "viem/accounts"; const account = privateKeyToAccount("0xabc…123"); Mppx.create({ methods: [tempo.charge({ account })], }); const response = await fetch("https://api.example.com/resource"); ``` ### Without polyfill If you don't want to patch `globalThis.fetch`, use `mppx.fetch` directly: ```ts twoslash import { Mppx, tempo } from "mppx/client"; import { privateKeyToAccount } from "viem/accounts"; const account = privateKeyToAccount("0xabc…123"); const mppx = Mppx.create({ methods: [tempo.charge({ account })], polyfill: false, }); const response = await mppx.fetch("https://api.example.com/resource"); ``` :::info See [`tempo.charge` client reference](/sdk/typescript/client/Method.tempo.charge) for the full parameter list. ::: ### Auto-swap When the client doesn't hold the requested currency, `autoSwap` automatically swaps from a fallback stablecoin (pathUSD, USDC.e) via the Tempo DEX before transferring. ```ts twoslash import { Mppx, tempo } from "mppx/client"; import { privateKeyToAccount } from "viem/accounts"; const account = privateKeyToAccount("0xabc…123"); Mppx.create({ methods: [ tempo.charge({ account, autoSwap: true, // [!code hl] }), ], }); ``` Pass an object for custom fallback tokens or slippage: ```ts twoslash import { Mppx, tempo } from "mppx/client"; import { privateKeyToAccount } from "viem/accounts"; const account = privateKeyToAccount("0xabc…123"); Mppx.create({ methods: [ tempo.charge({ account, autoSwap: { // [!code hl] slippage: 2, // max slippage % (default: 1) // [!code hl] tokenIn: ["0x0000000000000000000000000000000000000001"], // [!code hl] }, // [!code hl] }), ], }); ``` See [auto-swap](/payment-methods/tempo#auto-swap) for more details. ## Specification # Session \[Low-cost high-throughput payments] The `session` intent enables high-frequency, pay-as-you-go payments over unidirectional payment channels. Clients deposit funds into an on-chain escrow and sign off-chain vouchers as they consume resources. The server verifies vouchers with fast signature checks—no RPC or blockchain calls—and settles periodically in batches. Payment sessions reduce payment verification to near constant time, making it possible to meter and bill at the granularity of individual LLM tokens, API calls, or bytes transferred. ## Why sessions matter in MPP Traditional payment rails target human purchase flows: a buyer decides, pays, and receives goods. Usage-based billing—the model that powers cloud infrastructure, LLM APIs, and metered services—requires something fundamentally different. It needs payment verification that can keep pace with the service itself. Consider an LLM API: a single inference request can generate hundreds of tokens over several seconds. Each token has a known cost, but the total cost isn't known when the request begins. Standard billing models handle this by accumulating usage and charging after the fact, introducing credit risk, reconciliation complexity, and billing disputes. Prepaid credit systems require the client to guess consumption upfront and lose unused funds. Sessions on Tempo solve this by making payment a continuous, inline part of the HTTP request. The client signs a cumulative voucher for each increment of service consumed, and the server verifies it in microseconds. The server delays on-chain settlement to whenever it chooses, batching hundreds or thousands of vouchers into a single on-chain transaction. This reduces both the latency and the cost of payment verification to near zero. ## How it works ### Overview >Tempo: (1) Deposit tokens Tempo-->>Client: Channel created Client->>Server: (2) Open credential Note over Server: Verify on-chain deposit Server-->>Client: 200 OK (session established) loop Per request Client->>Server: (3) Request + voucher Note over Server: recover signature (ecrecover) Server-->>Client: 200 OK + Receipt end Note over Server: (4) Periodic settlement Server->>Tempo: settle(channelId, voucher) Client->>Server: (5) Close Server->>Tempo: close(channelId, voucher) Tempo-->>Client: Refund remaining deposit `} /> A payment session has four phases: ::::steps ### Open The client deposits funds into an on-chain escrow contract, creating a payment channel between the client (payer) and server (payee). A unique `channelId` identifies the channel and holds the deposited TIP-20 tokens. ### Session The client signs EIP-712 vouchers with increasing cumulative amounts as service is consumed. Each voucher authorizes "I have now consumed up to X total." The server verifies the signature, checks that the cumulative amount is higher than the previous voucher, and grants access based on the delta. Voucher verification is CPU-bound: a single `ecrecover` call against the EIP-712 typed data. No RPC calls. No database lookups in the critical path. This is what enables per-token LLM billing without adding latency. ### Top up If the channel runs low on funds, the client deposits additional tokens without closing the channel. The session continues uninterrupted. ### 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 and refunding any unused deposit to the client. :::: ## Session receipts Session Receipts differ from charge Receipts. The `reference` field contains the payment channel ID (a `bytes32` hash), not a transaction hash. The on-chain settlement transaction hash is only available after closing the channel. | Field | Charge receipt | Session receipt | |-------|---------------|-----------------| | `reference` | Transaction hash | Channel ID | | `status` | `"success"` | `"success"` | | `method` | `"tempo"` | `"tempo"` | To get the settlement transaction hash, close the channel via `session.close()` and read the `txHash` field from the returned receipt. ## High volume API billing Payment sessions match the billing model that high-volume APIs need: pay stablecoin tokens, receive API responses. The granularity of payment matches the granularity of consumption. A typical flow for a high-volume large language model API: 1. **Client:** opens a channel with a 10 USDC.e deposit 2. **Client:** sends a prompt to the API 3. **Server:** issues Challenges requesting payment for each chunk (for example, 0.000025 USDC.e per token) 4. **Client:** signs a voucher for each chunk—the cumulative amount increases by the cost of tokens received 5. **Server:** verifies the voucher signature (~microseconds) and sends the next chunk 6. **Server:** settles on-chain and the client gets the unused deposit back The server never touches the chain during inference. Payment verification adds microseconds of CPU overhead per chunk, not hundreds of milliseconds of network latency. :::info\[Why Tempo] Tempo handles payments at scale and has properties that make it a uniquely good fit for payment sessions: * **Channel management UX**—Opening, topping up, and closing channels are on-chain operations. Tempo's ~500ms finality and sub-cent fees keep channel lifecycle from becoming a UX bottleneck. * **Payment lane**—Tempo's 2D nonce system provides dedicated nonce lanes for payment transactions, so channel operations don't block other account activity. This matters for clients that use the same account for payments and other on-chain interactions. * **High throughput**—When a server settles thousands of channels, Tempo's throughput handles the settlement volume without congestion or fee spikes. * **Fee sponsorship**—Servers can pay channel management fees on behalf of clients, making the client-side integration purely off-chain after the initial deposit. * **Enshrined tokens**—TIP-20 tokens are precompile-based, not smart contracts. Token operations are cheaper and more predictable than ERC-20 interactions on other chains. ::: ## Integration
Use [`tempo`](/sdk/typescript/server/Method.tempo.session) to accept payment sessions. The server needs an RPC URL for on-chain verification during channel open/close, and a store backend for channel state. ```ts twoslash import { Mppx, Store, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [ tempo({ recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', store: Store.memory(), }), ], }) ``` `Store.memory()` works for local development. For multi-instance deployments, use `Store.redis()`, `Store.upstash()`, or `Store.cloudflare()` so channel state is shared across processes. During a session, the server verifies each voucher with a single `ecrecover`—no RPC calls, no database writes in the hot path. On-chain interaction only happens during open, settlement, and close. Use `mppx.session` in your request handler to meter access: ```ts twoslash import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [ tempo.session({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', }), ], }) // ---cut--- export async function handler(request: Request) { const result = await mppx.session({ amount: '25', unitType: 'llm_token', })(request) if (result.status === 402) return result.challenge return result.withReceipt(Response.json({ data: '...' })) } ```
Use [`tempo`](/sdk/typescript/client/Method.tempo) with `Mppx.create` to sign vouchers automatically when the server requests payment sessions. ```ts twoslash import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0xabc…123') Mppx.create({ methods: [tempo({ account })], }) const response = await fetch('https://api.example.com/v1/chat/completions') // Automatically opens channel, signs vouchers per chunk ``` ### Without polyfill If you don't want to patch `globalThis.fetch`, use `mppx.fetch` directly: ```ts twoslash import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0xabc…123') const mppx = Mppx.create({ methods: [tempo.session({ account })], polyfill: false, }) const response = await mppx.fetch('https://api.example.com/v1/chat/completions') ``` ### With multiple methods Register multiple methods so the client can handle servers that offer multiple payment methods. For example, to accept both charge and payment sessions: ```ts twoslash import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0xabc…123') Mppx.create({ methods: [ tempo.charge({ account }), tempo.session({ account }), ], }) ``` ### Closing the channel Channels remain open for reuse across requests. Call `session.close()` to settle on-chain and reclaim unspent deposit. :::warning Channels do not close automatically. If you don't call `close()`, the deposit stays locked in the escrow contract until the channel expires or is manually closed. ::: See [`tempo.session` manager](/sdk/typescript/client/Method.tempo.session-manager) for the full session lifecycle API.
## Escrow contract Sessions use the `TempoStreamChannel` escrow contract for on-chain deposits, settlement, and channel close. The [IETF Specification](https://paymentauth.org/draft-tempo-session-00) documents the escrow model and voucher format. | Network | Chain ID | Contract Address | |---|---|---| | [Mainnet](https://explore.mainnet.tempo.xyz/address/0x33b901018174DDabE4841042ab76ba85D4e24f25?tab=contract) | 4217 | `0x33b901018174DDabE4841042ab76ba85D4e24f25` | | [Testnet (Moderato)](https://explore.testnet.tempo.xyz/address/0xe1c4d3dce17bc111181ddf716f75bae49e61a336?tab=contract) | 42431 | `0xe1c4d3dce17bc111181ddf716f75bae49e61a336` | ## Specification # Tempo subscription \[Recurring billing] The `subscription` intent enables recurring stablecoin payments on Tempo with reusable access authorization. Use subscriptions when access has a fixed price per billing period: paid plans, premium API tiers, recurring MCP tool access, and usage bundles that renew on a schedule. ## Why subscriptions matter Charges work well for one-time purchases. Sessions work well when usage changes inside a request. Subscriptions cover the third common pattern: the client authorizes recurring access once, and the server bills each period without asking the client to sign every request. A Tempo subscription uses a key authorization. The client authorizes a scoped access key to transfer a fixed amount of a specific TIP-20 token to a specific recipient once per period until `subscriptionExpires`. The server stores the active subscription record and returns Receipts on later requests without another Credential while the current period is paid. ## Choosing a payment method | | **Charge** | **Session** | **Subscription** New | |---|---|---|---| | **Pattern** | One-time payment | Pay-as-you-go usage | Recurring access | | **Client action** | Sign each paid transfer | Open channel and sign vouchers | Authorize an access key once | | **Server hot path** | Verify and broadcast transfer | Verify voucher signatures | Resolve active subscription | | **Best for** | Single API calls and purchases | LLM tokens, bytes, streamed usage | Plans, recurring API access, memberships | | **Renewal** | None | Top up channel as needed | Bill each day or week | ## Flow >Server: Protected request Server-->>Client: 402 subscription Challenge Note over Client: Authorize access key Client->>Server: Retry with Credential Server->>Tempo: Transfer first period Tempo-->>Server: Transaction hash Server-->>Client: 200 OK + Receipt Client->>Server: Later request Server-->>Client: 200 OK + Receipt `} /> ## Activation The first request activates the subscription. The server resolves the request to a stable lookup key, such as `user:123:plan:pro`, and includes an access key in the Challenge. The client signs a `keyAuthorization` Credential that binds: * `amount` * `currency` * `periodCount` * `periodUnit` * `recipient` * `subscriptionExpires` * `accessKey` The server verifies the Credential, charges the first period, stores a `SubscriptionRecord`, and returns a Receipt with a `subscriptionId`. ## Access reuse After activation, future requests do not need another Credential while the subscription is active and current. The server calls `resolve`, finds the subscription record for the route or user, validates that it still matches the request terms, and returns a Receipt. >Server: Request protected resource Server->>Store: Lookup active subscription by resolved key Store-->>Server: SubscriptionRecord Server->>Server: Check expiry, request binding, paid period Server-->>Client: 200 OK + Receipt `} /> ## Renewals When the next billing period starts, the server renews the subscription before granting access. The SDK uses an atomic store lock so concurrent requests do not charge the same period twice. If one request is already renewing, another request receives `409` with `Retry-After: 1`. >Store: Lock renewal period RequestB->>Store: Try same renewal Store-->>RequestB: In flight RequestA->>Tempo: Transfer period payment Tempo-->>RequestA: Transaction hash RequestA->>Store: Commit renewed record RequestA-->>RequestA: 200 OK + Receipt RequestB-->>RequestB: 409 Retry-After `} /> You can renew in the request path with `renew`, or run renewals from a background worker with [`tempo.renewSubscription`](/sdk/typescript/server/Method.tempo.renewSubscription). ## Cancellation Cancel a Tempo subscription by marking its stored `SubscriptionRecord` with `canceledAt`. `mppx` treats records with `canceledAt` or `revokedAt` as inactive, so later protected requests return a new `402` Challenge instead of reusing or renewing the old subscription. The recommended client flow is to call your cancellation endpoint first, then optionally revoke the Tempo access key as a backstop. Server cancellation controls product access. Access-key revocation blocks future on-chain renewal attempts, but it doesn't update the merchant's stored subscription record by itself. ```ts twoslash import { Store } from 'mppx/server' import { Subscription } from 'mppx/tempo' const store = Store.memory() const subscriptions = Subscription.fromStore(store) export async function cancelSubscription(userId: string) { const subscription = await subscriptions.getByKey(`user:${userId}:plan:pro`) if (!subscription) return false await subscriptions.put({ ...subscription, canceledAt: new Date().toISOString(), }) return true } ``` Keep the canceled record for audit and reconciliation. If the client subscribes again, activation creates a new `subscriptionId` for the same resolved lookup key. On Tempo, clients that know the authorized access key can revoke it from the payer account: ```ts twoslash import { createClient, http } from 'viem' import { tempo } from 'viem/chains' import { privateKeyToAccount } from 'viem/accounts' import { Actions } from 'viem/tempo' const client = createClient({ account: privateKeyToAccount( '0x0000000000000000000000000000000000000000000000000000000000000001', // your account ), chain: tempo, transport: http(), }) await Actions.accessKey.revokeSync(client, { accessKey: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', }) ``` ## Receipts Subscription Receipts confirm activation or renewal. The `reference` field is the Tempo transaction hash for the period payment. | Field | Description | |---|---| | `externalId` | Optional app-defined reference | | `method` | Always `"tempo"` | | `reference` | Tempo transaction hash | | `status` | Always `"success"` | | `subscriptionId` | Server-issued subscription identifier | | `timestamp` | Receipt timestamp | ## Integration ### Server Register `tempo.subscription()` explicitly. The `tempo()` convenience helper registers charge and session intents, but it doesn't register subscriptions. ```ts twoslash import { Mppx, Store, tempo } from 'mppx/server' const store = Store.memory() const mppx = Mppx.create({ methods: [ tempo.subscription({ amount: '1.00', currency: '0x20c0000000000000000000000000000000000000', periodCount: '1', periodUnit: 'week', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', resolve: async ({ input }) => { const userId = input.headers.get('X-User-Id') return userId ? { key: `user:${userId}:plan:pro` } : null }, store, subscriptionExpires: new Date('2027-01-01T00:00:00.000Z'), }), ], }) export async function handler(request: Request) { const result = await mppx.tempo.subscription({})(request) if (result.status === 402) return result.challenge const response = result.withReceipt(Response.json({ plan: 'pro' })) console.log(response.status) // @log: 200 return response } ``` :::warning Use a durable atomic store such as Redis, Upstash, or Cloudflare KV for production. `Store.memory()` is for local development. ::: ### Client Register `tempo.subscription()` on the client. The SDK signs the access-key authorization and retries the request after the server returns a subscription Challenge. ```ts twoslash import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0xabc…123') Mppx.create({ methods: [tempo.subscription({ account })], }) const response = await fetch('https://api.example.com/pro') console.log(response.status) // @log: 200 ``` ## Advanced options ### Custom activation Pass `activate` when your application owns settlement and record creation. The SDK still verifies the `keyAuthorization` Credential and validates the returned Receipt and subscription record. ### Custom access keys Pass `accessKey` or return `accessKey` from `resolve` when you want to use an existing access key. Omit it for the recommended path: the server generates and stores one access key per resolved subscription key. ### Background renewal Use [`tempo.renewSubscription`](/sdk/typescript/server/Method.tempo.renewSubscription) from a cron job when you want billing to happen before the next user request. ## Related # Stripe \[Cards, wallets, and other Stripe supported payment methods] The Stripe payment method enables payments using [Shared Payment Tokens (SPTs)](https://docs.stripe.com/agentic-commerce/concepts/shared-payment-tokens)—tokens that allow a client to share a customer's payment method with your Stripe account, with configurable expiration and usage limits. SPTs provide you basic visibility into the payment method (such as card brand and last four digits), while keeping the customer's actual payment details separate. Both the client and server need a Stripe account. The client creates an SPT using the Stripe API or Stripe.js, and the server consumes it to create a Stripe `PaymentIntent`. ::::info\[Stripe account setup] Machine payments must be enabled on your Stripe account before you can accept MPP payments. [Request access](https://docs.stripe.com/payments/machine#sign-up) through the Stripe Dashboard. :::: To learn more, read the [Stripe documentation](https://docs.stripe.com/payments/machine/mpp) on accepting MPP. ::::tip\[For agents] Use the [Link CLI](/tools/wallet#link-cli) to pay for `stripe.charge` services from a Link wallet without writing integration code. `link-cli mpp pay` handles the full 402 → SPT → retry flow automatically. :::: ## How it works >Server: (1) GET /resource Server-->>Client: (2) 402 + Challenge Client->>Stripe: (3) Create SPT from Challenge Stripe-->>Client: (4) spt_... Client->>Server: (5) GET /resource + Credential Server->>Stripe: (6) Create PaymentIntent (using SPT) Stripe-->>Server: (7) pi_... Server-->>Client: (8) 200 OK + Receipt `} /> 1. **Server** responds with `402` and a Challenge containing the amount, currency, and Stripe method details (Business Network profile, allowed payment method types). 2. **Client** collects a payment method (via Stripe Elements or a stored method), then creates an SPT through the Stripe API with usage limits matching the Challenge. 3. **Client** sends a Credential containing the SPT. 4. **Server** creates a Stripe `PaymentIntent` using the SPT and confirms it. 5. **Server** returns the resource with a Receipt referencing the `PaymentIntent`. ## Intents # Stripe charge \[One-time payments using Shared Payment Tokens] The Stripe implementation of the [charge](/intents/charge) intent. The client creates a [Shared Payment Token (SPT)](https://docs.stripe.com/agentic-commerce/concepts/shared-payment-tokens) and sends it as a Credential. The server creates a Stripe `PaymentIntent` using the SPT, and settlement completes through Stripe's payment rails. Use this method for single API calls, content access, or one-off purchases where you want to accept cards, wallets, or other Stripe-supported payment methods. ## Server Use `stripe.charge` to require a one-time Stripe payment before returning a response. The method handles Challenge generation, Credential verification, PaymentIntent creation, and Receipt generation. You can provide either a `client` (a pre-configured Stripe SDK instance) or a raw `secretKey`. Using `client` is recommended — it lets you configure retries, API version, and other options on the Stripe instance you control. ### With Stripe SDK client (recommended) ```ts twoslash import Stripe from 'stripe' import { Mppx, stripe } from 'mppx/server' const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!) const mppx = Mppx.create({ methods: [ stripe.charge({ client: stripeClient, // [!code hl] networkId: 'internal', paymentMethodTypes: ['card'], }), ], }) export async function handler(request: Request) { const result = await mppx.charge({ amount: '1', currency: 'usd', decimals: 2, description: 'Premium API access', })(request) if (result.status === 402) return result.challenge return result.withReceipt(Response.json({ data: '...' })) } ``` ### With secret key If you don't need to customize the Stripe SDK instance, pass a `secretKey` directly and mppx makes raw API calls to Stripe. ```ts twoslash import { Mppx, stripe } from 'mppx/server' const mppx = Mppx.create({ methods: [ stripe.charge({ secretKey: process.env.STRIPE_SECRET_KEY!, // [!code hl] networkId: 'internal', paymentMethodTypes: ['card'], }), ], }) ``` ### With metadata Include `metadata` in the `stripe.charge` configuration to forward key-value pairs to Stripe. The metadata appears in the Challenge and attaches to the Stripe `PaymentIntent`. ```ts twoslash import Stripe from 'stripe' import { Mppx, stripe } from 'mppx/server' const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!) const mppx = Mppx.create({ methods: [ stripe.charge({ client: stripeClient, metadata: { plan: 'pro' }, // [!code hl] networkId: 'internal', paymentMethodTypes: ['card'], }), ], }) ``` ### With multiple payment method types Allow multiple payment methods, like cards and Link, by specifying them in `paymentMethodTypes`. ```ts twoslash import Stripe from 'stripe' import { Mppx, stripe } from 'mppx/server' const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!) const mppx = Mppx.create({ methods: [ stripe.charge({ client: stripeClient, networkId: 'internal', paymentMethodTypes: ['card', 'link'], // [!code hl] }), ], }) ``` ### Payment links Set `html` on the method to render a Stripe Elements payment form when a browser visits the endpoint. ```ts twoslash import Stripe from 'stripe' import { Mppx, stripe } from 'mppx/server' const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!) const mppx = Mppx.create({ methods: [ stripe.charge({ client: stripeClient, html: { // [!code hl] createTokenUrl: '/api/create-spt', // [!code hl] publishableKey: process.env.STRIPE_PUBLISHABLE_KEY!, // [!code hl] }, // [!code hl] networkId: 'internal', paymentMethodTypes: ['card'], }), ], }) ``` ### html.createTokenUrl * **Type:** `string` A same-origin URL on your server that accepts a `POST` with `{ paymentMethod, amount, currency, expiresAt }` and returns `{ spt: string }`. This is the same endpoint used by the [client-side `createToken` callback](#client). ### html.publishableKey * **Type:** `string` Your Stripe publishable key (`pk_live_...` or `pk_test_...`), embedded in the payment page for Stripe.js initialization. Programmatic clients with `Authorization` headers are unaffected. See the [payment links guide](/guides/payment-links) for a full walkthrough and live demo. ### Server parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `client` | `StripeClient` | One of `client` or `secretKey` | Pre-configured Stripe SDK instance (`new Stripe(...)`) | | `secretKey` | `string` | One of `client` or `secretKey` | Stripe secret API key (mppx makes raw API calls) | | `metadata` | `Record` | Optional | Key-value pairs forwarded to Stripe | | `networkId` | `string` | Required | Stripe [Business Network](https://docs.stripe.com/get-started/account/profile) profile ID | | `paymentMethodTypes` | `string[]` | Required | Allowed Stripe payment method types | ## Client ::::tip\[For agents] The [Link CLI](/tools/wallet#link-cli) handles `stripe.charge` end-to-end—`link-cli mpp pay` parses the 402 Challenge, creates an SPT from the user's Link wallet, and retries the request with the Credential. No code required. :::: Use `stripe` with `Mppx.create` to automatically handle `402` responses. The client parses the Challenge, creates an SPT through the `createToken` callback, and retries with the Credential. SPT creation requires a Stripe secret key, so the client accepts a `createToken` callback that proxies through a server endpoint. You can optionally pass a `client` (a Stripe.js instance from `@stripe/stripe-js`) which is forwarded to the `createToken` callback for use with Elements. ### Simple (known payment method) If you already have a payment method ID (for example a test card or a stored method), pass it as `paymentMethod` and mppx handles the full 402 → SPT → retry flow automatically. ```ts twoslash import { loadStripe } from '@stripe/stripe-js' import { Mppx, stripe } from 'mppx/client' const stripeJs = (await loadStripe('pk_test_...'))! Mppx.create({ methods: [ stripe({ client: stripeJs, createToken: async (params) => { const res = await fetch('/api/create-spt', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(params), }) if (!res.ok) throw new Error('Failed to create SPT') return (await res.json()).spt }, paymentMethod: 'pm_card_visa', // [!code hl] }), ], }) // fetch() now handles 402 → credential → retry automatically const response = await fetch('https://api.example.com/resource') // @log: Response { status: 200, ... } ``` ### With Stripe Elements For interactive payment collection, use `onChallenge` to render Stripe Elements when a 402 is received. The user enters card details, you create a payment method, then pass it to `createCredential`. ```ts twoslash import { loadStripe } from '@stripe/stripe-js' import { Receipt } from 'mppx' import { Mppx, stripe } from 'mppx/client' const stripeJs = (await loadStripe('pk_test_...'))! const mppx = Mppx.create({ methods: [ stripe.charge({ client: stripeJs, createToken: async ({ amount, currency, expiresAt, metadata, networkId, paymentMethod }) => { const response = await fetch('/api/create-spt', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ paymentMethod, amount, currency, networkId, expiresAt, metadata }), }) if (!response.ok) throw new Error('Failed to create SPT') return (await response.json()).spt }, }), ], onChallenge: async (challenge, { createCredential }) => { // Extract payment method types from the challenge const methodDetails = challenge.request.methodDetails as | { paymentMethodTypes?: string[] } | undefined const paymentMethodTypes = methodDetails?.paymentMethodTypes ?? ['card'] // Create Stripe Elements for payment collection const elements = stripeJs.elements({ mode: 'payment', amount: Number(challenge.request.amount), currency: challenge.request.currency as string, paymentMethodTypes, paymentMethodCreation: 'manual', }) // Mount the payment element (you'd mount this to a DOM container) const paymentElement = elements.create('payment') paymentElement.mount('#payment-element') // After user submits the form: await elements.submit() const { paymentMethod } = await stripeJs.createPaymentMethod({ elements }) // Create credential with the collected payment method return createCredential({ paymentMethod: paymentMethod!.id }) }, polyfill: false, }) const response = await mppx.fetch('/api/resource') const receipt = Receipt.fromResponse(response) ``` ## SPT creation proxy endpoint The `createToken` callback proxies through your own server because SPT creation requires a Stripe secret key. :::warning\[Security: server-side authorization] 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. Send only: * An authenticated session (cookie or bearer token) * A server-known resource identifier (for example, `orderId`, `quoteId`, `toolCallId`) The server then looks up the approved amount, currency, recipient, expiry, and rate/spend limits from its own records. ::: ```ts // Example: server derives all SPT parameters from a known order export async function POST(request: Request) { // 1. Authenticate the caller (session cookie, bearer token, etc.) const session = await getSession(request) if (!session) return Response.json({ error: 'Unauthorized' }, { status: 401 }) // 2. Accept only a server-known resource identifier from the client const { orderId, paymentMethod } = await request.json() // 3. Look up the authorized payment parameters server-side const order = await db.orders.get(orderId) if (!order) return Response.json({ error: 'Order not found' }, { status: 404 }) if (order.userId !== session.userId) return Response.json({ error: 'Forbidden' }, { status: 403 }) // 4. Server derives SPT parameters — the client never specifies amount/currency/expiry const body = new URLSearchParams({ payment_method: paymentMethod, 'usage_limits[currency]': order.currency, 'usage_limits[max_amount]': order.amount.toString(), 'usage_limits[expires_at]': Math.floor( (Date.now() + 5 * 60 * 1000) / 1000, ).toString(), }) const response = await fetch( 'https://api.stripe.com/v1/test_helpers/shared_payment/granted_tokens', { method: 'POST', headers: { Authorization: `Basic ${btoa(`${process.env.STRIPE_SECRET_KEY}:`)}`, 'Content-Type': 'application/x-www-form-urlencoded', }, body, }, ) if (!response.ok) { const error = await response.json() return Response.json({ error: error.error.message }, { status: 400 }) } const { id: spt } = await response.json() return Response.json({ spt }) } ``` :::info The `test_helpers/shared_payment/granted_tokens` endpoint is for testing. In production, SPTs are created through the agent-side `issued_tokens` API. ::: ### Client parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `client` | `StripeJs` | Optional | Stripe.js instance from `@stripe/stripe-js` — forwarded to `createToken` for use with Elements | | `createToken` | `(params) => Promise` | Required | Callback to create an SPT (proxied through a server endpoint) | | `externalId` | `string` | Optional | Client reference ID included in the Credential payload | | `paymentMethod` | `string` | Optional | Default Stripe payment method ID (overridden by `context.paymentMethod`) | ### `createToken` callback parameters The `createToken` callback receives a single object with the following fields: | Field | Type | Description | | --- | --- | --- | | `amount` | `string` | Payment amount in smallest currency unit | | `challenge` | `Challenge` | The parsed Challenge from the server | | `client` | `StripeJs \| undefined` | Stripe.js instance, if provided to `stripe.charge()` | | `currency` | `string` | Three-letter ISO currency code | | `expiresAt` | `number` | SPT expiration as a Unix timestamp (seconds) | | `metadata` | `Record` | Optional metadata from the Challenge | | `networkId` | `string \| undefined` | Stripe Business Network profile ID | | `paymentMethod` | `string \| undefined` | Stripe payment method ID | ## Request fields The Challenge request includes the base charge fields plus Stripe method details. | Field | Type | Required | Description | | --- | --- | --- | --- | | `amount` | `string` | Required | Amount in the smallest currency unit | | `currency` | `string` | Required | ISO currency code | | `decimals` | `number` | Required | Number of decimal places in the amount (for example, `2` for cents) | | `description` | `string` | Optional | Human-readable payment description | | `expires` | `string` | Optional | ISO 8601 expiration timestamp (defaults to 5 minutes) | | `externalId` | `string` | Optional | Merchant reference ID | | `methodDetails.metadata` | `Record` | Optional | Metadata forwarded to Stripe | | `methodDetails.networkId` | `string` | Required | Stripe Business Network profile ID | | `methodDetails.paymentMethodTypes` | `string[]` | Required | Allowed Stripe payment method types | ## Credential payload The Credential payload contains the SPT and an optional client reference ID. | Field | Type | Required | Description | | --- | --- | --- | --- | | `externalId` | `string` | Optional | Client reference ID | | `spt` | `string` | Required | Shared Payment Token ID (starts with `spt_`) | ## Specification # Card \[Card payments via encrypted network tokens] The Card method enables payments using encrypted, single use network payment tokens and dynamic data provided by a card network for machine-initiated transactions. Payment tokens, such as those provided by [Visa Intelligent Commerce](https://developer.visa.com/capabilities/visa-intelligent-commerce), settle through existing card infrastructure, and the client and server can each use independent payment providers rather than sharing a single platform. The [`mpp-card`](https://www.npmjs.com/package/mpp-card) SDK implements the `card` method with the `charge` intent. The protocol is defined in the [Card Network Charge Intent](https://paymentauth.org/draft-card-charge-00) specification. ## Installation :::code-group ```bash [npm] $ npm install mpp-card ``` ```bash [pnpm] $ pnpm add mpp-card ``` ```bash [bun] $ bun add mpp-card ``` ::: ## How it works >Server: (1) GET /resource Server-->>Client: (2) 402 + Challenge (amount, networks, encryption key) Client->>CE: (3) cardId + challenge context CE-->>Client: (4) Encrypted network token (JWE) Client->>Server: (5) GET /resource + Credential (encrypted token) Server->>SE: (6) Decrypt token + charge card SE-->>Server: (7) Authorization reference Server-->>Client: (8) 200 OK + Receipt + resource `} /> 1. **Client** requests a resource from the server. 2. **Server** responds with `402` and a Challenge containing the amount, currency, accepted card networks, and an RSA public key (`encryptionJwk`). 3. **Client** sends the card identifier and challenge context to a credential issuer. 4. **Credential Issuer** provisions a network token, generates a cryptogram, and encrypts both as a JWE using the server's public key. The encrypted token is returned to the client. 5. **Client** retries the original request with an `Authorization: Payment` header containing the encrypted credential. 6. **Server** decrypts the token using its private key and forwards it to the payment gateway for authorization through the card network. 7. **Server** returns the resource with a `Payment-Receipt` header confirming the charge. ## Intents # Card charge \[One-time payments using encrypted network tokens] The card implementation of the [charge](/intents/charge) intent. The client obtains an encrypted network token from a credential issuer and sends it as a Credential. The server decrypts the token and charges the card through existing card network rails. This method is best for single API calls, content access, or one-off purchases. ## Server Use `MppCard.create` and `mpp.charge` to gate any endpoint behind a one-time card payment. The method handles Challenge generation, Credential decryption, gateway authorization, and Receipt generation. ```ts import { MppCard } from 'mpp-card/server' const mpp = MppCard.create({ acceptedNetworks: ['visa'], merchantName: 'Demo', privateKey: process.env.PRIVATE_KEY, secretKey: process.env.MPP_SECRET_KEY, gateway: { async charge({ token, amount, currency, idempotencyKey }) { // Call your payment processor here return { reference: 'txn_123', status: 'success' } }, }, }) const charge = mpp.charge({ amount: '500', currency: 'usd' }) export async function handler(request: Request) { const result = await charge(request) if (result.status === 402) return result.challenge return result.withReceipt(Response.json({ data: '...' })) } ``` ### Server parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `acceptedNetworks` | `string[]` | Required | Accepted card networks | | `merchantName` | `string` | Required | Display name shown to cardholder | | `secretKey` | `string` | Required | HMAC signing key for challenge integrity | | `gateway` | `ServerEnabler` | Required | Payment gateway for charging decrypted tokens | | `privateKey` | `string` | Required | RSA-2048 PEM for token decryption | | `billingRequired` | `boolean` | Optional | Request billing address from client | ## Client Use `MppCard.create` to automatically handle `402` responses. The client parses the Challenge, requests an encrypted network token from the credential issuer, and retries with the Credential. For production payments, enroll a card through a tokenization provider (a secure card collection form or vault API) to obtain a `cardId`. ```ts import { MppCard } from 'mpp-card/client' MppCard.create({ cardId: 'card_abc123', enabler: { async getPaymentData({ cardId, challenge }) { // Call your credential issuer here return { encryptedPayload: '...', network: 'visa' } }, }, }) // Global fetch now handles 402 automatically const res = await fetch('https://api.merchant.com/data') ``` ### Dev mode Omit `enabler` to use the SDK's built-in dev mode. The client generates test network tokens encrypted with the server's published public key—no card enrollment or credential issuer required. ### Without polyfill If you don't want to patch `globalThis.fetch`, use `mppCard.fetch` directly: ```ts import { MppCard } from 'mpp-card/client' const mppCard = MppCard.create({ cardId: 'card_abc123', polyfill: false, }) const res = await mppCard.fetch('https://api.example.com/resource') ``` ### Client parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `cardId` | `string` | Required | Card identifier from your tokenization provider | | `enabler` | `ClientEnabler` | Optional | Credential issuer for token provisioning. Omit for dev mode. | ## Request fields The Challenge request includes the base charge fields plus card method details. | Field | Type | Required | Description | | --- | --- | --- | --- | | `amount` | `string` | Required | Amount in the smallest currency unit | | `currency` | `string` | Required | ISO currency code | | `description` | `string` | Optional | Human-readable payment description | | `recipient` | `string` | Optional | Merchant identifier | | `externalId` | `string` | Optional | Merchant reference ID | | `methodDetails.acceptedNetworks` | `string[]` | Required | Accepted card networks | | `methodDetails.merchantName` | `string` | Required | Display name shown to cardholder | | `methodDetails.encryptionJwk` | `JWK` | Conditional | RSA-OAEP-256 public key for token encryption | | `methodDetails.jwksUri` | `string` | Conditional | HTTPS URI to JWK Set | | `methodDetails.kid` | `string` | Conditional | Key ID when `jwksUri` is used | | `methodDetails.billingRequired` | `boolean` | Optional | Request billing address from client | ## Credential payload The Credential payload contains the encrypted network token and card metadata. | Field | Type | Required | Description | | --- | --- | --- | --- | | `encryptedPayload` | `string` | Required | JWE-encrypted network token (RSA-OAEP-256 + AES-256-GCM) | | `network` | `string` | Required | Card network identifier | | `panLastFour` | `string` | Required | Last four digits of card number | | `panExpirationMonth` | `string` | Required | Card expiration month | | `panExpirationYear` | `string` | Required | Card expiration year | | `billingAddress` | `object` | Conditional | Billing address (present when `billingRequired` is set) | | `cardholderFullName` | `string` | Optional | Cardholder name | | `paymentAccountReference` | `string` | Conditional | Payment Account Reference from the token service provider | ## Specification # Lightning \[Bitcoin payments over the Lightning Network] The Lightning payment method enables payments using Bitcoin over the [Lightning Network](https://lightning.network) within the MPP framework. Lightning supports two intents—**charge** for one-time payments and **session** for prepaid metered access—covering everything from single API calls to high-frequency streaming billing. The implementation is provided by [`@buildonspark/lightning-mpp-sdk`](https://github.com/buildonspark/lightning-mpp-sdk), which extends the [`mppx`](https://github.com/tempoxyz/mpp) SDK with Lightning Network support alongside built-in methods like [Stripe](/payment-methods/stripe) and [Tempo](/payment-methods/tempo). The reference implementation uses [Spark](https://spark.money) for wallet and node operations, but the protocol works with any Lightning node or wallet that can create BOLT11 invoices and verify preimages. ## Installation :::code-group ```bash [npm] $ npm install @buildonspark/lightning-mpp-sdk ``` ```bash [pnpm] $ pnpm add @buildonspark/lightning-mpp-sdk ``` ```bash [bun] $ bun add @buildonspark/lightning-mpp-sdk ``` ::: ## Payments on Lightning Lightning brings a distinct set of properties to MPP: * **Cryptographic verification**—The server checks `sha256(preimage) == paymentHash` with a single hash operation. Verification is entirely local and self-contained. * **Synchronous settlement**—Lightning HTLC settlement reveals the preimage atomically. The preimage *is* the proof of payment, available the instant the payment settles. * **Global and permissionless**—Bitcoin works identically in every jurisdiction. Anyone can participate without accounts, approvals, or special routing. * **Self-custodial**—Both client and server hold their own keys via Spark wallets. Funds stay under each party's control throughout the entire flow. ## Choosing an intent | | **Charge** | **Session** | |---|---|---| | **Pattern** | One-time payment per request | Prepaid deposit, per-request billing | | **Latency overhead** | One Lightning payment per request | Near-zero (bearer token after deposit) | | **Throughput** | One invoice + payment per request | Hundreds of requests per session | | **Best for** | Single API calls, content access, one-off purchases | LLM APIs, metered services, streaming | | **Settlement** | Immediate per-request via HTLC | Deposit upfront, per-request deduction, refund on close | ## Intents # Lightning charge \[One-time payments using BOLT11 invoices] The Lightning implementation of the [charge](/intents/charge) intent. The server generates a fresh [BOLT11](https://github.com/lightning/bolts/blob/master/11-payment-encoding.md) invoice for each request. The client pays it over the [Lightning Network](https://lightning.network) and presents the payment preimage as a credential. The server verifies `sha256(preimage) == paymentHash` locally and returns the resource with a receipt. This method is best for single API calls, content access, or one-off purchases. ## Server The `spark` namespace is the reference implementation using [Spark](https://spark.money) wallets. The protocol works with any Lightning node—you can build your own method handler using [LND](https://github.com/lightningnetwork/lnd), [LDK](https://lightningdevkit.org), or any stack that can create BOLT11 invoices and verify preimages. Use `spark.charge` to gate any endpoint behind a one-time Lightning payment. The method handles invoice generation, Challenge creation, preimage verification, and Receipt generation. ```ts import { Mppx, spark } from '@buildonspark/lightning-mpp-sdk/server' const mppx = Mppx.create({ methods: [spark.charge({ mnemonic: process.env.MNEMONIC! })], secretKey: process.env.MPP_SECRET_KEY!, }) export async function handler(request: Request) { const result = await mppx.charge({ amount: '100', currency: 'BTC', description: 'Premium API access', })(request) if (result.status === 402) return result.challenge return result.withReceipt(Response.json({ data: '...' })) } ``` ### With expiry ```ts import { Mppx, Expires, spark } from '@buildonspark/lightning-mpp-sdk/server' const mppx = Mppx.create({ methods: [spark.charge({ mnemonic: process.env.MNEMONIC! })], secretKey: process.env.MPP_SECRET_KEY!, }) export async function handler(request: Request) { const result = await mppx.charge({ amount: '100', currency: 'BTC', expires: Expires.minutes(10), // [!code hl] })(request) if (result.status === 402) return result.challenge return result.withReceipt(Response.json({ data: '...' })) } ``` ### With regtest For local development and testing, set `network` to `'regtest'` and use the [Spark faucet](https://docs.spark.money/tools/faucet) to fund wallets. ```ts import { Mppx, spark } from '@buildonspark/lightning-mpp-sdk/server' const mppx = Mppx.create({ methods: [spark.charge({ mnemonic: process.env.MNEMONIC!, network: 'regtest', // [!code hl] })], secretKey: process.env.MPP_SECRET_KEY!, }) ``` ### Server parameters | Parameter | Type | Required | Default | | --- | --- | --- | --- | | `mnemonic` | `string` | Required | | | `network` | `'mainnet'` | `'regtest'` | `'signet'` | Optional | `'mainnet'` | ## Client Use `spark.charge` with `Mppx.create` to automatically handle `402` responses. The client parses the Challenge, pays the BOLT11 invoice, and retries with the preimage as a Credential. ```ts import { Mppx, spark } from '@buildonspark/lightning-mpp-sdk/client' Mppx.create({ methods: [spark.charge({ mnemonic: process.env.MNEMONIC! })], }) const response = await fetch('https://api.example.com/resource') ``` ### Without polyfill If you don't want to patch `globalThis.fetch`, use `mppx.fetch` directly: ```ts import { Mppx, spark } from '@buildonspark/lightning-mpp-sdk/client' const method = spark.charge({ mnemonic: process.env.MNEMONIC! }) const mppx = Mppx.create({ methods: [method], polyfill: false, }) try { const response = await mppx.fetch('https://api.example.com/resource') console.log(await response.json()) } finally { await method.cleanup() } ``` :::info The Spark SDK maintains WebSocket connections for Lightning payments. Call `method.cleanup()` when done to close connections and allow the process to exit. ::: ### Client parameters | Parameter | Type | Required | Default | | --- | --- | --- | --- | | `mnemonic` | `string` | Required | | | `network` | `'mainnet'` | `'regtest'` | `'signet'` | Optional | `'mainnet'` | | `maxFeeSats` | `number` | Optional | `100` | ## Request fields The Challenge request includes the base charge fields plus Lightning method details. | Field | Type | Required | Description | | --- | --- | --- | --- | | `amount` | `string` | Required | Invoice amount in satoshis | | `currency` | `string` | Optional | Must be `'BTC'` if present. Defaults to `'BTC'` | | `description` | `string` | Optional | Human-readable memo. Maps to the BOLT11 description field | | `methodDetails.invoice` | `string` | Required | Full BOLT11-encoded payment request (`lnbc...`). Authoritative source for all payment parameters | | `methodDetails.paymentHash` | `string` | Optional | SHA-256 hash of the preimage, lowercase hex. Convenience field—must match the hash decoded from `invoice` | | `methodDetails.network` | `string` | Optional | `'mainnet'`, `'regtest'`, or `'signet'`. Convenience field—must match `invoice`'s human-readable prefix. Defaults to `'mainnet'` | ## Credential payload The Credential payload contains the payment preimage revealed by Lightning HTLC settlement. | Field | Type | Required | Description | | --- | --- | --- | --- | | `preimage` | `string` | Required | 32-byte payment preimage, lowercase hex (64 characters) | ## Verification The server verifies payment with a single hash operation: 1. Decode the Credential and extract `preimage`. 2. Compute `sha256(hex_to_bytes(preimage))`. 3. Compare against the `paymentHash` from the original Challenge. 4. If equal, payment is verified. Return the resource with a Receipt. The entire verification path is local and self-contained. ## Specification # Lightning session \[Pay-as-you-go payments over Lightning] The `session` intent enables high-frequency, pay-as-you-go payments over the Lightning Network. Clients pay a deposit invoice upfront, then authenticate subsequent requests by presenting the payment preimage as a bearer token. The server tracks a running balance and deducts the configured cost per unit of service. When the session closes, the server refunds any unspent balance via the client's return invoice. Payment sessions reduce payment verification to a single SHA-256 check, making it possible to meter and bill at the granularity of individual LLM tokens, API calls, or bytes transferred. ## Why sessions A charge intent requires a full Lightning round-trip per request—invoice generation, HTLC routing, preimage reveal. That's fine for a single API call, but an LLM inference can generate hundreds of tokens over several seconds. Paying per token over Lightning would add seconds of latency per chunk. Sessions fix this: one deposit, then the preimage becomes a bearer token. Every subsequent request is verified with a single `sha256` call, keeping the entire flow local and inline. The server deducts from the balance as it streams. When the client is done, the server refunds the unspent sats via the return invoice. ## How it works ### Overview >Server: (1) GET /generate Server->>LN: Create deposit invoice LN-->>Server: invoice + paymentHash Server-->>Client: 402 + deposit invoice Client->>LN: (2) Pay deposit invoice LN-->>Client: preimage Client->>Server: (3) GET /generate + open credential Note over Server: Verify preimage, store session Server-->>Client: 200 OK + SSE stream Client->>Server: (4) GET /generate + bearer credential Note over Server: Verify preimage, deduct per chunk Server-->>Client: 200 OK + SSE stream Client->>Server: (5) GET /generate + close credential Note over Server: Refund unspent via return invoice Server-->>Client: 200 {"status":"closed"} `} /> A Lightning session has four phases: ::::steps ### Open The client pays a deposit invoice over the Lightning Network. HTLC settlement reveals the payment preimage—a 32-byte random secret that becomes the bearer token for the session. The client submits the preimage along with a return invoice (a zero-amount BOLT11 invoice for refunds) to open the session. ### Session (bearer) The client authenticates subsequent requests by presenting the preimage and session ID. The server verifies `sha256(preimage) == paymentHash` with a single hash operation, entirely locally. The streaming layer deducts the per-unit cost from the session balance for each chunk delivered. ### Top up If the balance runs out mid-stream, the server emits a `payment-need-topup` SSE event and holds the connection open. The client pays a fresh deposit invoice and submits a `topUp` credential. The server credits the balance and resumes the stream on the original connection. The client doesn't need to replay the request. ### Close The client submits a `close` credential. The server computes `refundSats = depositSats - spent` and pays the return invoice with the unspent balance. The session is marked closed and no further actions are accepted. :::: ## Streaming LLM billing A typical flow for a streaming LLM API priced at 2 sats per token: 1. **Client:** sends an unauthenticated request to the API 2. **Server:** returns `402` with a deposit invoice for 300 sats (~150 tokens) 3. **Client:** pays the invoice, opens a session with the preimage + return invoice 4. **Server:** begins streaming tokens, deducting 2 sats per chunk from the session balance 5. **Server:** balance exhausted mid-stream—emits `payment-need-topup`, holds connection open 6. **Client:** pays a new deposit invoice, submits a `topUp` credential—stream resumes 7. **Client:** closes the session—server refunds unspent sats to the return invoice Everything happens locally during streaming. Verification is a single SHA-256 hash per request, and billing is an integer decrement per chunk. :::info\[Why Lightning] Lightning has properties that make it a natural fit for session-based billing: * **Open network**—Bitcoin is permissionless. A payment layer for the open internet is just as open as the network it runs on. * **Private by default**—Lightning payments are onion-routed. Only the payer and the payee know about a payment. * **Micropayment-friendly**—Lightning can route sub-cent payments economically, making per-token and per-request billing practical at any price point. * **Self-custodial**—Both client and server hold their own keys. Funds stay under each party's control throughout the entire flow. ::: ## Integration
Use `spark.session` to accept prepaid Lightning sessions. The method handles deposit invoice generation, preimage verification, balance tracking, and refund on close. :::info Session support in `@buildonspark/lightning-mpp-sdk` is coming soon. The API below shows the anticipated interface based on the [specification](https://paymentauth.org/draft-lightning-session-00). ::: ```ts import { Mppx, spark } from '@buildonspark/lightning-mpp-sdk/server' const mppx = Mppx.create({ methods: [spark.session({ mnemonic: process.env.MNEMONIC! })], secretKey: process.env.MPP_SECRET_KEY!, }) export async function handler(request: Request) { const result = await mppx.session({ amount: '2', currency: 'BTC', unitType: 'token', })(request) if (result.status === 402) return result.challenge return result.withReceipt(Response.json({ data: '...' })) } ```
Use `spark.session` with `Mppx.create` to automatically handle deposits, bearer authentication, top-ups, and session close. ```ts import { Mppx, spark } from '@buildonspark/lightning-mpp-sdk/client' const method = spark.session({ mnemonic: process.env.MNEMONIC! }) Mppx.create({ methods: [method], }) const response = await fetch('https://api.example.com/v1/chat/completions') // Automatically pays deposit, authenticates per request ``` ### Without polyfill If you don't want to patch `globalThis.fetch`, use `mppx.fetch` directly: ```ts import { Mppx, spark } from '@buildonspark/lightning-mpp-sdk/client' const method = spark.session({ mnemonic: process.env.MNEMONIC! }) const mppx = Mppx.create({ methods: [method], polyfill: false, }) try { const response = await mppx.fetch('https://api.example.com/v1/chat/completions') console.log(await response.json()) } finally { await method.cleanup() } ``` ### With multiple methods Register both charge and session so the client can handle either intent: ```ts import { Mppx, spark } from '@buildonspark/lightning-mpp-sdk/client' const charge = spark.charge({ mnemonic: process.env.MNEMONIC! }) const session = spark.session({ mnemonic: process.env.MNEMONIC! }) Mppx.create({ methods: [charge, session], }) ``` :::info The Spark SDK maintains WebSocket connections for Lightning payments. Call `method.cleanup()` when done to close connections and allow the process to exit. :::
## Specification # Solana \[Native SOL and SPL token payments] The Solana payment method enables MPP payments on Solana using native SOL, SPL tokens, and Token-2022 assets. Solana supports two intents: **charge** for one-time payments and **session** for escrowed, pay-as-you-go billing. The reference implementation is provided by [`@solana/mpp`](https://github.com/solana-foundation/mpp-sdk), which extends [`mppx`](https://github.com/tempoxyz/mpp) with Solana-native client and server handlers. ## Installation :::code-group ```bash [npm] $ npm install @solana/mpp mppx @solana/kit ``` ```bash [pnpm] $ pnpm add @solana/mpp mppx @solana/kit ``` ```bash [bun] $ bun add @solana/mpp mppx @solana/kit ``` ::: ## Why Solana Solana enables several useful capabilities for MPP: * **Split payouts and richer settlement flows** through multiple instructions per transaction * **Fast finality** for low-latency charge flows * **Cheap transactions** for micropayments and fee-sponsored UX * **Native fee payer support** so servers can sponsor network fees * **Token flexibility** across SOL, SPL, and Token-2022 assets * **Delegated signer options** including Ed25519 and passkey-friendly secp256r1 flows ## Choosing an intent | | **Charge** | **Session** | |---|---|---| | **Pattern** | One-time payment per request | Escrow once, pay incrementally with vouchers | | **Latency overhead** | One transaction or confirmed signature per request | Low after open; vouchers are off-chain | | **Throughput** | Best for discrete purchases | Best for high-frequency metered usage | | **Best for** | Paid API calls, downloads, fixed-price purchases | LLM APIs, streaming, repeated calls | | **Settlement** | Immediate on-chain transfer | Escrow on-chain, settle accepted usage later | ## Intents # Solana charge \[One-time payments on Solana] The Solana implementation of the [charge](/intents/charge) intent. The server issues a charge challenge describing the expected amount, currency, recipient, and Solana-specific `methodDetails`. The client either presents a signed transaction for server broadcast or presents a confirmed transaction signature. The server verifies the transfer on-chain and returns the resource with a receipt. This method is best for fixed-price API calls, digital goods, and payments that settle directly on Solana. ## Server Use `solana.charge` to gate endpoints behind native SOL or SPL token payments. ```ts import { Mppx } from 'mppx/server' import { solana } from '@solana/mpp/server' const mppx = Mppx.create({ methods: [solana.charge({ recipient: '9xAXssX9j7vuK99c7cFwqbixzL3bFrzPy9PUhCtDPAYJ', network: 'localnet', })], secretKey: process.env.MPP_SECRET_KEY!, }) ``` ## Client Use `solana.charge` with `Mppx.create` to automatically handle Solana charge challenges. ```ts import { Mppx } from 'mppx/client' import { solana } from '@solana/mpp/client' const mppx = Mppx.create({ methods: [solana.charge()], }) ``` ## Payment links Enable `html: true` on `solana.charge()` to turn any endpoint into a shareable payment link. Browsers see a payment page with a "Continue with Solana" button; programmatic clients get the standard `402` flow. ```ts import { Mppx } from 'mppx/server' import { solana } from '@solana/mpp/server' const mppx = Mppx.create({ methods: [solana.charge({ recipient: '9xAXssX9j7vuK99c7cFwqbixzL3bFrzPy9PUhCtDPAYJ', currency: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC decimals: 6, network: 'localnet', html: true, // [!code hl] })], secretKey: process.env.MPP_SECRET_KEY!, }) ``` The `html` option accepts: | Type | Behavior | | --- | --- | | `true` | Default Solana-branded payment page with "Continue with Solana" button | | `false` / omitted | No payment page — standard JSON `402` only | On devnet and localnet, the payment page shows a network badge on the button and uses [Surfpool](https://surfpool.run) cheatcodes to fund test accounts automatically. See the [Payment links guide](/guides/payment-links) for framework-specific setup. ## Solana-specific request fields The Solana charge request extends the base charge schema with `methodDetails` fields such as: * `network` * `decimals` * `tokenProgram` * `feePayer` * `feePayerKey` * `splits` These fields let the server describe whether payment is in SOL or an SPL asset, whether fee sponsorship is available, and whether the payment is split across multiple recipients. ## Specification # Stellar \[SEP-41 token payments on the Stellar network] The [Stellar](https://stellar.org) payment method enables payments using SEP-41-compliant tokens on the Stellar network. Stellar supports two intents – **charge** for one-time on-chain transfers and **channel** for high-frequency off-chain payment channels – covering everything from single API calls to metered billing at token-level granularity. The reference implementation is provided by [`@stellar/mpp`](https://github.com/stellar/stellar-mpp-sdk), which extends [`mppx`](https://github.com/wevm/mppx) with Stellar-native client and server handlers. ## Installation :::code-group ```bash [npm] $ npm install @stellar/mpp mppx @stellar/stellar-sdk ``` ```bash [pnpm] $ pnpm add @stellar/mpp mppx @stellar/stellar-sdk ``` ```bash [bun] $ bun add @stellar/mpp mppx @stellar/stellar-sdk ``` ::: ## Why Stellar Stellar enables several useful capabilities for MPP: * **5-second finality**: Transactions settle with deterministic confirmation, no probabilistic waiting * **Sub-cent fees**: Transaction costs low enough for micropayments and per-request billing * **Fee sponsorship**: Servers can pay transaction fees on behalf of clients, removing wallet friction entirely * **Stellar smart contracts**: Payment channels run as auditable on-chain contracts for secure escrow and settlement * **Flexible token support**: Any SEP-41 compliant token is supported, which includes smart contract transfers and classic assets (via SAC). * **Payment channels**: Off-chain cumulative commitments enable high-frequency metered billing without per-request on-chain cost ## Choosing an intent | | **Charge** | **Channel** | |---|---|---| | **Pattern** | One-time payment per request | Deposit once, pay incrementally with commitments | | **Latency overhead** | ~5s (on-chain confirmation) | Near-zero after channel open | | **Throughput** | One transaction per request | Many commitments per second per channel | | **Best for** | Single API calls, content access, one-off purchases | LLM APIs, metered services, usage-based billing | | **On-chain cost** | Per request | Amortized across many requests | | **Settlement** | Immediate on-chain token transfer | Off-chain commitments, on-chain close | ## Intents ## Fee sponsorship Stellar supports server-paid transaction fees for charge intents. When enabled, the server rebuilds the transaction with its own source account and optionally wraps it in a fee bump. The client signs only the Stellar auth entries – no need to hold XLM for gas. Pass a `feePayer` configuration to `stellar.charge()` to enable this: ```ts import { Mppx } from 'mppx/server' import { stellar } from '@stellar/mpp/charge/server' import { USDC_SAC_TESTNET } from '@stellar/mpp' import { Keypair } from '@stellar/stellar-sdk' const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY, methods: [ stellar.charge({ recipient: process.env.STELLAR_RECIPIENT, currency: USDC_SAC_TESTNET, network: 'stellar:testnet', feePayer: { // [!code hl] envelopeSigner: Keypair.fromSecret('S...'), // [!code hl] }, // [!code hl] }), ], }) ``` ## Supported assets Any [SEP-41](https://stellar.org/protocol/sep-41) compliant token is supported. This includes classic Stellar assets wrapped via SAC, as well as custom smart contract tokens that implement the SEP-41 interface. A few common examples: * **USDC** – Circle's USD stablecoin on Stellar * **XLM** – Stellar's native asset, wrapped as a SEP-41 token via SAC * **Any custom SEP-41 token** – smart contracts implementing the SEP-41 token interface The `@stellar/mpp` package exports constants for commonly used token addresses (`USDC_SAC_MAINNET`, `USDC_SAC_TESTNET`, `XLM_SAC_MAINNET`, `XLM_SAC_TESTNET`). # Stellar charge \[One-time SEP-41 token transfers] The Stellar implementation of the [charge](/intents/charge) intent. The client signs a SEP-41 `transfer` invocation, and the server verifies and broadcasts the transaction on-chain. Settlement completes in ~5 seconds with deterministic finality. Stellar charge supports two credential modes: * **Pull mode** (default): The client signs Stellar auth entries and sends the transaction XDR. The server broadcasts it. Two variants: sponsored (server holds an envelope signer and optionally wraps with a fee bump) or unsponsored (server broadcasts the transaction as-is, without modification). * **Push mode**: The client builds, signs, and broadcasts the transaction itself, then sends the transaction hash. The server polls for confirmation. This method is best for single API calls, content access, or one-off purchases. ## How it works >Server: (1) GET /resource Server-->>Client: 402 + Challenge (amount, currency, recipient) Client->>Client: (2) Build SEP-41 transfer, sign auth entries Client->>Server: (3) Retry with Credential (signed XDR) Server->>Stellar: (4) Simulate + broadcast transaction Stellar-->>Server: Transaction confirmed Server-->>Client: 200 OK + Receipt `} /> ## Server Use `stellar.charge` to gate any endpoint behind a one-time SEP-41 token payment. The method handles Challenge generation, Credential verification, transaction broadcast, and Receipt creation. ```ts import express from 'express' import { Mppx } from 'mppx/server' import { stellar } from '@stellar/mpp/charge/server' import { USDC_SAC_TESTNET } from '@stellar/mpp' const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY, methods: [ stellar.charge({ recipient: process.env.STELLAR_RECIPIENT, currency: USDC_SAC_TESTNET, network: 'stellar:testnet', }), ], }) const app = express() app.get('/my-service', async (req, res) => { const webReq = new Request(`http://localhost:3000${req.url}`, { method: req.method, headers: new Headers(req.headers as Record), }) const result = await mppx.charge({ amount: '0.01', description: 'Premium API access', })(webReq) if (result.status === 402) { const challenge = result.challenge challenge.headers.forEach((value, key) => res.setHeader(key, value)) return res.status(402).send(await challenge.text()) } const response = result.withReceipt( Response.json({ message: 'Payment verified' }), ) response.headers.forEach((value, key) => res.setHeader(key, value)) return res.status(response.status).send(await response.text()) }) app.listen(3000) ``` ### With fee sponsorship When `feePayer` is configured, the server adds its own source account and optionally wraps the transaction in a fee bump before broadcasting. The client doesn't need XLM for gas – it signs only the Stellar auth entries. ```ts import { Mppx } from 'mppx/server' import { stellar } from '@stellar/mpp/charge/server' import { USDC_SAC_TESTNET } from '@stellar/mpp' import { Keypair } from '@stellar/stellar-sdk' const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY, methods: [ stellar.charge({ recipient: process.env.STELLAR_RECIPIENT, currency: USDC_SAC_TESTNET, network: 'stellar:testnet', feePayer: { // [!code hl] envelopeSigner: Keypair.fromSecret(process.env.FEE_PAYER_SECRET), // [!code hl] feeBumpSigner: Keypair.fromSecret(process.env.FEE_BUMP_SECRET), // optional // [!code hl] }, // [!code hl] }), ], }) ``` ### Server configuration | Parameter | Type | Default | Description | |---|---|---|---| | `currency` | `string` | – | SEP-41 token contract address | | `decimals` | `number` | `7` | Token decimal precision | | `feePayer` | `object` | – | Fee sponsorship configuration | | `maxFeeBumpStroops` | `number` | `10,000,000` | Maximum fee bump amount in stroops | | `network` | `string` | – | `'stellar:testnet'` or `'stellar:pubnet'` | | `pollDelayMs` | `number` | `1,000` | Delay between transaction poll attempts | | `pollMaxAttempts` | `number` | `30` | Maximum transaction poll attempts | | `pollTimeoutMs` | `number` | `30,000` | Total poll timeout | | `recipient` | `string` | – | Stellar public key (`G...`) or contract (`C...`) | | `rpcUrl` | `string` | – | Stellar RPC endpoint | | `simulationTimeoutMs` | `number` | `10,000` | Simulation timeout | | `store` | `Store` | – | State store for replay protection | ## Client Use `stellar.charge` with `Mppx.create` to automatically handle `402` responses. The client signs SEP-41 transfer auth entries and retries with the Credential. ```ts import { Keypair } from '@stellar/stellar-sdk' import { Mppx } from 'mppx/client' import { stellar } from '@stellar/mpp/charge/client' Mppx.create({ methods: [ stellar.charge({ keypair: Keypair.fromSecret('S...'), mode: 'pull', onProgress(event) { console.log(event.type) // challenge → signing → signed → paying → confirming → paid }, }), ], }) const response = await fetch('http://localhost:3000/my-service') const data = await response.json() ``` ### Push mode In push mode, the client broadcasts the transaction and sends the hash for server verification: ```ts import { Keypair } from '@stellar/stellar-sdk' import { Mppx } from 'mppx/client' import { stellar } from '@stellar/mpp/charge/client' Mppx.create({ methods: [ stellar.charge({ keypair: Keypair.fromSecret('S...'), mode: 'push', // [!code hl] }), ], }) ``` ### Without polyfill If you don't want to patch `globalThis.fetch`, use `mppx.fetch` directly: ```ts import { Keypair } from '@stellar/stellar-sdk' import { Mppx } from 'mppx/client' import { stellar } from '@stellar/mpp/charge/client' const mppx = Mppx.create({ methods: [ stellar.charge({ keypair: Keypair.fromSecret('S...'), }), ], polyfill: false, }) const response = await mppx.fetch('http://localhost:3000/my-service') ``` ### Client configuration | Parameter | Type | Default | Description | |---|---|---|---| | `decimals` | `number` | `7` | Token decimal precision | | `keypair` | `Keypair` | – | Stellar keypair for signing | | `mode` | `'pull' \| 'push'` | `'pull'` | Credential mode | | `onProgress` | `function` | – | Lifecycle event callback | | `pollDelayMs` | `number` | `1,000` | Delay between poll attempts (push mode) | | `pollMaxAttempts` | `number` | `30` | Maximum poll attempts (push mode) | | `pollTimeoutMs` | `number` | `30,000` | Total poll timeout (push mode) | | `rpcUrl` | `string` | – | Stellar RPC endpoint | | `secretKey` | `string` | – | Stellar secret key (alternative to `keypair`) | | `simulationTimeoutMs` | `number` | `10,000` | Simulation timeout | | `timeout` | `number` | `180` | Transaction timeout in seconds | ### Progress events The `onProgress` callback fires at each stage of the charge flow: | Event | Description | |---|---| | `challenge` | Challenge received from server | | `signing` | Building and signing SEP-41 transfer | | `signed` | Transaction signed | | `paying` | Sending Credential to server | | `confirming` | Waiting for on-chain confirmation (push mode) | | `paid` | Payment confirmed, Receipt received | # Channel \[High-frequency off-chain payments] :::info Stellar uses the term "channel" for its streaming payment intent. This corresponds to the MPP [session](/payment-methods/tempo/session) concept used by other payment methods. ::: :::warning\[Spec in progress] The formal specification for the Stellar channel intent is still being drafted. An initial implementation is available in [`@stellar/mpp`](https://github.com/stellar/stellar-mpp-sdk) and this documentation reflects that implementation. Details may change as the spec is finalized. ::: The `channel` intent enables high-frequency, pay-as-you-go payments over unidirectional payment channels on Stellar. Clients deposit tokens into an on-chain smart contract escrow and sign off-chain cumulative commitments as they consume resources. The server verifies commitments via contract simulation–no per-request on-chain transactions–and closes the channel to settle the final balance. Payment channels reduce payment verification to a single contract simulation per request, making it possible to meter and bill at the granularity of individual LLM tokens, API calls, or bytes transferred. ## How it works >Stellar: (1) Deploy channel + deposit tokens Stellar-->>Client: Channel contract created Client->>Server: (2) Open credential (channel address) Note over Server: Verify on-chain deposit Server-->>Client: 200 OK (session established) loop Per request Client->>Server: (3) Request + commitment signature Note over Server: Verify via prepare_commitment simulation Server-->>Client: 200 OK + Receipt end Server->>Stellar: (4) Close channel with highest commitment Stellar-->>Client: Refund remaining deposit `} /> A payment channel has four phases: ::::steps ### Open The client deploys a one-way-channel smart contract with a commitment key and an `asset` deposit. This creates a payment channel between the client (funder) and server (recipient). The channel contract holds the deposited tokens on-chain. ### Session The client signs ed25519 cumulative commitment amounts as service is consumed. Each commitment authorizes "I have now consumed up to X total." The server verifies the commitment signature by simulating `prepare_commitment` on the channel contract and checks that the cumulative amount is higher than the previous commitment. Commitment verification requires a single contract simulation per request. This is what enables per-token LLM billing without significant latency overhead. ### Top up If the channel runs low on funds, the client deposits additional tokens via the `top_up` function without closing the channel. The session continues uninterrupted. ### Close Either party can close the channel. The server calls `close()` on the channel contract with the highest commitment amount and signature, settling the final balance on-chain and refunding any unused deposit to the funder. :::: ## Integration
Use `stellar.channel` to accept payment channels. The server needs the channel contract address, commitment public key, and a storage backend for channel state. ```ts import { Mppx, Store } from 'mppx/server' import { stellar } from '@stellar/mpp/channel/server' const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY, methods: [ stellar.channel({ channel: process.env.CHANNEL_CONTRACT, // C... address commitmentKey: process.env.COMMITMENT_PUBLIC_KEY, network: 'stellar:testnet', store: Store.memory(), }), ], }) ``` During a session, the server verifies each commitment by simulating `prepare_commitment` on the channel contract. On-chain interaction only happens during open and close. Use `mppx.session` in your request handler to meter access: ```ts import { Mppx, Store } from 'mppx/server' import { stellar } from '@stellar/mpp/channel/server' const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY, methods: [ stellar.channel({ channel: process.env.CHANNEL_CONTRACT, commitmentKey: process.env.COMMITMENT_PUBLIC_KEY, network: 'stellar:testnet', store: Store.memory(), }), ], }) export async function handler(request: Request) { const result = await mppx.session({ amount: '0.01', description: 'API access', })(request) if (result.status === 402) return result.challenge return result.withReceipt(Response.json({ data: '...' })) } ``` ### Server configuration | Parameter | Type | Default | Description | |---|---|---|---| | `channel` | `string` | – | On-chain channel contract address (`C...`) | | `checkOnChainState` | `boolean` | `false` | Verify on-chain state during voucher verification | | `commitmentKey` | `string \| Keypair` | – | Commitment verification public key | | `decimals` | `number` | `7` | Token decimal precision | | `feePayer` | `object` | – | Fee sponsorship configuration for close transactions | | `feePayer.envelopeSigner` | `Keypair \| string` | – | Signer for close transaction envelopes | | `feePayer.feeBumpSigner` | `Keypair \| string` | – | Optional fee bump signer for close transactions | | `maxFeeBumpStroops` | `number` | `10,000,000` | Maximum fee bump amount | | `network` | `string` | – | `'stellar:testnet'` or `'stellar:pubnet'` | | `onDisputeDetected` | `function` | – | Callback when on-chain dispute is detected | | `pollDelayMs` | `number` | `1,000` | Delay between poll attempts | | `pollMaxAttempts` | `number` | `30` | Maximum poll attempts | | `pollTimeoutMs` | `number` | `30,000` | Total poll timeout | | `rpcUrl` | `string` | – | Stellar RPC endpoint | | `simulationTimeoutMs` | `number` | `10,000` | Simulation timeout | | `sourceAccount` | `string` | – | Source account for transactions | | `store` | `Store` | – | State store for channel data |
Use `stellar.channel` with `Mppx.create` to sign commitment amounts automatically when the server requests payment channels. ```ts import { Keypair } from '@stellar/stellar-sdk' import { Mppx } from 'mppx/client' import { stellar } from '@stellar/mpp/channel/client' Mppx.create({ methods: [ stellar.channel({ commitmentKey: Keypair.fromSecret('S...'), onProgress(event) { console.log(event.type) // challenge → signing → signed }, }), ], }) const response = await fetch('http://localhost:3000/my-service') // Automatically signs cumulative commitments per request ``` ### Without polyfill If you don't want to patch `globalThis.fetch`, use `mppx.fetch` directly: ```ts import { Keypair } from '@stellar/stellar-sdk' import { Mppx } from 'mppx/client' import { stellar } from '@stellar/mpp/channel/client' const mppx = Mppx.create({ methods: [ stellar.channel({ commitmentKey: Keypair.fromSecret('S...'), }), ], polyfill: false, }) const response = await mppx.fetch('http://localhost:3000/my-service') ``` ### With multiple methods Register both charge and channel methods so the client can handle servers that offer either: ```ts import { Keypair } from '@stellar/stellar-sdk' import { Mppx } from 'mppx/client' import { stellar } from '@stellar/mpp/charge/client' import { stellar as stellarChannel } from '@stellar/mpp/channel/client' Mppx.create({ methods: [ stellar.charge({ keypair: Keypair.fromSecret('S...') }), stellarChannel.channel({ commitmentKey: Keypair.fromSecret('S...') }), ], }) ``` ### Client configuration | Parameter | Type | Default | Description | |---|---|---|---| | `commitmentKey` | `Keypair` | – | Ed25519 keypair for signing commitments | | `commitmentSecret` | `string` | – | Secret key (alternative to `commitmentKey`) | | `onProgress` | `function` | – | Lifecycle event callback | | `rpcUrl` | `string` | – | Stellar RPC endpoint | | `simulationTimeoutMs` | `number` | `10,000` | Simulation timeout | | `sourceAccount` | `string` | – | Source account for transactions |
## Closing the channel The server closes the channel by submitting the highest cumulative commitment amount and signature on-chain. The `close` function is exported from the server package: ```ts import { close } from '@stellar/mpp/channel/server' import { Keypair } from '@stellar/stellar-sdk' const txHash = await close({ channel: 'CABC...', // channel contract address amount: 2000000n, // cumulative amount in base units (bigint) signature: lastCommitmentSignature, // Uint8Array feePayer: { envelopeSigner: Keypair.fromSecret('S...') }, network: 'stellar:testnet', }) ``` :::warning Channels do not close automatically. If you don't call `close()`, the deposit stays locked in the channel contract until the funder initiates a refund after the waiting period expires. ::: ## Monitoring channels Use `getChannelState` to query the on-chain state of a channel, and `watchChannel` to poll for contract events: ```ts import { getChannelState, watchChannel } from '@stellar/mpp/channel/server' // Query current state const state = await getChannelState({ channel: 'CABC...', rpcUrl: 'https://soroban-testnet.stellar.org', }) // Watch for events (close, refund, top_up) const stop = watchChannel({ channel: 'CABC...', rpcUrl: 'https://soroban-testnet.stellar.org', onEvent(event) { console.log(event.type, event.data) }, }) // Stop watching stop() ``` ## Channel contract Payment channels use the [one-way-channel](https://github.com/stellar-experimental/one-way-channel) smart contract for on-chain deposits, commitment verification, and settlement. The contract lifecycle: **Open** (deploy + deposit) -> **Off-chain payments** (signed commitments) -> **Settle** (partial withdrawal) -> **Close** (final settlement) or **Close Start** -> **Refund** (funder reclaims after waiting period). Commitment signatures use ed25519 over XDR-encoded `ScVal::Map` containing the amount, channel address, domain separator (`chancmmt`), and network ID–preventing replay across channels and networks. :::info The one-way-channel contract is experimental and has not been audited. See the [repository](https://github.com/stellar-experimental/one-way-channel) for the latest status. ::: # Monad \[ERC-20 token payments on Monad] The Monad payment method enables MPP payments on Monad using ERC-20 tokens. Monad supports the **charge** intent for one-time payments with two settlement modes: **push** where the client broadcasts the transfer, and **pull** where the client signs an ERC-3009 authorization for the server to broadcast. The reference implementation is provided by [`@monad-crypto/mpp`](https://github.com/monad-crypto/monad-ts), which extends [`mppx`](https://github.com/tempoxyz/mpp) with Monad-native client and server handlers. ## Installation :::code-group ```bash [npm] $ npm install @monad-crypto/mpp mppx viem ``` ```bash [pnpm] $ pnpm add @monad-crypto/mpp mppx viem ``` ```bash [bun] $ bun add @monad-crypto/mpp mppx viem ``` ::: ## Intents # Monad charge \[One-time payments on Monad] The Monad implementation of the [charge](/intents/charge) intent. The server issues a charge challenge describing the expected amount, currency, and recipient. The client either broadcasts an ERC-20 `transfer` and returns the transaction hash (**push** mode), or signs an ERC-3009 `transferWithAuthorization` for the server to broadcast (**pull** mode). The server verifies the transfer on-chain and returns the resource with a receipt. This method is best for fixed-price API calls, digital goods, and payments that settle directly on Monad. ## Server Use `monad.charge` to gate any endpoint behind a one-time ERC-20 payment. The method handles Challenge generation, Credential verification, on-chain settlement, and Receipt creation. ```ts import { Mppx } from "mppx/server"; import { monad } from "@monad-crypto/mpp/server"; const mppx = Mppx.create({ methods: [monad()], }); export async function handler(request: Request) { const result = await mppx.charge({ amount: "0.1", currency: "0x754704Bc059F8C67012fEd69BC8A327a5aafb603", // USDC recipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", })(request); if (result.status === 402) return result.challenge; return result.withReceipt(Response.json({ data: "..." })); } ``` ### With pull mode (ERC-3009) To accept pull mode credentials, provide an `account` so the server can broadcast `transferWithAuthorization` and pay gas: ```ts import { Mppx } from "mppx/server"; import { monad } from "@monad-crypto/mpp/server"; import { privateKeyToAccount } from "viem/accounts"; const account = privateKeyToAccount(process.env.SERVER_PRIVATE_KEY as `0x${string}`); const mppx = Mppx.create({ methods: [monad.charge({ account, // [!code hl] })], }); ``` ## Client Use `monad.charge` with `Mppx.create` to automatically handle `402` responses. The client parses the Challenge, creates a credential (either a signed transfer or an ERC-3009 authorization), and retries with the Credential. ```ts import { Mppx } from "mppx/client"; import { monad } from "@monad-crypto/mpp/client"; import { privateKeyToAccount } from "viem/accounts"; const account = privateKeyToAccount("0xabc…123"); const mppx = Mppx.create({ methods: [monad.charge({ account })], }); const response = await mppx.fetch("https://api.example.com/resource"); ``` ## Settlement modes Monad charge supports two settlement modes: * **Push** — the client broadcasts an ERC-20 `transfer(recipient, amount)` transaction on-chain and includes the transaction hash in the credential. The server verifies the transfer by reading `Transfer` event logs. * **Pull** — the client signs an ERC-3009 `transferWithAuthorization` off-chain. The server broadcasts the authorization on-chain via `transferWithAuthorization`, paying the gas. The server account does not need to match the recipient address. # RedotPay \[Balance and stablecoin rails] The RedotPay payment method enables MPP payments using either a RedotPay balance proof (`rdt`) or a stablecoin payment proof (transaction hash), carried inside the MPP Credential payload. This method is implemented as `method="redotpay"`, with a single intent: `intent="charge"`. ## How it works >Server: (1) GET /resource Server-->>Client: (2) 402 + Challenge (method=redotpay, intent=charge) Client->>RedotPay: (3) Obtain payment proof (rdt or transaction hash) RedotPay-->>Client: (4) proof Client->>Server: (5) GET /resource + Credential (Authorization: Payment ...) Server->>RedotPay: (6) Verify proof (merchant callbacks) RedotPay-->>Server: (7) verified Server-->>Client: (8) 200 OK + Receipt `} /> 1. **Server** responds with `402` and a Challenge containing amount/currency and RedotPay method details. 2. **Client** obtains a payment proof from RedotPay rails (balance `rdt` or stablecoin transaction hash). 3. **Client** retries the request with `Authorization: Payment ...` containing a Credential with the proof in `payload`. 4. **Server** verifies the proof, enforces replay protection, and returns the resource with a Receipt. ## SDK The first-party SDK re-exports `Mppx` so users only need one import: ```ts import { Mppx, charge } from '@redotpay/mpp/server' import { charge as clientCharge } from '@redotpay/mpp/client' ``` ## Intents # Charge \[One-time payments] The RedotPay `charge` intent is a one-time payment flow using MPP's `402 -> Credential -> 200` pattern. ## Method / intent * `method="redotpay"` * `intent="charge"` ## Install ```bash [terminal] $ npm i @redotpay/mpp mppx ``` ## Server integration Use a single import to get both `Mppx` and the RedotPay method factory: ```ts import * as mppx from 'mppx' import { Mppx, charge } from '@redotpay/mpp/server' const consumed = new Set() const payment = Mppx.create({ methods: [ charge({ consumeReference: async ({ reference }) => { if (consumed.has(reference)) return false consumed.add(reference) return true }, verifyBalance: async ({ rdt }) => Boolean(rdt), verifyCrypto: async ({ hash }) => Boolean(hash), }), ], realm: process.env.MPP_REALM, secretKey: process.env.MPP_SECRET_KEY!, }) export async function handler(req: Request) { const authorization = req.headers.get('Authorization') if (!authorization) { const challenge = await payment.challenge.redotpay.charge({ amount: '1.00', decimal: 0, currency: 'usd', methodDetails: { balance: {} }, } as any) return new Response(null, { status: 402, headers: { 'WWW-Authenticate': mppx.Challenge.serialize(challenge) }, }) } const receipt = await payment.verifyCredential(authorization) return new Response(JSON.stringify({ ok: true, receipt }), { status: 200, headers: { 'Content-Type': 'application/json' }, }) } ``` ## Client integration The client handles `402 -> Credential -> retry`: ```ts import * as mppx from 'mppx' import { charge } from '@redotpay/mpp/client' export async function fetchWithAutoPay(url: string) { const first = await fetch(url) if (first.status !== 402) return first const wwwAuthenticate = first.headers.get('www-authenticate') if (!wwwAuthenticate) throw new Error('missing WWW-Authenticate') const method = charge({ payload: { type: 'balance', rdt: 'rdt_xxx' } }) const challenge = mppx.Challenge.deserialize(wwwAuthenticate, { methods: [method] }) const authorization = await method.createCredential({ challenge }) return fetch(url, { headers: { Authorization: authorization } }) } ``` ## Request shape (decoded Challenge request) * `amount`: string * `decimal`: number (default 0) * `currency`: string (lowercased) * `methodDetails.balance` and/or `methodDetails.crypto[]` ## Credential payload (decoded Credential payload) * Balance rail: `{ type: "balance", rdt: string, externalId?: string }` * Stablecoin rail: `{ type: "crypto", chainId: number, currency: string, hash: string, externalId?: string }` # Custom \[Build your own payment method] The `mppx` SDK supports dynamic extensibility for new payment methods. You can implement custom payment methods to integrate any payment rail—other blockchains, card processors, or proprietary systems. | Approach | Description | Best for | |---|---|---| | **[Dynamic extension](#dynamic-extension)** | Define a method inline in your app | Integrating a new payment rail | | **[First-party SDK](#first-party-sdk)** | Package your method as a standalone npm module | Publishing a reusable method for the ecosystem | ## Dynamic extension A custom payment method requires three pieces: 1. **Method definition** — Define the method name, intent, and schemas for request parameters and Credential payloads 2. **Client logic** — Create Credentials when the client gets a `402` response 3. **Server logic** — Verify Credentials and return Receipts ### Define a method Start by defining your payment method with `Method.from`. The definition includes the method name, intent type, and schemas for request parameters and Credential payloads. ```ts twoslash [methods.ts] import { Method, z } from 'mppx' const lightning = Method.from({ intent: 'charge', name: 'lightning', schema: { credential: { payload: z.object({ preimage: z.string(), }), }, request: z.object({ amount: z.string(), currency: z.string(), invoice: z.string(), paymentHash: z.string(), recipient: z.string(), }), }, }) ``` ### Client implementation Extend the method with Credential creation logic using `Method.toClient`. The `createCredential` function runs when the client gets a `402` response: ```ts twoslash [methods.client.ts] import { Credential, Method, z } from 'mppx' const lightning = Method.from({ intent: 'charge', name: 'lightning', schema: { credential: { payload: z.object({ preimage: z.string(), }), }, request: z.object({ amount: z.string(), currency: z.string(), invoice: z.string(), paymentHash: z.string(), recipient: z.string(), }), }, }) declare function payInvoice(invoice: string): Promise<{ preimage: string }> // ---cut--- const clientMethod = Method.toClient(lightning, { async createCredential({ challenge }) { const result = await payInvoice(challenge.request.invoice) return Credential.serialize({ challenge, payload: { preimage: result.preimage, }, }) }, }) ``` ### Server implementation Extend the method with verification logic using `Method.toServer`. For Lightning Network, verify that the preimage hashes to the payment hash: ```ts twoslash [methods.server.ts] import { Method, Receipt, z } from 'mppx' const lightning = Method.from({ intent: 'charge', name: 'lightning', schema: { credential: { payload: z.object({ preimage: z.string(), }), }, request: z.object({ amount: z.string(), currency: z.string(), invoice: z.string(), paymentHash: z.string(), recipient: z.string(), }), }, }) declare function bytesToHex(bytes: Uint8Array): string declare function hexToBytes(hex: string): Uint8Array declare function sha256(data: Uint8Array): Uint8Array // ---cut--- const serverMethod = Method.toServer(lightning, { async verify({ credential }) { const preimage = credential.payload.preimage const expectedHash = credential.challenge.request.paymentHash const actualHash = bytesToHex(sha256(hexToBytes(preimage))) if (actualHash !== expectedHash) { throw new Error('Preimage does not match payment hash') } return Receipt.from({ method: 'lightning', reference: preimage, status: 'success', timestamp: new Date().toISOString(), }) }, }) ``` ### Use in your app ### Client Pass the client method to `Mppx.create`: ```ts twoslash [client.ts] import { Credential, Method, z } from 'mppx' import { Mppx } from 'mppx/client' const lightning = Method.from({ intent: 'charge', name: 'lightning', schema: { credential: { payload: z.object({ preimage: z.string(), }), }, request: z.object({ amount: z.string(), currency: z.string(), invoice: z.string(), paymentHash: z.string(), recipient: z.string(), }), }, }) declare function payInvoice(invoice: string): Promise<{ preimage: string }> const clientMethod = Method.toClient(lightning, { async createCredential({ challenge }) { const result = await payInvoice(challenge.request.invoice) return Credential.serialize({ challenge, payload: { preimage: result.preimage, }, }) }, }) // ---cut--- const { fetch } = Mppx.create({ methods: [clientMethod], polyfill: false, }) const response = await fetch('https://api.example.com/premium') ``` ### Server Pass the server method to `Mppx.create`: ```ts twoslash [server.ts] import { Method, Receipt, z } from 'mppx' import { Mppx } from 'mppx/server' const lightning = Method.from({ intent: 'charge', name: 'lightning', schema: { credential: { payload: z.object({ preimage: z.string(), }), }, request: z.object({ amount: z.string(), currency: z.string(), invoice: z.string(), paymentHash: z.string(), recipient: z.string(), }), }, }) declare function bytesToHex(bytes: Uint8Array): string declare function hexToBytes(hex: string): Uint8Array declare function sha256(data: Uint8Array): Uint8Array const serverMethod = Method.toServer(lightning, { async verify({ credential }) { const preimage = credential.payload.preimage const expectedHash = credential.challenge.request.paymentHash const actualHash = bytesToHex(sha256(hexToBytes(preimage))) if (actualHash !== expectedHash) { throw new Error('Preimage does not match payment hash') } return Receipt.from({ method: 'lightning', reference: preimage, status: 'success', timestamp: new Date().toISOString(), }) }, }) // ---cut--- const mppx = Mppx.create({ methods: [serverMethod], }) ``` ### Advanced options ### Pre-fill defaults Use `defaults` to pre-fill request parameters so callers don't repeat them. Fields in `defaults` become optional at the call site. ```ts twoslash [methods.server.ts] import { Method, Receipt, z } from 'mppx' const lightning = Method.from({ intent: 'charge', name: 'lightning', schema: { credential: { payload: z.object({ preimage: z.string() }) }, request: z.object({ amount: z.string(), currency: z.string(), invoice: z.string(), paymentHash: z.string(), recipient: z.string() }), }, }) // ---cut--- const serverMethod = Method.toServer(lightning, { defaults: { currency: 'BTC', recipient: 'lnbc1...', }, async verify({ credential }) { return Receipt.from({ method: 'lightning', reference: credential.payload.preimage, status: 'success', timestamp: new Date().toISOString(), }) }, }) ``` ### Transform with `z.pipe` Use `z.pipe` to accept human-readable input and emit a normalized wire format. The built-in `tempo` method uses this to convert dollar amounts to atomic units. ```ts twoslash [methods.ts] import { Method, z } from 'mppx' import { parseUnits } from 'viem' export const charge = Method.from({ intent: 'charge', name: 'acme-pay', schema: { credential: { payload: z.object({ receiptId: z.string() }), }, request: z.pipe( z.object({ amount: z.string(), currency: z.string(), decimals: z.number(), recipient: z.string(), }), z.transform(({ amount, decimals, ...rest }) => ({ ...rest, amount: parseUnits(amount, decimals).toString(), })), ), }, }) ``` Callers pass `{ amount: '1.50', decimals: 6 }`, the Challenge contains `{ amount: '1500000' }`. Use `parseUnits` from viem for decimal-safe conversion—never use `Number()` for monetary amounts. ### Client context Declare a `context` schema to accept per-call parameters. The context is validated at runtime before `createCredential` runs. ```ts twoslash [methods.client.ts] import { Credential, Method, z } from 'mppx' const lightning = Method.from({ intent: 'charge', name: 'lightning', schema: { credential: { payload: z.object({ preimage: z.string() }) }, request: z.object({ amount: z.string(), currency: z.string(), invoice: z.string(), paymentHash: z.string(), recipient: z.string() }), }, }) declare function payInvoice(invoice: string, maxFeeSats: number): Promise<{ preimage: string }> // ---cut--- const clientMethod = Method.toClient(lightning, { context: z.object({ maxFeeSats: z.number() }), async createCredential({ challenge, context }) { const result = await payInvoice(challenge.request.invoice, context.maxFeeSats) return Credential.serialize({ challenge, payload: { preimage: result.preimage } }) }, }) ``` ### Request hook Use the `request` hook to enrich parameters before the Challenge is issued: ```ts twoslash [methods.server.ts] import { Method, Receipt, z } from 'mppx' const lightning = Method.from({ intent: 'charge', name: 'lightning', schema: { credential: { payload: z.object({ preimage: z.string() }) }, request: z.object({ amount: z.string(), currency: z.string(), invoice: z.string(), paymentHash: z.string(), recipient: z.string() }), }, }) declare function createInvoice(amount: string): Promise<{ invoice: string; hash: string }> // ---cut--- const serverMethod = Method.toServer(lightning, { async request({ request }) { const result = await createInvoice(request.amount) return { ...request, invoice: result.invoice, paymentHash: result.hash } }, async verify({ credential }) { return Receipt.from({ method: 'lightning', reference: credential.payload.preimage, status: 'success', timestamp: new Date().toISOString(), }) }, }) ``` ### Respond hook Use `respond` to return a Response directly after verification, skipping the route handler. Return `undefined` to let the handler run normally. ```ts twoslash [methods.server.ts] import { Method, Receipt, z } from 'mppx' const lightning = Method.from({ intent: 'charge', name: 'lightning', schema: { credential: { payload: z.object({ preimage: z.string() }) }, request: z.object({ amount: z.string(), currency: z.string(), invoice: z.string(), paymentHash: z.string(), recipient: z.string() }), }, }) // ---cut--- const serverMethod = Method.toServer(lightning, { async verify({ credential }) { return Receipt.from({ method: 'lightning', reference: credential.payload.preimage, status: 'success', timestamp: new Date().toISOString(), }) }, respond({ input }) { if (input.method === 'POST' && input.headers.get('content-length') === '0') { return new Response(null, { status: 204 }) } return undefined }, }) ``` ## First-party SDK When you want others to use your payment method, package it as a standalone npm module. Users install it and import your method the same way they use `tempo` or `stripe`—a single import gives them both `Mppx` and your method factory. ### Package structure Organize your SDK with three export paths: root (shared schemas), `./client`, and `./server`. Start with a single intent (`charge`) and add more later. ``` my-method-sdk/ ├── src/ │ ├── index.ts # Re-export shared schemas │ ├── Methods.ts # Shared Method.from() definitions │ ├── client/ │ │ ├── index.ts # ./client entry point │ │ └── Charge.ts # Client charge implementation │ └── server/ │ ├── index.ts # ./server entry point │ └── Charge.ts # Server charge implementation ├── package.json └── tsconfig.json ``` ### Exports map Define three entry points in `package.json`. Declare `mppx` as a peer dependency so the user's app shares a single instance. ```json [package.json] { "name": "@my-org/my-method-sdk", "type": "module", "sideEffects": false, "files": ["dist", "src"], "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" }, "./client": { "types": "./dist/client/index.d.ts", "default": "./dist/client/index.js" }, "./server": { "types": "./dist/server/index.d.ts", "default": "./dist/server/index.js" } }, "peerDependencies": { "mppx": ">=0.3.15" } } ``` ### Shared method definition Define your schemas once in a shared file. Both client and server import from here. ```ts [src/Methods.ts] import { Method, z } from 'mppx' export const charge = Method.from({ intent: 'charge', name: 'my-method', schema: { credential: { payload: z.object({ proof: z.string() }), }, request: z.object({ amount: z.string(), currency: z.string(), recipient: z.string(), }), }, }) ``` ### Re-export `Mppx` Re-export `Mppx` (and `Expires`, `Store` on the server) from your entry points so users need only one import: ```ts [src/server/index.ts] export { charge } from './Charge.js' export { Mppx, Expires, Store } from 'mppx/server' ``` ```ts [src/client/index.ts] export { charge } from './Charge.js' export { Mppx } from 'mppx/client' ``` Users get a single-line import: ```ts [server.ts] import { Mppx, charge } from '@my-org/my-method-sdk/server' const mppx = Mppx.create({ methods: [charge({ /* config */ })], }) ``` ### Advanced SDK patterns ### Method namespace When your SDK supports multiple intents, export a namespace that groups them under a single name. Calling the namespace directly defaults to `charge`. ```ts [src/server/Methods.ts] import { charge as charge_ } from './Charge.js' import { session as session_ } from './Session.js' export function myMethod(parameters: myMethod.Parameters) { return myMethod.charge(parameters) } export namespace myMethod { export type Parameters = charge_.Parameters export const charge = charge_ export const session = session_ } ``` ```ts [server.ts] import { myMethod } from '@my-org/my-method-sdk/server' myMethod(opts) // defaults to charge myMethod.charge(opts) // explicit charge myMethod.session(opts) // explicit session ``` ### Augment the returned method Use `Object.assign` to attach lifecycle methods (like `cleanup` or `close`) to the method object returned by `Method.toClient` or `Method.toServer`: ```ts [src/client/Charge.ts] import { Credential, Method } from 'mppx' import * as Methods from '../Methods.js' export function charge(parameters: charge.Parameters) { let connection: WebSocket | null = null const method = Method.toClient(Methods.charge, { async createCredential({ challenge }) { // ... pay and return credential }, }) async function cleanup() { connection?.close() } return Object.assign(method, { cleanup }) } ``` ### Reference implementations | SDK | Payment rail | Intents | Source | |---|---|---|---| | [`@buildonspark/lightning-mpp-sdk`](https://github.com/buildonspark/lightning-mpp-sdk) | Lightning Network | charge, session | [GitHub](https://github.com/buildonspark/lightning-mpp-sdk) | ## Gotchas * **Always reject invalid proofs.** Throw an error from `verify` when verification fails. Never return a success Receipt without checking the proof. * **Use decimal-safe math for amounts.** Use `parseUnits`/`formatUnits` from viem instead of `Number()` or floating-point arithmetic. * **Verify against the original Challenge.** Always check the Credential's proof against the request fields from the Challenge (amount, currency, recipient). Don't trust the payload alone. * **Keep secrets server-side.** The Challenge is sent to the client. Don't put API keys, private keys, or other secrets in request fields. * **Use `methodDetails` for method-specific fields.** Nest non-standard request fields under a `methodDetails` object to avoid collisions with the base schema. * **Make invoice/order creation idempotent.** The `request` hook runs on both the initial `402` and the Credential submission. Don't generate a new invoice if one already exists for the Challenge. * **Clean up resources.** If your client method opens WebSocket connections, SDK instances, or listeners, expose a `cleanup()` method so callers can tear them down. ## SDK references * [`Method.from`](/sdk/typescript/Method.from) — Define a payment method with schemas * [`Method.toClient`](/sdk/typescript/core/Method.toClient) — Extend a method with client-side Credential creation logic * [`Method.toServer`](/sdk/typescript/core/Method.toServer) — Extend a method with server-side verification logic * [Custom HTML](/sdk/typescript/html/custom) — Add payment link support to your method # SDKs \[Official implementations in multiple languages] ## Capabilities | Capability | TypeScript | Python | Rust | Go | Ruby | |---|---|---|---|---|---| | **Client** | | | | | | | **Server** | | | | | | | **Core types** | | | | | | | **Charge intent** | | | | | | | **Event handling** | | | | | | | **Session intent** | | | | | | | **Stripe method** | | | | | | | **Fee sponsorship** | | | | | | | **Proof credentials** | | | | | | | **MCP support** | | | | | | | **Framework middleware** | Next.js, Hono, Express, Elysia | FastAPI | Axum, Tower | net/http, Gin, Echo, Chi | Rack | | **HTTP transport** | fetch polyfill | httpx transport | reqwest-middleware | http.RoundTripper | async-http | ## Other languages Community-maintained SDKs extend MPP to more language ecosystems. | Language | SDK | Maintainer | Status | Links | |---|---|---|---|---| | Elixir | `mpp` | ZenHive | Community | [GitHub](https://github.com/ZenHive/mpp) · [hex.pm](https://hex.pm/packages/mpp) · [Docs](https://hexdocs.pm/mpp/) | | Go | `mppx` | cp0x | Community | [GitHub](https://github.com/cp0x-org/mppx) · [Docs](https://pkg.go.dev/github.com/cp0x-org/mppx) | Want to add another SDK? Open a PR and we can list it here. # SDK features \[Parity across TypeScript, Python, Rust, and Ruby] This page tracks which features are implemented in each official SDK. ## Core | Component | [TypeScript](https://github.com/wevm/mppx) | [Rust](https://github.com/tempoxyz/mpp-rs) | [Python](https://github.com/tempoxyz/pympp) | [Ruby](https://github.com/stripe/mpp-rb) | |---|---|---|---|---| | Client | ✓ | ✓ | ✓ | ✓ | | Event handling | ✓ | ✓ | ✓ | ✓ | | Proxy | ✓ | ✓ | — | — | | Server | ✓ | ✓ | ✓ | ✓ | ## Payment methods | Method | TypeScript | Rust | Python | Ruby | |---|---|---|---|---| | [Tempo](/payment-methods/tempo) | ✓ | ✓ | ✓ | ✓ | | [Stripe](/payment-methods/stripe) | ✓ | ✓ | ✓ | ✓ | Additional payment methods implement their own SDKs. Refer to the maintaining organizations for support and availability. ## Intents | Intent | TypeScript | Rust | Python | Ruby | |---|---|---|---|---| | [Charge](/intents/charge) | ✓ | ✓ | ✓ | ✓ | | [Session](/payment-methods/tempo/session) | ✓ | ✓ | — | — | | [Subscription](/intents/subscription) | ✓ | — | — | — | ## Transports | Transport | TypeScript | Rust | Python | Ruby | |---|---|---|---|---| | [HTTP](/protocol/transports/http) | ✓ | ✓ | ✓ | ✓ | | [MCP](/protocol/transports/mcp) | ✓ | ✓ | ✓ | ✓ | ### HTTP framework integrations | Role | TypeScript | Rust | Python | Ruby | |---|---|---|---|---| | **Client** | fetch | reqwest | httpx | async-http | | **Server** | Elysia, Express, Hono, Next.js | Axum, Tower | Decorator (`@server.pay`) | Rack | ### MCP framework integrations | Role | TypeScript | Rust | Python | Ruby | |---|---|---|---|---| | **Client** | MCP SDK transport | — | MCP SDK transport | — | | **Server** | MCP SDK transport | — | FastMCP (`@pay` decorator) | — | ## Tempo features | Feature | TypeScript | Rust | Python | Ruby | |---|---|---|---|---| | Charge verification (server) | ✓ | ✓ | ✓ | ✓ | | Credential creation (client) | ✓ | ✓ | ✓ | ✓ | | `transaction` payload type | ✓ | ✓ | ✓ | ✓ | | `hash` payload type | ✓ | ✓ | ✓ | ✓ | | `proof` payload type | ✓ | — | — | ✓ | | Fee payer co-signing | ✓ | ✓ | ✓ | ✓ | | Session channels (open/voucher/topUp/close) | ✓ | ✓ | — | — | | SSE metered streaming | ✓ | ✓ | — | — | | Attribution memo | ✓ | ✓ | ✓ | ✓ | # Getting started \[The mppx TypeScript library] ## Overview The `mppx` TypeScript library provides a typed interface over the Machine Payments Protocol, from high-level abstractions to low-level primitives and building blocks.
## Install :::code-group ```bash [npm] $ npm install mppx ``` ```bash [pnpm] $ pnpm add mppx ``` ```bash [bun] $ bun add mppx ``` ::: ## Quick start This quick start guide shows you how to use `mppx` with the [`tempo` payment method](/payment-methods/tempo). You can apply the same patterns to [other payment methods](/payment-methods).
::::steps ### Install peer dependencies In this example, you use the `viem` library to instantiate an account. :::code-group ```bash [npm] $ npm install viem ``` ```bash [pnpm] $ pnpm add viem ``` ```bash [bun] $ bun add viem ``` ::: ### Define an account Next, define an account to sign payments. ```ts twoslash [define-account.ts] import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0xabc…123') ``` :::tip When using Tempo, you can also use [Passkey or WebCrypto accounts](https://viem.sh/tempo/accounts). ::: ### Create payment handler Call `Mppx.create` at startup. This polyfills the global `fetch` to automatically handle `402` payment challenges. ```ts twoslash [create-paid-fetch.ts] import { privateKeyToAccount } from 'viem/accounts' import { Mppx, tempo } from 'mppx/client' // [!code hl] const account = privateKeyToAccount('0xabc…123') Mppx.create({ // [!code hl] methods: [tempo({ account })], // [!code hl] }) // [!code hl] ``` :::tip If you want to avoid polyfilling, use the returned `fetch` instead. ```ts const mppx = Mppx.create({ polyfill: false, // [!code hl] methods: [tempo({ account })] }) const response = await mppx.fetch('https://mpp.dev/api/ping/paid') // [!code hl] ``` ::: ### Request protected resources Use `fetch`. Payment happens when a server returns `402`. ```ts twoslash [fetch-resource.ts] const response = await fetch('https://mpp.dev/api/ping/paid') ``` ::::
### Framework mode Use the framework-specific middleware from `mppx` to integrate payment into your server. Each middleware handles the `402` challenge/credential flow and attaches receipts automatically. ::::code-group ```ts [Next.js] import { Mppx, tempo } from 'mppx/nextjs' // [!code hl:start] const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) // [!code hl:end] export const GET = mppx.charge({ amount: '0.1' }) // [!code hl] (() => Response.json({ data: '...' })) ``` ```ts [Hono] import { Hono } from 'hono' import { Mppx, tempo } from 'mppx/hono' const app = new Hono() // [!code hl:start] const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) // [!code hl:end] app.get( '/resource', mppx.charge({ amount: '0.1' }), // [!code hl] (c) => c.json({ data: '...' }), ) ``` ```ts [Elysia] import { Elysia } from 'elysia' import { Mppx, tempo } from 'mppx/elysia' // [!code hl:start] const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) // [!code hl:end] const app = new Elysia() .guard( { beforeHandle: mppx.charge({ amount: '0.1' }) }, // [!code hl] (app) => app.get('/resource', () => ({ data: '...' })), ) ``` ```ts [Express] import express from 'express' import { Mppx, tempo } from 'mppx/express' const app = express() // [!code hl:start] const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) // [!code hl:end] app.get( '/resource', mppx.charge({ amount: '0.1' }), // [!code hl] (req, res) => res.json({ data: '...' })) ``` :::: :::tip You can also override `currency` and `recipient` per call if different routes need different payment configurations. ```ts mppx.charge({ amount: '0.1', currency: '0x…', // [!code ++] recipient: '0x…', // [!code ++] }) ``` ::: :::note Don't see your framework? `mppx` is designed to be framework-agnostic. See [Manual mode](#manual-mode) below. :::
***
### Manual mode If you prefer full control over the payment flow, use `mppx/server` directly with the Fetch API. ```ts twoslash import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) // [!code focus:start] export async function handler(request: Request) { const response = await mppx.charge({ amount: '0.1' })(request) // [!code focus:end] // Payment required: send 402 response with challenge if (response.status === 402) return response.challenge // Payment verified: attach receipt and return resource return response.withReceipt(Response.json({ data: '...' })) } ``` :::info\[Currency and recipient values] `currency` is the TIP-20 token contract address—`0x20c0…` is PathUSD on Tempo. `recipient` is the address that receives payment. See [Tempo payment method](/payment-methods/tempo) for supported tokens. ::: The intent handler accepts a [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)-compatible request object, and returns a `Response` object. The Fetch API is compatible with most server frameworks, including: [Hono](https://hono.dev), [Deno](https://deno.com), [Cloudflare Workers](https://workers.dev), [Next.js](https://nextjs.org), [Bun](https://bun.sh), and other Fetch API-compatible frameworks. :::tip You can also override `currency` and `recipient` per call if different routes need different payment configurations. ```ts const response = await mppx.charge({ amount: '0.1', currency: '0x…', // [!code ++] recipient: '0x…', // [!code ++] })(request) ``` :::
***
## Node.js & Express compatibility If your framework doesn't support the **Fetch API** (for example, Express or Node.js), you're likely interfacing with the [Node.js Request Listener API](https://nodejs.org/api/http.html#httpcreateserveroptions-requestlistener). Use the `Mppx.toNodeListener` helper to transform the handler into a Node.js-compatible listener. ```ts twoslash import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })], }) type IncomingMessage = import('node:http').IncomingMessage type ServerResponse = import('node:http').ServerResponse // ---cut--- export async function handler(req: IncomingMessage, res: ServerResponse) { const response = await Mppx.toNodeListener( // [!code ++] mppx.charge({ amount: '0.1' }) )(req, res) // [!code ++] // Payment required: send 402 response with challenge if (response.status === 402) return response.challenge // Payment verified: attach receipt and return resource return response.withReceipt(Response.json({ data: '...' })) } ```
The `mppx` package install automatically includes a [CLI tool](/sdk/typescript/cli) you can use to make the same request from the command line. ::::steps ### Create an account Create a Tempo account to sign payments. The account is auto-funded on testnet and key is stored in your system keychain. :::code-group ```bash [npm] $ npx mppx account create ``` ```bash [pnpm] $ pnpm mppx account create ``` ```bash [bun] $ bunx mppx account create ``` ::: ### Make a paid request Run the CLI with a URL to make a paid request. Payment is handled automatically when the server returns `402`. :::code-group ```bash [npm] $ npx mppx https://mpp.dev/api/ping/paid ``` ```bash [pnpm] $ pnpm mppx https://mpp.dev/api/ping/paid ``` ```bash [bun] $ bunx mppx https://mpp.dev/api/ping/paid ``` ::: ::::
# `evm` \[Sign EVM charge Credentials] Namespace for EVM payment methods and known asset metadata. ## Usage ```ts twoslash import { Mppx, evm } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount( '0x0123456789012345678901234567890123456789012345678901234567890123', ) Mppx.create({ methods: [ // [!code focus:start] evm.charge({ account, currencies: [evm.assets.baseSepolia.USDC], maxAmount: '1.00', }), // [!code focus:end] ], }) ``` ## Exports ### assets * **Type:** `typeof import('mppx/client').evm.assets` Known EVM asset metadata. Use `evm.assets.base.USDC` for Base mainnet and `evm.assets.baseSepolia.USDC` for Base Sepolia. Use `evm.assets.define` for custom EVM assets: ```ts twoslash import { evm } from 'mppx/client' const USDC = evm.assets.define({ address: '0x0000000000000000000000000000000000000000', decimals: 6, network: 'eip155:84532', transfer: { name: 'USD Coin', type: 'eip3009', version: '2', }, }) ``` ### chains * **Type:** `typeof import('mppx/client').evm.chains` Known EVM chain IDs. Use `evm.chains.base` for Base mainnet and `evm.chains.baseSepolia` for Base Sepolia. ### charge * **Type:** `typeof evm.charge` Creates an EVM charge payment method. See [`evm.charge`](/sdk/typescript/client/Method.evm.charge). # `Method.evm.charge` \[Sign EVM charge Credentials] Creates an EVM charge payment method for client-side EIP-3009 authorization signing. ## Usage ```ts twoslash import { Fetch, evm } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const fetch = Fetch.from({ methods: [ // [!code focus:start] evm.charge({ account: privateKeyToAccount( '0x0123456789012345678901234567890123456789012345678901234567890123', ), currencies: [evm.assets.baseSepolia.USDC], maxAmount: '1.00', }), // [!code focus:end] ], }) const response = await fetch('https://api.example.com/paid') console.log(response.status) // @log: 200 ``` The client signs native MPP EVM charge Challenges and x402 exact Challenges for compatible assets. ## Return type ```ts import type { Method } from 'mppx' type ReturnType = Method.Client ``` ## Parameters ### account * **Type:** `Account` Account that signs EVM charge Credentials. ```ts twoslash import { evm } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const method = evm.charge({ account: privateKeyToAccount( '0x0123456789012345678901234567890123456789012345678901234567890123', ), // [!code focus] }) ``` ### assets (optional) * **Type:** `readonly (Address | KnownAsset)[]` Legacy alias for `currencies`. ### authorization (optional) * **Type:** `{ name: string; version: string }` EIP-3009 token domain metadata for custom currencies. Known assets infer this value. ### currencies (optional) * **Type:** `readonly (Address | KnownAsset)[]` Allowlist of EVM currencies the client accepts. Use known assets when possible so mppx can infer chain, decimals, and transfer metadata. ```ts twoslash import { evm } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const method = evm.charge({ account: privateKeyToAccount( '0x0123456789012345678901234567890123456789012345678901234567890123', ), currencies: [evm.assets.baseSepolia.USDC], // [!code focus] }) ``` ### decimals (optional) * **Type:** `number` Token decimal places used to parse `maxAmount` when currency metadata doesn't provide decimals. ### maxAmount (optional) * **Type:** `string` Maximum display-unit amount the client pays. ### maxAtomicAmount (optional) * **Type:** `string` Maximum atomic-unit amount the client pays. ### networks (optional) * **Type:** `readonly number[]` Allowlist of EVM chain IDs the client accepts. # `tempo` \[Register default Tempo intents] Convenience function that creates both `tempo.charge` and `tempo.session` method intents with shared configuration. Register [`tempo.subscription`](/sdk/typescript/client/Method.tempo.subscription) separately for recurring payments. ## Usage ```ts twoslash import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0xabc…123') Mppx.create({ methods: [ // [!code focus:start] tempo({ account }), // [!code focus:end] ], }) ``` This is equivalent to: ```ts twoslash import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0xabc…123') Mppx.create({ methods: [ tempo.charge({ account }), tempo.session({ account }), ], }) ``` ### Standalone session manager `tempo.session()` also creates a standalone session manager with explicit `.fetch()`, `.close()`, and `.sse()` methods—no `Mppx.create` required: ```ts twoslash import { tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const session = tempo.session({ account: privateKeyToAccount('0x...'), maxDeposit: '1', }) const res = await session.fetch('https://api.example.com/resource') await session.close() ``` See [`tempo.session` manager](/sdk/typescript/client/Method.tempo.session-manager) for the full API. ## Return type ```ts type ReturnType = readonly [Method.Client, Method.Client] ``` A tuple of `[charge, session]` methods. `Mppx.create` accepts tuples in the `methods` array and flattens them automatically. ## Parameters Accepts the union of [`tempo.charge`](/sdk/typescript/client/Method.tempo.charge) and [`tempo.session`](/sdk/typescript/client/Method.tempo.session) parameters. The most common are listed below. ### account (optional) * **Type:** `Account` Account to sign transactions and vouchers with. ### deposit (optional) * **Type:** `string` Initial deposit amount in human-readable units (for example `"10"` for 10 tokens). Enables automatic session channel management. ### getClient (optional) * **Type:** `(parameters: { chainId?: number }) => MaybePromise` Function that returns a viem client for the given chain ID. ### maxDeposit (optional) * **Type:** `string` Maximum deposit in human-readable units. Caps the server's `suggestedDeposit`. Enables auto-management like `deposit`. # `Method.tempo.charge` \[One-time payments] Creates a Tempo charge payment method for client-side transaction and proof signing. ## Usage ```ts twoslash import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0xabc…123') Mppx.create({ methods: [ // [!code focus:start] tempo.charge({ account }), // [!code focus:end] ], }) const response = await fetch('https://mpp.dev/api/ping/paid') ``` For non-zero Challenges, the client returns either a `transaction` or `hash` payload depending on `mode`. For zero-amount Challenges, it always returns a `proof` payload and skips transaction construction. ## Return type ```ts import type { Method } from 'mppx' type ReturnType = Method.Client ``` ## Parameters ### account (optional) * **Type:** `Account` Account to sign transactions and zero-dollar proofs with. You can override this per call using the context. ```ts twoslash import { tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const method = tempo.charge({ account: privateKeyToAccount('0xabc…123'), // [!code focus] }) ``` ### autoSwap (optional) * **Type:** `boolean | { tokenIn?: Address[]; slippage?: number }` Automatically swap from a supported stablecoin (USDC.e, pathUSD) via the Tempo DEX precompile when the client lacks sufficient balance of the requested currency. Pass `true` to enable, or an object for custom tokens and slippage. ```ts twoslash import { tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const method = tempo.charge({ account: privateKeyToAccount('0xabc…123'), autoSwap: true, // [!code focus] }) ``` ### clientId (optional) * **Type:** `string` Client identifier used to derive the client fingerprint in attribution memos. ### expectedRecipients (optional) * **Type:** `readonly string[]` Allowlist of addresses the client accepts as split recipients. When set, the client rejects any Challenge whose split recipients are not in this list, preventing a compromised server from redirecting funds. ```ts twoslash import { tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const method = tempo.charge({ account: privateKeyToAccount('0xabc…123'), expectedRecipients: [ // [!code focus] '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', // [!code focus] ], // [!code focus] }) ``` ### getClient (optional) * **Type:** `(parameters: { chainId: number }) => MaybePromise` Function that returns a viem client for the given chain ID. ### mode (optional) * **Type:** `'push' | 'pull'` * **Default:** `'push'` for JSON-RPC accounts, `'pull'` for local accounts Controls how non-zero charge transactions are submitted. Zero-amount Challenges ignore this option and always use a `proof` payload. * `'push'`: the client broadcasts the transaction and sends the transaction hash to the server for verification. * `'pull'`: the client signs the transaction and sends the serialized transaction to the server, which broadcasts it. This is required for server-side [fee sponsorship](/quickstart/server#fee-sponsorship). ```ts twoslash import { tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const method = tempo.charge({ account: privateKeyToAccount('0xabc…123'), mode: 'pull', // [!code focus] }) ``` # `Method.tempo.session` \[Low-cost high-throughput payments] Creates a Tempo payment session method for client-side voucher signing. Pass the result to `Mppx.create` for automatic 402 handling. :::info For standalone session management with explicit `.fetch()`, `.close()`, and `.sse()` methods, see [`tempo.session` manager](/sdk/typescript/client/Method.tempo.session-manager). ::: ## Usage ```ts twoslash import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0xabc…123') Mppx.create({ methods: [ // [!code focus:start] tempo.session({ account }), // [!code focus:end] ], }) ``` ### With charge and session Register both intents on a single client. ```ts twoslash import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0xabc…123') Mppx.create({ methods: [ tempo.charge({ account }), tempo.session({ account }), ], }) ``` ## Return type ```ts twoslash import type { Method } from 'mppx' type ReturnType = Method.Client ``` ## Parameters ### account (optional) * **Type:** `Account` Account to sign vouchers with. You can override this per call using the context. ```ts twoslash import { tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const method = tempo.session({ account: privateKeyToAccount('0xabc…123'), // [!code focus] }) ``` ### decimals (optional) * **Type:** `number` * **Default:** `6` Token decimals for parsing human-readable amounts. ### deposit (optional) * **Type:** `string` Initial deposit amount in human-readable units (for example `"10"` for 10 tokens). When set, the client handles the full channel lifecycle (open, voucher, cumulative tracking) automatically. ### escrowContract (optional) * **Type:** `Address` Escrow contract address override. Derived from the server challenge if not provided. ### getClient (optional) * **Type:** `(parameters: { chainId: number }) => MaybePromise` Function that returns a viem client for the given chain ID. ### maxDeposit (optional) * **Type:** `string` Maximum deposit in human-readable units (for example `"10"`). Caps the server's `suggestedDeposit`. Enables auto-management like `deposit`. ### onChannelUpdate (optional) * **Type:** `(entry: ChannelEntry) => void` Called whenever channel state changes (open, voucher, close, recovery). ### voucherSigner (optional) * **Type:** `Account` Account that signs voucher digests. Defaults to `account`. Access-key accounts sign raw vouchers as their access-key address. # `tempo.session` \[Standalone session manager] Creates a standalone session manager that handles the full payment channel lifecycle—open, fetch, stream, and close—without `Mppx.create` or `Fetch.from`. Use this when you need direct control over session state instead of automatic 402 handling. ## Usage ```ts twoslash import { tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0x...') const session = tempo.session({ account, maxDeposit: '1', }) // Make a paid request — opens a channel on first call const response = await session.fetch('https://api.example.com/resource') console.log(response.status) // @log: 200 // Access payment metadata console.log(response.receipt) console.log(response.cumulative) // Close the channel and settle on-chain const receipt = await session.close() ``` :::warning Channels remain open until you call `session.close()`. Always close sessions when done to settle on-chain and reclaim unspent deposit. ::: ### Session resumption All channel state is held in memory. If the client process restarts, the session is lost and a new on-chain channel opens on the next request—the previous channel's deposit is orphaned until manually closed. When the server includes a `channelId` in the `402` Challenge, the client attempts to recover the channel by reading its on-chain state. If the channel has a positive deposit and is not finalized, it resumes from the on-chain settled amount. ### With SSE streaming Stream server-sent events with automatic voucher handling. The session signs incremental vouchers as the server requests them during the stream. ```ts twoslash import { tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const session = tempo.session({ account: privateKeyToAccount('0x...'), maxDeposit: '5', }) const stream = await session.sse('https://api.example.com/stream', { onReceipt(receipt) { console.log('Receipt:', receipt) }, signal: AbortSignal.timeout(30_000), }) for await (const message of stream) { console.log(message) } await session.close() ``` ### With WebSocket streaming Open a paid WebSocket session. The session manager handles the HTTP `402` probe, channel open, and in-band voucher signing. Payment control frames are intercepted internally — your socket listeners only see application messages. ```ts import { tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const session = tempo.session({ account: privateKeyToAccount('0x...'), maxDeposit: '1', }) const url = new URL('wss://api.example.com/ws/chat') url.searchParams.set('prompt', 'Tell me something interesting') const socket = await session.ws(url, { onReceipt(receipt) { console.log('Receipt:', receipt) }, }) socket.addEventListener('message', (event) => { process.stdout.write(event.data) }) await new Promise((resolve) => { socket.addEventListener('close', () => resolve(), { once: true }) }) const receipt = await session.close() ``` :::tip In Node.js or other server-side runtimes without a global `WebSocket`, pass the constructor to `tempo.session()`: ```ts import { WebSocket } from 'isows' import { tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const session = tempo.session({ account: privateKeyToAccount('0x...'), maxDeposit: '1', webSocket: WebSocket, }) ``` ::: ### With explicit open Open the channel before the first request. This separates the on-chain deposit transaction from the first API call. ```ts twoslash import { tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0x...') const session = tempo.session({ account, maxDeposit: '10', }) // Trigger a 402 to receive the challenge await session.fetch('https://api.example.com/resource') // Open the channel explicitly await session.open() // Subsequent requests use the open channel const response = await session.fetch('https://api.example.com/resource') ``` ## Return type `tempo.session()` returns a `SessionManager` object: ```ts type SessionRequestInit = RequestInit & { orderChallenges?: SessionOrderChallenges } type SessionManager = { readonly channelId: Hex | undefined readonly cumulative: bigint readonly opened: boolean open(options?: { deposit?: bigint }): Promise fetch(input: RequestInfo | URL, init?: SessionRequestInit): Promise sse( input: RequestInfo | URL, init?: SessionRequestInit & { onReceipt?: (receipt: SessionReceipt) => void signal?: AbortSignal }, ): Promise> ws( input: string | URL, init?: { onReceipt?: (receipt: SessionReceipt) => void orderChallenges?: SessionOrderChallenges protocols?: string | string[] signal?: AbortSignal }, ): Promise close(): Promise } ``` ### `PaymentResponse` `session.fetch()` returns a standard `Response` extended with payment metadata: ```ts type PaymentResponse = Response & { receipt: SessionReceipt | null challenge: Challenge | null channelId: Hex | null cumulative: bigint } ``` ### `SessionReceipt` The receipt returned by `session.close()` and available on `PaymentResponse.receipt`: ```ts type SessionReceipt = { method: 'tempo' intent: 'session' status: 'success' timestamp: string /** Payment channel ID (not a transaction hash). */ reference: string challengeId: string channelId: Hex /** Highest cumulative voucher amount accepted by the server. */ acceptedCumulative: string /** Total amount spent in this session. */ spent: string /** Number of units consumed (if the server tracks units). */ units?: number /** On-chain settlement transaction hash. Present after close. */ txHash?: Hex } ``` :::info The `reference` field contains the channel ID, not a transaction hash. To get the settlement transaction hash, read `txHash` from the receipt returned by `session.close()`. ::: ## Parameters ### account (optional) * **Type:** `Account` Account to sign vouchers with. ### client (optional) * **Type:** `Client` Viem client instance. Shorthand for `getClient: () => client`. ### decimals (optional) * **Type:** `number` * **Default:** `6` Token decimals for parsing human-readable amounts like `maxDeposit`. ### escrowContract (optional) * **Type:** `Address` Escrow contract address override. Derived from the server Challenge if not provided. ### fetch (optional) * **Type:** `typeof globalThis.fetch` * **Default:** `globalThis.fetch` Custom fetch function to use for requests. ### getClient (optional) * **Type:** `(parameters: { chainId: number }) => MaybePromise` Function that returns a viem client for the given chain ID. ### maxDeposit (optional) * **Type:** `string` Maximum deposit in human-readable units (for example `"10"` for 10 tokens). Caps the server's suggested deposit and enables automatic channel management. ### orderChallenges (optional) * **Type:** `OrderChallenges<[SessionMethod]>` Filters and sorts supported session Challenges before Credential creation. ### voucherSigner (optional) * **Type:** `Account` Account that signs voucher digests. Defaults to `account`. Access-key accounts sign raw vouchers as their access-key address. ### webSocket (optional) * **Type:** `WebSocketConstructor` WebSocket constructor for runtimes without a global `WebSocket` (for example Node.js). Required for `session.ws()` in non-browser environments. Use [`isows`](https://github.com/wevm/isows) for isomorphic support. ```ts import { WebSocket } from 'isows' const session = tempo.session({ account, webSocket: WebSocket, }) ``` ## Methods ### `session.close()` Closes the payment channel and settles on-chain. Returns the final `SessionReceipt` if available, or `undefined` if no channel was open. ```ts const receipt = await session.close() ``` ### `session.fetch(input, init?)` Makes a payment-aware request. Handles the 402 Challenge → Credential flow automatically, opening a channel on first use if needed. Returns a [`PaymentResponse`](#paymentresponse) with payment metadata attached. ```ts const response = await session.fetch('https://api.example.com/resource') console.log(response.receipt) console.log(response.cumulative) ``` ### `session.open(options?)` Opens the payment channel explicitly. You must make at least one `fetch()`, `sse()`, or `ws()` call first to receive a 402 Challenge from the server. * **options.deposit** (`bigint`, optional) — Raw deposit amount in token units. ```ts await session.fetch('https://api.example.com/resource') await session.open() ``` ### `session.sse(input, init?)` Opens a server-sent events stream with automatic voucher signing. The server requests incremental vouchers via `payment-need-voucher` events, and the session signs them transparently. Returns an `AsyncIterable` of message payloads. * **init.onReceipt** (`(receipt: SessionReceipt) => void`, optional) — Called when the server sends a payment Receipt during the stream. * **init.signal** (`AbortSignal`, optional) — Aborts the stream. ```ts const stream = await session.sse('https://api.example.com/stream', { onReceipt: (receipt) => console.log('paid:', receipt), signal: AbortSignal.timeout(60_000), }) for await (const message of stream) { console.log(message) } ``` ### `session.ws(input, init?)` Opens a paid WebSocket connection. Performs the HTTP `402` probe, creates the first credential, opens the socket, and sends the auth frame. Returns a `WebSocket` that only surfaces application messages — payment control frames are handled internally. * **input** (`string | URL`) — The WebSocket URL (`ws:` or `wss:`). * **init.onReceipt** (`(receipt: SessionReceipt) => void`, optional) — Called when the server sends a payment Receipt. * **init.protocols** (`string | string[]`, optional) — WebSocket sub-protocols. * **init.signal** (`AbortSignal`, optional) — Aborts the payment flow and closes the socket. ```ts const socket = await session.ws('wss://api.example.com/ws/chat', { onReceipt: (receipt) => console.log('paid:', receipt), }) socket.addEventListener('message', (event) => { console.log(event.data) }) ``` :::info The returned `WebSocket` is a managed wrapper. Messages are buffered until the first listener is installed, so you won't miss frames between `await session.ws()` and adding your `message` listener. ::: ## Properties ### `session.channelId` * **Type:** `Hex | undefined` The on-chain channel ID after the channel opens. `undefined` before the first successful payment. ### `session.cumulative` * **Type:** `bigint` The cumulative amount paid across all vouchers in this session. Starts at `0n`. ### `session.opened` * **Type:** `boolean` Whether the payment channel is currently open. # `Method.tempo.subscription` \[Recurring stablecoin payments] Creates a Tempo subscription client method for signing access-key authorizations. ## Usage ```ts twoslash import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0xabc…123') Mppx.create({ methods: [ // [!code focus:start] tempo.subscription({ account }), // [!code focus:end] ], }) const response = await fetch('https://api.example.com/pro') console.log(response.status) // @log: 200 ``` ### With request validation Use `validateRequest` to enforce local client policy before signing. ```ts twoslash import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0xabc…123') Mppx.create({ methods: [ tempo.subscription({ account, validateRequest: (request) => { if (request.periodUnit !== 'week') { throw new Error('Expected weekly billing') } }, }), ], }) ``` ### With explicit access key Most servers include an access key in the Challenge. Pass `accessKey` only when the server expects the client to supply a specific key. ```ts twoslash import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0xabc…123') Mppx.create({ methods: [ tempo.subscription({ accessKey: { accessKeyAddress: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', keyType: 'secp256k1', }, account, }), ], }) ``` ## Return type ```ts twoslash import type { Method } from 'mppx' type ReturnType = Method.Client ``` ## Parameters ### accessKey (optional) * **Type:** `SubscriptionAccessKey` Access key authorized by the root account. Omit this when the Challenge includes `methodDetails.accessKey`. ### account (optional) * **Type:** `Account | Address` Account that signs the key authorization. You can override this per request with fetch context. ### getClient (optional) * **Type:** `(parameters: { chainId?: number }) => MaybePromise` Function that returns a viem client for the given Tempo chain ID. ### validateRequest (optional) * **Type:** `(request: SubscriptionRequest) => MaybePromise` Runs before the client signs the key authorization. Throw to reject subscription terms. ## Context Pass request-specific values through fetch context when needed. ```ts twoslash import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0xabc…123') const mppx = Mppx.create({ methods: [tempo.subscription()], }) await mppx.fetch('https://api.example.com/pro', { context: { account, }, }) ``` ### accessKey (optional) * **Type:** `SubscriptionAccessKey` Access key to use for this request. ### account (optional) * **Type:** `Account | Address` Account to use for this request. ## Cancellation `tempo.subscription()` doesn't expose a client-side cancel method. Call the service's cancellation endpoint so the server can mark the subscription `canceledAt`. You can also revoke the Tempo access key from the payer account as a wallet-level backstop. ```ts [client.ts] await fetch('https://api.example.com/subscription/cancel', { method: 'POST', }) ``` ```ts twoslash [revoke.ts] import { createClient, http } from 'viem' import { tempo } from 'viem/chains' import { privateKeyToAccount } from 'viem/accounts' import { Actions } from 'viem/tempo' const client = createClient({ account: privateKeyToAccount( '0x0000000000000000000000000000000000000000000000000000000000000001', // your account ), chain: tempo, transport: http(), }) await Actions.accessKey.revokeSync(client, { accessKey: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', }) ``` Revocation blocks future access-key charges, but it doesn't update the merchant's subscription record. Treat it as a backstop, not the primary cancellation flow. # `stripe` \[Register all Stripe intents] Convenience function that creates the Stripe `charge` method intent. ## Usage ```ts twoslash import { loadStripe } from '@stripe/stripe-js' import { Mppx, stripe } from 'mppx/client' const stripeJs = (await loadStripe('pk_test_...'))! Mppx.create({ methods: [ // [!code focus:start] stripe({ client: stripeJs, createToken: async (params) => { const res = await fetch('/api/create-spt', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(params), }) return (await res.json()).spt }, paymentMethod: 'pm_card_visa', }), // [!code focus:end] ], }) ``` ## Return type ```ts import type { Method } from 'mppx' type ReturnType = Method.Client ``` ## Parameters See [`stripe.charge`](/sdk/typescript/client/Method.stripe.charge) for the full parameter list. # `Method.stripe.charge` \[One-time payments via Shared Payment Tokens] Creates a Stripe charge payment method for client-side SPT-based payments. ## Usage ```ts twoslash import { loadStripe } from '@stripe/stripe-js' import { Mppx, stripe } from 'mppx/client' const stripeJs = (await loadStripe('pk_test_...'))! Mppx.create({ methods: [ // [!code focus:start] stripe.charge({ client: stripeJs, createToken: async ({ amount, currency, expiresAt, metadata, networkId, paymentMethod }) => { const res = await fetch('/api/create-spt', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ amount, currency, expiresAt, metadata, networkId, paymentMethod }), }) return (await res.json()).spt }, }), // [!code focus:end] ], }) ``` ## Return type ```ts import type { Method } from 'mppx' type ReturnType = Method.Client ``` ## Parameters ### client (optional) * **Type:** `StripeJs` Stripe.js instance from `@stripe/stripe-js`. Forwarded to the `createToken` callback for use with Stripe Elements. ```ts twoslash import { loadStripe } from '@stripe/stripe-js' import { stripe } from 'mppx/client' const stripeJs = (await loadStripe('pk_test_...'))! const method = stripe.charge({ client: stripeJs, // [!code focus] createToken: async (params) => '...', }) ``` ### createToken * **Type:** `(params: OnChallengeParameters) => Promise` Callback invoked when a Stripe challenge is received. Must return an SPT token string (`spt_...`). Typically proxied through a server endpoint since SPT creation requires a Stripe secret key. The callback receives: | Field | Type | Description | | --- | --- | --- | | `amount` | `string` | Payment amount in smallest currency unit | | `challenge` | `Challenge` | The parsed Challenge from the server | | `client` | `StripeJs \| undefined` | Stripe.js instance, if provided | | `currency` | `string` | Three-letter ISO currency code | | `expiresAt` | `number` | SPT expiration as a Unix timestamp (seconds) | | `metadata` | `Record` | Optional metadata from the Challenge | | `networkId` | `string \| undefined` | Stripe Business Network profile ID | | `paymentMethod` | `string \| undefined` | Stripe payment method ID | ### externalId (optional) * **Type:** `string` Client reference ID included in the Credential payload. ### paymentMethod (optional) * **Type:** `string` Default Stripe payment method ID (for example `pm_card_visa`). Overridden by `context.paymentMethod` at credential-creation time. ```ts twoslash import { stripe } from 'mppx/client' const method = stripe.charge({ createToken: async (params) => '...', paymentMethod: 'pm_card_visa', // [!code focus] }) ``` # `Mppx.create` \[Create a payment-aware fetch client] Creates a client-side payment handler. Returns a payment handler with a `fetch` function that automatically handles `402` Payment Required responses. By default, also polyfills `globalThis.fetch`. ## Usage ```ts twoslash import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0x...') Mppx.create({ methods: [tempo({ account })], }) // Global fetch now handles 402 automatically const res = await fetch('https://mpp.dev/api/ping/paid') ``` ### With payment hooks Register hooks on the returned `mppx` instance to observe the payment flow. ```ts twoslash import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0x...') const mppx = Mppx.create({ methods: [tempo({ account })], polyfill: false, }) const offFailure = mppx.onPaymentFailed(({ error, input }) => { console.error('payment failed:', input, error) }) const offResponse = mppx.onPaymentResponse(({ challenge, response }) => { console.log('payment response:', challenge.id, response.status) }) const res = await mppx.fetch('https://mpp.dev/api/ping/paid') console.log(res.status) // @log: 200 offFailure() offResponse() ``` ### Without polyfill Set `polyfill: false` to get a scoped fetch without modifying the global: ```ts twoslash import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0x...') const mppx = Mppx.create({ methods: [tempo({ account })], polyfill: false, // [!code hl] }) // Use the returned fetch // [!code hl] const res = await mppx.fetch('https://mpp.dev/api/ping/paid') // [!code hl] ``` ### Manual credential handling For full control over the payment flow: ```ts twoslash import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const mppx = Mppx.create({ methods: [tempo()], polyfill: false, // [!code hl] }) // [!code hl:start] const response = await fetch('https://mpp.dev/api/ping/paid') if (response.status === 402) { const credential = await mppx.createCredential(response, { account: privateKeyToAccount('0x...'), }) const paidResponse = await fetch('https://mpp.dev/api/ping/paid', { headers: { Authorization: credential }, }) } // [!code hl:end] ``` Pass `acceptPayment` to override method selection for this one response: ```ts twoslash import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const mppx = Mppx.create({ methods: [tempo()], polyfill: false, }) const response = await fetch('https://mpp.dev/api/ping/paid') const credential = await mppx.createCredential( response, { account: privateKeyToAccount('0x...') }, { acceptPayment: 'tempo/charge;q=1, tempo/session;q=0' }, // [!code focus] ) ``` ## Return type ```ts type Mppx = { /** Payment-aware fetch function that automatically handles 402 responses. */ fetch: Fetch /** The original, unwrapped fetch — bypasses payment interception. */ rawFetch: typeof globalThis.fetch /** The configured payment methods. */ methods: readonly Method.Client[] /** The transport used. */ transport: Transport /** Creates a credential from a payment-required response. */ createCredential: ( response: Response, context?: Context, options?: { acceptPayment?: string | AcceptPayment.Entry[] }, ) => Promise /** Register a handler for any client payment event. */ on(name: ClientEventName | '*', handler: ClientEventHandler): Unsubscribe /** Register a handler for received payment Challenges. */ onChallengeReceived(handler: ChallengeReceivedHandler): Unsubscribe /** Register a handler for created Credentials. */ onCredentialCreated(handler: CredentialCreatedHandler): Unsubscribe /** Register a handler for failed automatic payment handling. */ onPaymentFailed(handler: PaymentFailedHandler): Unsubscribe /** Register a handler for payment retry responses. */ onPaymentResponse(handler: PaymentResponseHandler): Unsubscribe } ``` ### `rawFetch` The original `fetch` function, before payment interception. Use `rawFetch` when you need to make requests that bypass the 402 handler—for example, probing a 402 endpoint for websocket auth tokens or calling APIs that return 402 for non-payment reasons. ```ts twoslash import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0x...') const mppx = Mppx.create({ polyfill: false, methods: [tempo({ account })], }) // Bypass payment interception const raw = await mppx.rawFetch('https://api.example.com/ws-auth') // [!code focus] ``` ## Payment hooks Payment hooks are for logging, monitoring, tracing, and request-local context. Each registration returns an unsubscribe function. | Hook | Runs when | |---|---| | `onChallengeReceived` | A `402` Challenge is selected | | `onCredentialCreated` | A Credential is created for the selected Challenge | | `onPaymentResponse` | The retry after payment returns a successful response | | `onPaymentFailed` | Challenge parsing, credential creation, or retry handling fails | | `on('*', handler)` | Any client payment event fires | `onChallengeReceived` runs before `onChallenge`. It can return a non-empty Credential string to override the default credential flow. Other hooks are observers: thrown errors are ignored and don't change payment handling. ```ts twoslash import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0x...') const mppx = Mppx.create({ methods: [tempo({ account })], polyfill: false, }) mppx.onChallengeReceived(async ({ challenge, createCredential }) => { console.log('challenge received:', challenge.id) return createCredential() }) mppx.on('*', ({ name }) => { console.log('payment event:', name) }) ``` ## Parameters ### acceptPaymentPolicy (optional) * **Type:** `'always' | 'same-origin' | 'never' | { origins: readonly string[] }` * **Default:** `'same-origin'` when `polyfill` is `true` in browsers, `'always'` otherwise Controls when `mppx` injects `Accept-Payment` on outgoing requests. Browser polyfills default to same-origin injection to avoid CORS preflight failures on APIs that don't support payment discovery. Use `{ origins }` for cross-origin paid APIs. Origin patterns support `*.` subdomain wildcards. ```ts twoslash import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0x...') Mppx.create({ acceptPaymentPolicy: { origins: ['https://api.example.com', '*.paid.example.com'], }, // [!code focus] methods: [tempo({ account })], }) ``` ### fetch (optional) * **Type:** `typeof globalThis.fetch` * **Default:** `globalThis.fetch` Custom fetch function to wrap. ```ts twoslash import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const customFetch = globalThis.fetch const account = privateKeyToAccount('0x...') const mppx = Mppx.create({ fetch: customFetch, // [!code focus] methods: [tempo({ account })], }) ``` ### methods * **Type:** `readonly Method.Client[]` Array of payment methods to use. ```ts twoslash import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0x...') const mppx = Mppx.create({ // [!code focus:start] methods: [tempo({ account })], // [!code focus:end] }) ``` ### onChallenge (optional) * **Type:** `(challenge: Challenge, helpers: { createCredential: (context?) => Promise }) => Promise` Called when a `402` challenge is received, before credential creation. Return a credential string to use it directly, or `undefined` to fall back to the default credential flow. ```ts twoslash import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0x...') const mppx = Mppx.create({ methods: [tempo({ account })], onChallenge: async (challenge, { createCredential }) => { // [!code focus:start] console.log('Challenge received:', challenge.method) return createCredential() }, // [!code focus:end] }) ``` ### paymentPreferences (optional) * **Type:** `AcceptPayment.Config` Configures which payment methods the client prefers when a server offers multiple options via `Mppx.compose`. Emits an `Accept-Payment` header on requests so the server can filter Challenges to the client's preferred methods. Accepts a definition map or a callback that receives a typed key tree. Each key is a `method/intent` string (like `'tempo/charge'`). Values are q-values from 0 to 1—higher means more preferred. Set to 0 to explicitly opt out. ```ts twoslash import { Mppx, stripe, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0x...') Mppx.create({ methods: [ tempo({ account }), stripe.charge({ createToken: async (opts) => { const res = await fetch('/api/create-spt', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(opts), }) const { spt } = await res.json() as { spt: string } return spt }, }), ], paymentPreferences: ({ tempo, stripe }) => ({ // [!code focus:start] [tempo.charge]: 1, [stripe.charge]: 0.5, [tempo.session]: 0.2, }), // [!code focus:end] }) ``` With a plain definition map: ```ts twoslash import { Mppx, stripe, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0x...') Mppx.create({ methods: [ tempo({ account }), stripe.charge({ createToken: async (opts) => { const res = await fetch('/api/create-spt', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(opts), }) const { spt } = await res.json() as { spt: string } return spt }, }), ], paymentPreferences: { // [!code focus:start] 'tempo/charge': 1, 'stripe/charge': 0.5, 'tempo/session': 0.2, }, // [!code focus:end] }) ``` ### polyfill (optional) * **Type:** `boolean` * **Default:** `true` Whether to polyfill `globalThis.fetch` with the payment-aware wrapper. ```ts twoslash import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0x...') const mppx = Mppx.create({ methods: [tempo({ account })], polyfill: false, // [!code focus] }) ``` ## Manual credential options ### acceptPayment (optional) * **Type:** `string | readonly AcceptPayment.Entry[]` Request-local method preference override for `mppx.createCredential(response, context, options)`. Use this when you manually fetch a `402` response and want to select from the returned Challenges without changing the client's default `paymentPreferences`. ### transport (optional) * **Type:** `Transport` * **Default:** `Transport.http()` Transport to use for extracting challenges and attaching credentials. ```ts twoslash import { Mppx, Transport, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0x...') const mppx = Mppx.create({ methods: [tempo({ account })], transport: Transport.mcp(), // [!code focus] }) ``` # `Mppx.restore` \[Restore the original global fetch] Restores the original `fetch` after `Mppx.create()` polyfilled it. ## Usage ```ts twoslash import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0x...') Mppx.create({ methods: [tempo({ account })], }) // ... use payment-aware fetch ... Mppx.restore() ``` ## Return type ```ts void ``` ## Parameters None. # `Fetch.from` \[Create a payment-aware fetch wrapper] Creates a scoped `fetch` wrapper that handles `402` Payment Required responses without modifying `globalThis.fetch`. ## Usage ```ts twoslash import { Fetch, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0x...') const fetch = Fetch.from({ methods: [tempo({ account })], }) const response = await fetch('https://mpp.dev/api/ping/paid') console.log(response.status) // @log: 200 ``` ### With allowed origins Use `acceptPaymentPolicy` to control where the wrapper injects `Accept-Payment`. ```ts twoslash import { Fetch, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0x...') const fetch = Fetch.from({ acceptPaymentPolicy: { origins: ['https://api.example.com', '*.paid.example.com'], }, methods: [tempo({ account })], }) ``` ## Return type ```ts type ReturnType = ( input: RequestInfo | URL, init?: RequestInit & { context?: AnyContextFor } ) => Promise ``` ## Parameters ### acceptPayment (optional) * **Type:** `AcceptPayment.Resolved` Resolved `Accept-Payment` header and preference data. Most callers use `methods` and `paymentPreferences` on `Mppx.create` instead. ### acceptPaymentPolicy (optional) * **Type:** `'always' | 'same-origin' | 'never' | { origins: readonly string[] }` * **Default:** `'always'` Controls when `Accept-Payment` is injected. Use `'same-origin'` in browsers when you only want same-origin payment discovery. Use `{ origins }` when paid APIs live on specific origins. Origin patterns support `*.` subdomain wildcards. ### eventDispatcher (optional) * **Type:** `ClientEventDispatcher` Advanced shared event dispatcher. `challenge.received` handlers run before `onChallenge`; the first non-empty Credential string skips `onChallenge`. ### fetch (optional) * **Type:** `typeof globalThis.fetch` * **Default:** `globalThis.fetch` Custom fetch function to wrap. ### methods * **Type:** `readonly Method.AnyClient[]` Array of payment methods to use for `402` responses. ### onChallenge (optional) * **Type:** `(challenge, helpers) => Promise` Called after a `402` Challenge is selected and before the wrapper retries the request. Return a Credential string to override the default credential flow. # `Fetch.polyfill` \[Install a global payment-aware fetch] Replaces `globalThis.fetch` with a payment-aware wrapper that handles `402` Payment Required responses. ## Usage ```ts twoslash import { Fetch, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0x...') Fetch.polyfill({ methods: [tempo({ account })], }) const response = await fetch('https://mpp.dev/api/ping/paid') console.log(response.status) // @log: 200 ``` ### With cross-origin payments In browsers, `Fetch.polyfill` defaults to same-origin `Accept-Payment` injection. Pass `acceptPaymentPolicy` when your paid API lives on another origin. ```ts twoslash import { Fetch, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0x...') Fetch.polyfill({ acceptPaymentPolicy: { origins: ['https://api.example.com'], }, methods: [tempo({ account })], }) ``` ## Return type ```ts type ReturnType = void ``` ## Parameters ### acceptPayment (optional) * **Type:** `AcceptPayment.Resolved` Resolved `Accept-Payment` header and preference data. ### acceptPaymentPolicy (optional) * **Type:** `'always' | 'same-origin' | 'never' | { origins: readonly string[] }` * **Default:** `'same-origin'` in browsers, `'always'` otherwise Controls when `Accept-Payment` is injected. ### eventDispatcher (optional) * **Type:** `ClientEventDispatcher` Advanced shared event dispatcher. `challenge.received` handlers run before `onChallenge`; the first non-empty Credential string skips `onChallenge`. ### fetch (optional) * **Type:** `typeof globalThis.fetch` * **Default:** `globalThis.fetch` Custom fetch function to wrap. ### methods * **Type:** `readonly Method.AnyClient[]` Array of payment methods to use for `402` responses. ### onChallenge (optional) * **Type:** `(challenge, helpers) => Promise` Called after a `402` Challenge is selected and before the wrapper retries the request. # `Fetch.restore` \[Restore global fetch] Restores the original `globalThis.fetch` after `Fetch.polyfill` or `Mppx.create` installs a payment-aware wrapper. ## Usage ```ts twoslash import { Fetch, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0x...') Fetch.polyfill({ methods: [tempo({ account })], }) Fetch.restore() ``` ## Return type ```ts type ReturnType = void ``` ## Parameters None. # `Transport.from` \[Create a custom transport] Creates a custom client-side transport. ## Usage ```ts twoslash import { Challenge } from 'mppx' import { Transport } from 'mppx/client' const http = Transport.from({ getChallenge(response) { return Challenge.fromResponse(response) }, isPaymentRequired(response) { return response.status === 402 }, name: 'http', setCredential(request, credential) { const headers = new Headers(request.headers) headers.set('Authorization', credential) return { ...request, headers } }, }) ``` ## Return type ```ts type Transport = { /** Transport name for identification. */ name: string /** Checks if a response indicates payment is required. */ isPaymentRequired: (response: response) => boolean /** Extracts the challenge from a payment-required response. */ getChallenge: (response: response) => Challenge /** Attaches a credential to a request. */ setCredential: (request: request, credential: string) => request } ``` ## Parameters ### getChallenge * **Type:** `(response: Response) => Challenge` Function that extracts the challenge from a payment-required response. ### isPaymentRequired * **Type:** `(response: Response) => boolean` Function that checks if a response indicates payment is required. ### name * **Type:** `string` Transport name for identification. ### setCredential * **Type:** `(request: Request, credential: string) => Request` Function that attaches a credential to a request. # `Transport.http` \[HTTP transport for payments] HTTP transport for client-side payment handling. ## Usage ```ts twoslash import { Mppx, Transport, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0x...') const mppx = Mppx.create({ methods: [tempo({ account })], transport: Transport.http(), // [!code focus] }) ``` ## Behavior * Detects payment required using the **`402` status code** * Extracts challenges from the **`WWW-Authenticate` header** * Sends credentials using the **`Authorization` header** ## Return type ```ts type HttpTransport = Transport ``` ## Parameters None. # `Transport.mcp` \[MCP transport for payments] MCP transport for client-side payment handling. ## Usage ```ts twoslash import { Mppx, Transport, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0x...') const mppx = Mppx.create({ methods: [tempo({ account })], transport: Transport.mcp(), // [!code focus] }) ``` ## Behavior * Detects payment required using **error code `-32042`** * Extracts challenges from **`error.data.challenges[0]`** * Sends credentials using **`_meta["org.paymentauth/credential"]`** ## Return type ```ts type McpTransport = Transport ``` ## Parameters None. # `McpClient.wrap` \[Payment-aware MCP client] Wraps an MCP SDK client with automatic payment handling. When a tool call returns a `-32042` payment required error, the wrapper creates a Credential and retries the call. ## Usage ```ts import { Client } from '@modelcontextprotocol/sdk/client' import { McpClient, tempo } from 'mppx/mcp-sdk/client' import { privateKeyToAccount } from 'viem/accounts' const client = new Client({ name: 'my-client', version: '1.0.0' }) await client.connect(transport) const mcp = McpClient.wrap(client, { methods: [tempo({ account: privateKeyToAccount('0x...') })], }) const result = await mcp.callTool({ name: 'premium_tool', arguments: {} }) // @log: { content: [...], receipt: { ... } } ``` ### With call options Pass `context` and `timeout` through the second argument to `callTool`. ```ts const result = await mcp.callTool( { name: 'premium_tool', arguments: { query: 'hello' } }, { context: { foo: 'bar' }, timeout: 30_000 }, ) ``` ## Return type `McpClient.wrap` returns an object that spreads the original client and overrides `callTool` with a payment-aware version. ```ts type McpClient = Omit & { callTool: ( params: { arguments?: Record name: string _meta?: Record }, options?: CallToolOptions, ) => Promise } ``` The `CallToolResult` type extends the SDK's return type with a `receipt` field: ```ts type CallToolResult = Awaited> & { receipt: Mcp.Receipt | undefined } ``` ## Parameters ### client * **Type:** `Pick` The MCP SDK client instance to wrap. Must have a `callTool` method—typically an instance of `Client` from `@modelcontextprotocol/sdk/client`. ### config.methods * **Type:** `readonly Method.AnyClient[]` Array of payment methods to use when handling payment Challenges. The wrapper matches Challenges from the server against installed methods by name and intent. # `evm` \[Create EVM charge Challenges] Namespace for EVM payment methods and known asset metadata. ## Usage ```ts twoslash import { Mppx, evm } from 'mppx/server' Mppx.create({ methods: [ // [!code focus:start] evm.charge({ currency: evm.assets.baseSepolia.USDC, recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', x402: { facilitator: 'https://x402.org/facilitator', }, }), // [!code focus:end] ], secretKey: process.env.MPP_SECRET_KEY ?? 'local-dev-secret', }) ``` ## Exports ### assets * **Type:** `typeof import('mppx/server').evm.assets` Known EVM asset metadata. Use `evm.assets.base.USDC` for Base mainnet and `evm.assets.baseSepolia.USDC` for Base Sepolia. Use `evm.assets.define` for custom EVM assets: ```ts twoslash import { evm } from 'mppx/server' const USDC = evm.assets.define({ address: '0x0000000000000000000000000000000000000000', decimals: 6, network: 'eip155:84532', transfer: { name: 'USD Coin', type: 'eip3009', version: '2', }, }) ``` ### chains * **Type:** `typeof import('mppx/server').evm.chains` Known EVM chain IDs. Use `evm.chains.base` for Base mainnet and `evm.chains.baseSepolia` for Base Sepolia. ### charge * **Type:** `typeof evm.charge` Creates an EVM charge payment method. See [`evm.charge`](/sdk/typescript/server/Method.evm.charge). # `Method.evm.charge` \[One-time EVM payments] Creates an EVM charge payment method for native MPP and x402 exact payment flows. ## Usage ```ts twoslash import { Mppx, evm } from 'mppx/server' const mppx = Mppx.create({ methods: [ evm.charge({ currency: evm.assets.baseSepolia.USDC, recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', x402: { facilitator: 'https://x402.org/facilitator', }, }), ], secretKey: process.env.MPP_SECRET_KEY ?? 'local-dev-secret', }) export async function handler(request: Request) { // [!code focus:start] const response = await mppx.evm.charge({ amount: '0.01', description: 'Premium API access', })(request) // [!code focus:end] if (response.status === 402) return response.challenge return response.withReceipt(Response.json({ data: '...' })) } ``` Use `x402.facilitator` for x402 exact settlement. Use `settle` when you want to verify and settle Credentials yourself. ## Return type Returns a function that accepts a `Request` and returns a response object with payment status. ```ts type ReturnType = (request: Request) => Promise< | { status: 402; challenge: Response } | { status: 200; withReceipt: (response: T) => T } > ``` ## Configuration These parameters configure the `evm.charge()` constructor. ### authorization (optional) * **Type:** `{ name: string; version: string }` EIP-3009 token domain metadata. Required for custom currency addresses and inferred for known assets. ### chainId (optional) * **Type:** `number` EVM chain ID. Required for custom currency addresses and inferred for known assets. ### currency * **Type:** `Address | KnownAsset` Token contract address or known EVM asset metadata. ```ts twoslash import { evm } from 'mppx/server' const method = evm.charge({ currency: evm.assets.baseSepolia.USDC, // [!code focus] recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', x402: { facilitator: 'https://x402.org/facilitator', }, }) ``` ### decimals (optional) * **Type:** `number` Token decimal places. Required for custom currency addresses and inferred for known assets. ### recipient * **Type:** `Address` Wallet address that receives the payment. ### settle (optional) * **Type:** `(parameters: SettleAuthorizationParameters) => Promise<{ reference: string; timestamp?: string }>` Custom settlement callback. Use this instead of `x402.facilitator` when you settle EIP-3009 authorization Credentials yourself. ### x402 (optional) * **Type:** `{ facilitator: string | Facilitator }` x402 compatibility options. Pass a facilitator URL to verify and settle x402 exact payments. ## Request parameters ### amount * **Type:** `string` Payment amount in display units. ### description (optional) * **Type:** `string` Human-readable description of the payment request. ### externalId (optional) * **Type:** `string` External correlation ID for the payment request. # `tempo` \[Register default Tempo intents] Convenience function that creates both `tempo.charge` and `tempo.session` method intents with shared configuration. Register [`tempo.subscription`](/sdk/typescript/server/Method.tempo.subscription) separately for recurring payments. ## Usage ```ts twoslash import { Mppx, tempo } from 'mppx/server' Mppx.create({ methods: [ // [!code focus:start] tempo(), // [!code focus:end] ], }) ``` This is equivalent to: ```ts twoslash import { Mppx, tempo } from 'mppx/server' Mppx.create({ methods: [ tempo.charge(), tempo.session(), ], }) ``` ## Return type ```ts type ReturnType = readonly [Method.Server, Method.Server] ``` A tuple of `[charge, session]` methods. `Mppx.create` accepts tuples in the `methods` array and flattens them automatically. ## Parameters Accepts the union of [`tempo.charge`](/sdk/typescript/server/Method.tempo.charge) and [`tempo.session`](/sdk/typescript/server/Method.tempo.session) parameters. The most common are listed below. ### currency (optional) * **Type:** `Address` Default TIP-20 token address for the payment currency. ### decimals (optional) * **Type:** `number` * **Default:** `6` Decimal places for amount parsing. ### feePayer (optional) * **Type:** `Account | string | true` Account or URL for sponsoring transaction fees. Pass a viem `Account` to co-sign locally, a URL string to delegate to a remote [fee payer service](https://docs.tempo.xyz/sdk/typescript/server/handler.feePayer), or `true` when the `account` parameter doubles as the fee payer. ### getClient (optional) * **Type:** `(parameters: { chainId?: number }) => MaybePromise` Function that returns a viem client for the given chain ID. Overrides the default RPC configuration. ### recipient (optional) * **Type:** `Address` Default recipient address for payments. ### testnet (optional) * **Type:** `boolean` Testnet mode. Defaults the chain ID to `42431` (Tempo testnet). # `Method.tempo.charge` \[One-time stablecoin payments] The `charge` intent for the Tempo payment method. Requests a one-time payment from the client. Non-zero charges verify on-chain transfers. Zero-amount charges verify a `proof` payload signed by the client's identity key and return a Receipt without broadcasting a transaction. ## Usage ```ts twoslash import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo.charge()] }) export async function handler(request: Request) { // [!code focus:start] const response = await mppx.charge({ amount: '0.1', currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })(request) // [!code focus:end] if (response.status === 402) return response.challenge return response.withReceipt(Response.json({ data: '...' })) } ``` ### With expiry Set a custom expiration time for the charge using the `expires` option. ```ts twoslash import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo.charge()] }) // ---cut--- import { Expires } from 'mppx' export async function handler(request: Request) { const response = await mppx.charge({ amount: '0.1', currency: '0x20c0000000000000000000000000000000000000', expires: Expires.minutes(10), // [!code focus] recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })(request) if (response.status === 402) return response.challenge return response.withReceipt(Response.json({ data: '...' })) } ``` ### With description Add a human-readable description for the payment request. ```ts twoslash import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo.charge()] }) // ---cut--- export async function handler(request: Request) { const response = await mppx.charge({ amount: '0.1', currency: '0x20c0000000000000000000000000000000000000', description: 'API access for /resource', // [!code focus] recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })(request) if (response.status === 402) return response.challenge return response.withReceipt(Response.json({ data: '...' })) } ``` ### With a custom fee payer policy Override the local fee-sponsor limits when you co-sign charge transactions. ```ts twoslash import { Mppx, tempo } from 'mppx/server' import { privateKeyToAccount } from 'viem/accounts' const mppx = Mppx.create({ methods: [ tempo.charge({ feePayer: privateKeyToAccount( '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', ), feePayerPolicy: { maxPriorityFeePerGas: 50_000_000_000n, maxTotalFee: 100_000_000_000_000_000n, }, }), ], }) ``` ### With replay protection for zero-dollar auth Pass `store` when you want zero-dollar proof Credentials to be single-use. ```ts twoslash import { Mppx, Store, tempo } from 'mppx/server' const replayStore = Store.memory() const mppx = Mppx.create({ methods: [ tempo.charge({ store: replayStore, }), ], }) export async function handler(request: Request) { const response = await mppx.charge({ amount: '0', currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', })(request) if (response.status === 402) return response.challenge return response.withReceipt(Response.json({ data: '...' })) } ``` ## Return type Returns a function that accepts a `Request` and returns a response object with payment status. ```ts type ReturnType = (request: Request) => Promise< | { status: 402; challenge: Response } | { status: 200; withReceipt: (response: T) => T } > ``` ## Configuration These parameters configure the `tempo.charge()` constructor. ### decimals (optional) * **Type:** `number` * **Default:** `6` Decimal places for amount parsing. ### externalId (optional) * **Type:** `string` External identifier for the payment. ### feePayer (optional) * **Type:** `Account | string | true` Account or URL for sponsoring transaction fees. Pass a viem `Account` to co-sign locally, a URL string to delegate to a remote [fee payer service](https://docs.tempo.xyz/sdk/typescript/server/handler.feePayer), or `true` when the `account` parameter doubles as the fee payer. This setting only applies to non-zero charges. Zero-amount proof flows do not create a transaction. ### feePayerPolicy (optional) * **Type:** `Partial<{ maxFeePerGas: bigint; maxGas: bigint; maxPriorityFeePerGas: bigint; maxTotalFee: bigint; maxValidityWindowSeconds: number }>` Override the local fee-sponsor policy used when the server co-signs Tempo charge transactions. Remote fee payer services enforce their own policy. `mppx` resolves defaults per chain automatically. On mainnet (`4217`), the defaults are `maxFeePerGas: 100_000_000_000n`, `maxGas: 2_000_000n`, `maxPriorityFeePerGas: 10_000_000_000n`, `maxTotalFee: 50_000_000_000_000_000n`, and `maxValidityWindowSeconds: 900`. On Moderato (`42431`), `maxPriorityFeePerGas` increases to `50_000_000_000n` and the other limits stay the same. If you raise `maxFeePerGas` or `maxGas`, you may also need to raise `maxTotalFee` so the combined fee budget stays within policy. ```ts twoslash import { Mppx, tempo } from 'mppx/server' import { privateKeyToAccount } from 'viem/accounts' const mppx = Mppx.create({ methods: [ tempo.charge({ feePayer: privateKeyToAccount( '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', ), feePayerPolicy: { maxPriorityFeePerGas: 50_000_000_000n, maxTotalFee: 100_000_000_000_000_000n, }, }), ], }) ``` ### getClient (optional) * **Type:** `(parameters: { chainId?: number }) => MaybePromise` Function that returns a viem client for the given chain ID. Overrides the default RPC configuration. ### memo (optional) * **Type:** `string` On-chain memo for the transaction. ### store (optional) * **Type:** `Store.AtomicStore` Pass a store when you want replay protection for charge Credentials. A `Store` provides async key-value operations (`get`, `put`, `delete`). An `AtomicStore` extends `Store` with an atomic `update(key, fn)` method for safe concurrent replay checks. For non-zero charges, `mppx` falls back to an in-memory store when you omit this parameter. For zero-dollar proof auth, replay prevention is disabled unless you pass a store. Use `Store.memory()` for local development, tests, or a single long-lived server process. For multi-instance deployments, use `Store.redis()`, `Store.upstash()`, or `Store.cloudflare()`. All built-in factories return `AtomicStore` — for custom backends, provide an `update` function alongside `get`, `put`, and `delete`. ### testnet (optional) * **Type:** `boolean` Testnet mode. Defaults the chain ID to `42431` (Tempo testnet). ### waitForConfirmation (optional) * **Type:** `boolean` * **Default:** `true` Whether to wait for the charge transaction to confirm on-chain before responding. When `false`, the transaction is simulated via `eth_estimateGas` and broadcast without waiting for inclusion. The Receipt optimistically reports `status: 'success'` based on simulation alone. This option applies only to non-zero charges. Zero-amount proof flows return immediately after signature verification. ```ts twoslash import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo.charge({ waitForConfirmation: false, // [!code focus] })], }) ``` ## Request parameters ### amount * **Type:** `string` Payment amount in human-readable units. For example, `'0.1'` represents $0.10 USD. Set `'0'` to require identity-only zero-dollar auth. In that case, the client submits a `proof` payload instead of a transaction or hash. By default, zero-dollar proof Credentials remain reusable until the Challenge expires. Pass `store` to treat proofs as single-use across the scope of that store. ### currency * **Type:** `string` TIP-20 token address for the payment currency. ### description (optional) * **Type:** `string` Human-readable description of the payment request. ### expires (optional) * **Type:** `string` * **Default:** 5 minutes from now ISO 8601 timestamp for when the payment challenge expires. ### meta (optional) * **Type:** `Record` Server-defined correlation data. `mppx` serializes it as the base64url-encoded `opaque` auth-param on the Challenge, and clients echo that same string back in the Credential. ### recipient * **Type:** `string` Address to receive the payment. ### scope (optional) * **Type:** `string` Route or resource scope bound into the Challenge metadata. Use this to prevent a Credential issued for one route from being replayed against another route with the same payment terms. ### splits (optional) * **Type:** `Array<{ amount: string; memo?: string; recipient: string }>` Split the charge across additional recipients. Each entry specifies an `amount` (in human-readable units) and a `recipient` address. The primary `recipient` receives `amount` minus the sum of all split amounts. | Constraint | Value | |---|---| | Array length | 1–10 | | Each split amount | Must be > 0 | | Sum of splits | Must be strictly less than `amount` | | Split memo | Optional, 32-byte hex hash | ```ts twoslash import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo.charge()] }) // ---cut--- export async function handler(request: Request) { const response = await mppx.charge({ amount: '1.00', currency: '0x20c0000000000000000000000000000000000000', // pathUSD recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', // seller splits: [ // [!code focus] { amount: '0.10', recipient: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8' }, // platform fee // [!code focus] ], // [!code focus] })(request) if (response.status === 402) return response.challenge return response.withReceipt(Response.json({ data: '...' })) } ``` # `Method.tempo.session` \[Low-cost high-throughput payments] Creates a Tempo payment session method for server-side voucher verification and channel management. ## Usage ```ts twoslash import { Mppx, tempo } from 'mppx/server' import { privateKeyToAccount } from 'viem/accounts' const mppx = Mppx.create({ // [!code focus:start] methods: [ tempo.session({ account: privateKeyToAccount('0x...'), currency: '0x20c0000000000000000000000000000000000000', }), ], // [!code focus:end] }) ``` ### With charge and session Register both intents on a single server. ```ts twoslash import { Mppx, tempo } from 'mppx/server' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0x...') const mppx = Mppx.create({ methods: [ tempo.charge({ account }), tempo.session({ account }), ], }) ``` ## Return type ```ts import type { Method } from 'mppx' type ReturnType = Method.Server ``` ## Parameters ### account * **Type:** `Account` Account used to close and settle payment channels on-chain. The account address is also used as the default recipient. Without this, channel close and settlement fails. ### amount (optional) * **Type:** `string` Default amount to charge per unit. ### channelStateTtl (optional) * **Type:** `number` * **Default:** `60000` TTL in milliseconds for cached on-chain channel state. After this duration, the server re-queries on-chain state during voucher handling to detect forced close requests. ### currency (optional) * **Type:** `Address` Default TIP-20 token address for the payment currency. ### decimals (optional) * **Type:** `number` * **Default:** `6` Decimal places for amount parsing. ### escrowContract (optional) * **Type:** `Address` Escrow contract address for the payment channel. Defaults to the canonical escrow contract for the given chain ID. ### feePayer (optional) * **Type:** `Account | string | true` Account or URL for sponsoring open and top-up transaction fees. Pass a viem `Account` to co-sign locally, a URL string to delegate to a remote [fee payer service](https://docs.tempo.xyz/sdk/typescript/server/handler.feePayer), or `true` when the `account` parameter doubles as the fee payer. ### feePayerPolicy (optional) * **Type:** `Partial<{ maxFeePerGas: bigint; maxGas: bigint; maxPriorityFeePerGas: bigint; maxTotalFee: bigint; maxValidityWindowSeconds: number }>` Override the local fee-sponsor policy used when the server co-signs session open and top-up transactions. Remote fee payer services enforce their own policy. ### getClient (optional) * **Type:** `(parameters: { chainId: number }) => MaybePromise` Function that returns a viem client for the given chain ID. ### minVoucherDelta (optional) * **Type:** `string` * **Default:** `"0"` Minimum voucher delta to accept as a numeric string. Rejects vouchers where the increment over the previous highest voucher is below this threshold. ### recipient (optional) * **Type:** `Address` Default recipient address for payments. ### sse (optional) * **Type:** `boolean | { poll?: boolean; pollingInterval?: number }` Enable SSE streaming. Pass `true` to enable with defaults, or an options object to configure SSE (for example, `{ poll: true }` for Cloudflare Workers compatibility). ### store (optional) * **Type:** `Store.AtomicStore` * **Default:** `Store.memory()` Store backend for persisting channel and session state. A `Store` provides async key-value operations (`get`, `put`, `delete`). An `AtomicStore` extends `Store` with an atomic `update(key, fn)` method for safe concurrent read-modify-write — required for session channel state where multiple instances may process vouchers concurrently. ```ts twoslash import { Store, tempo } from 'mppx/server' const method = tempo.session({ store: Store.memory(), // [!code focus] }) ``` Use `Store.memory()` for local development and tests. For multi-instance deployments, use `Store.redis()`, `Store.upstash()`, or `Store.cloudflare()`. All built-in factories return `AtomicStore` — for custom backends, provide an `update` function alongside `get`, `put`, and `delete`. ### suggestedDeposit (optional) * **Type:** `string` Suggested deposit amount communicated to clients in the challenge. ### testnet (optional) * **Type:** `boolean` Testnet mode. ### unitType (optional) * **Type:** `string` Unit type label (for example, "token", "byte", "request"). ### waitForConfirmation (optional) * **Type:** `boolean` * **Default:** `true` Whether to wait for open transactions to confirm on-chain before responding. When `false`, the transaction is simulated and broadcast without waiting for inclusion. ## Related ### `tempo.Ws.serve` WebSocket transport for session streams. Bridges a WebSocket connection to the same session payment flow, with in-band authorization, voucher top-ups, and cooperative close over a single socket. See [`Ws.serve`](/sdk/typescript/server/Ws.serve) for full documentation. ### `tempo.settle` One-shot settlement: reads the highest voucher from the store and submits it on-chain. Use this for periodic batch settlement outside the request handler. ```ts import { tempo } from 'mppx/server' import { Store } from 'mppx/server' import { createClient, http } from 'viem' import { tempo as tempoChain } from 'viem/chains' const client = createClient({ chain: tempoChain, transport: http() }) const store = Store.memory() // Settle a specific channel const txHash = await tempo.settle(store, client, '0xchannelId...') ``` ### Parameters * **store** — `ChannelStore` — The channel store instance. * **client** — `Client` — A viem client for on-chain interaction. * **channelId** — `Hex` — The channel to settle. * **options.escrowContract** (optional) — `Address` — Escrow contract override. * **options.feePayer** (optional) — `Account | string` — Fee payer account or relay URL. ### Return type * **Type:** `Hex` — The settlement transaction hash. # `Method.tempo.subscription` \[Recurring stablecoin payments] Creates a Tempo subscription method for server-side activation, access reuse, and renewal. ## Usage ```ts twoslash import { Mppx, Store, tempo } from 'mppx/server' const store = Store.memory() const mppx = Mppx.create({ methods: [ // [!code focus:start] tempo.subscription({ amount: '1.00', currency: '0x20c0000000000000000000000000000000000000', periodCount: '1', periodUnit: 'week', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', resolve: async ({ input }) => { const userId = input.headers.get('X-User-Id') return userId ? { key: `user:${userId}:plan:pro` } : null }, store, subscriptionExpires: new Date('2027-01-01T00:00:00.000Z'), }), // [!code focus:end] ], }) export async function handler(request: Request) { const result = await mppx.tempo.subscription({})(request) if (result.status === 402) return result.challenge return result.withReceipt(Response.json({ plan: 'pro' })) } ``` ### With background renewals Use a durable store and call [`tempo.renewSubscription`](/sdk/typescript/server/Method.tempo.renewSubscription) from a worker. ```ts twoslash import { Mppx, Store, tempo } from 'mppx/server' const store = Store.memory() const mppx = Mppx.create({ methods: [ tempo.subscription({ amount: '1.00', currency: '0x20c0000000000000000000000000000000000000', periodCount: '1', periodUnit: 'week', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', resolve: async ({ input }) => { const userId = input.headers.get('X-User-Id') return userId ? { key: `user:${userId}:plan:pro` } : null }, store, subscriptionExpires: new Date('2027-01-01T00:00:00.000Z'), }), ], }) await tempo.renewSubscription({ store, subscriptionId: 'sub_abc123', }) ``` ### With custom activation Pass `activate` when your app owns first-period settlement and subscription record creation. ```ts twoslash import { Mppx, Store, tempo } from 'mppx/server' const store = Store.memory() const mppx = Mppx.create({ methods: [ tempo.subscription({ activate: async ({ request, resolved }) => { const timestamp = new Date().toISOString() return { receipt: { method: 'tempo', reference: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', status: 'success', subscriptionId: 'sub_abc123', timestamp, }, subscription: { amount: request.amount, billingAnchor: timestamp, currency: request.currency, lastChargedPeriod: 0, lookupKey: resolved.key, periodCount: request.periodCount, periodUnit: request.periodUnit, recipient: request.recipient, reference: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', subscriptionExpires: request.subscriptionExpires, subscriptionId: 'sub_abc123', timestamp, }, } }, amount: '1.00', currency: '0x20c0000000000000000000000000000000000000', periodCount: '1', periodUnit: 'week', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', resolve: async () => ({ key: 'user:123:plan:pro' }), store, subscriptionExpires: new Date('2027-01-01T00:00:00.000Z'), }), ], }) ``` ### With cancellation Mark the stored subscription record with `canceledAt`. `mppx` no longer reuses or renews that record, and the next protected request returns a new subscription Challenge. If the client also revokes the Tempo access key, still update the server record so access and billing state stay aligned. ```ts twoslash import { Store } from 'mppx/server' import { Subscription } from 'mppx/tempo' const store = Store.memory() const subscriptions = Subscription.fromStore(store) export async function cancelSubscription(userId: string) { const subscription = await subscriptions.getByKey(`user:${userId}:plan:pro`) if (!subscription) return false await subscriptions.put({ ...subscription, canceledAt: new Date().toISOString(), }) return true } ``` ## Return type ```ts import type { Method } from 'mppx' type ReturnType = Method.Server ``` ## Configuration These parameters configure `tempo.subscription()`. ### accessKey (optional) * **Type:** `(parameters: { input: Request; request: SubscriptionRequest; resolved: SubscriptionLookup }) => MaybePromise` Returns the access key to include in the Challenge. Omit this for the recommended path: `mppx` generates and stores a server-owned access key per resolved subscription key. ### account (optional) * **Type:** `Account` Account used as the default `recipient` and local transaction signer. ### activationTimeoutMs (optional) * **Type:** `number` * **Default:** `900000` Milliseconds before an in-flight activation lock can be replaced. ### amount (optional) * **Type:** `string` Default amount to charge per period. ### chainId (optional) * **Type:** `number` Tempo chain ID. Use `4217` for mainnet and `42431` for testnet. ### currency (optional) * **Type:** `Address` Default TIP-20 token address. ### decimals (optional) * **Type:** `number` * **Default:** `6` Decimal places for amount parsing. ### description (optional) * **Type:** `string` Human-readable subscription description. ### externalId (optional) * **Type:** `string` Application-defined identifier for reconciliation. ### getClient (optional) * **Type:** `(parameters: { chainId?: number }) => MaybePromise` Function that returns a viem client for the given Tempo chain ID. ### hooks (optional) * **Type:** `{ activated?: (parameters) => MaybePromise; renewed?: (parameters) => MaybePromise }` Callbacks that run after activation or renewal commits. ### periodCount (optional) * **Type:** `string` Number of period units per billing period. ### periodUnit (optional) * **Type:** `'day' | 'week'` Billing period unit. ### recipient (optional) * **Type:** `Address` Address that receives subscription payments. ### renew (optional) * **Type:** `(parameters: { inFlightReference: string; periodIndex: number; subscription: SubscriptionRecord }) => Promise` Custom renewal hook. Use this when your app owns period settlement and subscription record updates. ### renewalTimeoutMs (optional) * **Type:** `number` * **Default:** `900000` Milliseconds before an in-flight renewal lock can be replaced. ### resolve * **Type:** `(parameters: { input: Request; request: SubscriptionRequest }) => MaybePromise` Maps a request to a subscription lookup key. Return the same key for every route covered by the same subscription. ### store (optional) * **Type:** `Store.AtomicStore>` * **Default:** `Store.memory()` Atomic store for access keys, activation locks, renewal locks, and subscription records. Use [`Subscription.fromStore`](/payment-methods/tempo/subscription#cancellation) from `mppx/tempo` when you need to update subscription records directly, such as marking `canceledAt`. ### subscriptionExpires (optional) * **Type:** `string | Date` Maximum authorization expiry. The client authorization cannot outlive this timestamp. ### testnet (optional) * **Type:** `boolean` Uses Tempo testnet defaults. Testnet chain ID is `42431`. ### waitForConfirmation (optional) * **Type:** `boolean` * **Default:** `true` Whether to wait for activation and automatic renewal transfers to confirm before returning a Receipt. ## Request parameters These parameters configure each `mppx.tempo.subscription()` call. ### accessKey (optional) * **Type:** `SubscriptionAccessKey` Access key to authorize. Most apps let the server method generate this value. ### amount (optional) * **Type:** `string` Amount to charge per billing period. ### chainId (optional) * **Type:** `number` Tempo chain ID for the subscription. ### currency (optional) * **Type:** `Address` TIP-20 token address for payments. ### decimals (optional) * **Type:** `number` Decimal places for amount parsing. ### description (optional) * **Type:** `string` Human-readable subscription description. ### expires (optional) * **Type:** `string` Challenge expiry timestamp. ### externalId (optional) * **Type:** `string` Application-defined identifier for reconciliation. ### meta (optional) * **Type:** `Record` Server-defined correlation data serialized as the Challenge `opaque` auth-param. ### periodCount (optional) * **Type:** `string` Number of period units per billing period. ### periodUnit (optional) * **Type:** `'day' | 'week'` Billing period unit. ### recipient (optional) * **Type:** `Address` Address that receives payments. ### scope (optional) * **Type:** `string` Route or resource scope bound into the Challenge metadata. ### subscriptionExpires (optional) * **Type:** `string | Date` Maximum authorization expiry. ## Related ### `tempo.renewSubscription` Renews an overdue subscription outside the request path. See [`tempo.renewSubscription`](/sdk/typescript/server/Method.tempo.renewSubscription). # `stripe` \[Register all Stripe intents] Convenience function that creates the Stripe `charge` method intent. ## Usage ```ts twoslash import Stripe from 'stripe' import { Mppx, stripe } from 'mppx/server' const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!) Mppx.create({ methods: [ // [!code focus:start] stripe({ client: stripeClient, networkId: 'internal', paymentMethodTypes: ['card'], }), // [!code focus:end] ], }) ``` ## Return type ```ts import type { Method } from 'mppx' type ReturnType = Method.Server ``` ## Parameters See [`stripe.charge`](/sdk/typescript/server/Method.stripe.charge) for the full parameter list. # `Method.stripe.charge` \[One-time payments via Shared Payment Tokens] The `charge` intent for the Stripe payment method. Requests a one-time payment using [Shared Payment Tokens (SPTs)](https://docs.stripe.com/agentic-commerce/concepts/shared-payment-tokens). ## Usage ```ts twoslash import Stripe from 'stripe' import { Mppx, stripe } from 'mppx/server' const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!) const mppx = Mppx.create({ methods: [stripe.charge({ client: stripeClient, networkId: 'internal', paymentMethodTypes: ['card'], })], }) export async function handler(request: Request) { // [!code focus:start] const response = await mppx.charge({ amount: '1', currency: 'usd', decimals: 2, description: 'Premium API access', })(request) // [!code focus:end] if (response.status === 402) return response.challenge return response.withReceipt(Response.json({ data: '...' })) } ``` ## Return type Returns a function that accepts a `Request` and returns a response object with payment status. ```ts type ReturnType = (request: Request) => Promise< | { status: 402; challenge: Response } | { status: 200; withReceipt: (response: T) => T } > ``` ## Configuration These parameters configure the `stripe.charge()` constructor. You must provide either `client` or `secretKey`. ### client * **Type:** `StripeClient` Pre-configured Stripe SDK instance. Any object matching the duck-typed `StripeClient` shape works. Using `client` is recommended—it lets you configure retries, API version, and other options. ```ts twoslash import Stripe from 'stripe' import { stripe } from 'mppx/server' const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!) const method = stripe.charge({ client: stripeClient, // [!code focus] networkId: 'internal', paymentMethodTypes: ['card'], }) ``` ### secretKey * **Type:** `string` Stripe secret API key. When provided instead of `client`, mppx makes raw API calls to Stripe. ```ts twoslash import { stripe } from 'mppx/server' const method = stripe.charge({ secretKey: process.env.STRIPE_SECRET_KEY!, // [!code focus] networkId: 'internal', paymentMethodTypes: ['card'], }) ``` ### networkId * **Type:** `string` Stripe [Business Network](https://docs.stripe.com/get-started/account/profile) profile ID. ### paymentMethodTypes * **Type:** `string[]` Allowed Stripe payment method types (for example, `['card']`, `['card', 'link']`). ### metadata (optional) * **Type:** `Record` Key-value pairs forwarded to Stripe. Appears in the Challenge and attaches to the Stripe `PaymentIntent`. ## Request parameters ### amount * **Type:** `string` Payment amount in human-readable units. ### currency * **Type:** `string` ISO currency code (for example, `'usd'`). ### decimals * **Type:** `number` Number of decimal places in the amount (for example, `2` for cents). ### description (optional) * **Type:** `string` Human-readable description of the payment request. # `Mppx.compose` \[Present multiple payment options] Combines multiple method handlers into a single route handler that presents all methods to the client via multiple `WWW-Authenticate` headers. ## Usage Present both stablecoin and card payment options for a single endpoint. The client picks whichever method it supports. ```ts twoslash import { Mppx, stripe, tempo } from 'mppx/server' const pathUSD = '0x20c0000000000000000000000000000000000000' const USDCe = '0x20C000000000000000000000b9537d11c60E8b50' const recipient = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' const charge = tempo.charge({ recipient }) const card = stripe.charge({ networkId: 'acct_1234', paymentMethodTypes: ['card'], secretKey: 'sk_live_...', }) const mppx = Mppx.create({ methods: [charge, card] }) export async function handler(request: Request) { const result = await mppx.compose( [charge, { amount: '1', currency: pathUSD }], [charge, { amount: '1', currency: USDCe }], [card, { amount: '1', currency: 'usd' }], )(request) if (result.status === 402) return result.challenge return result.withReceipt(Response.json({ data: '...' })) } ``` ## Behavior * **No Credential present:** Calls all handlers and merges their `402` challenges into a single response with multiple `WWW-Authenticate` headers. * **`Accept-Payment` present:** Ranks and filters the merged Challenges by the client's supported `method/intent` entries. Entries with `q=0` are excluded. If the header is invalid or filters out every Challenge, all Challenges are returned. * **Credential present:** Dispatches to the handler matching the Credential's `method` and `intent`. ## Return type ```ts type ReturnType = (input: Request) => Promise< | { status: 402; challenge: Response } | { status: 200; withReceipt: (response: T) => T } > ``` ## Parameters ### ...entries * **Type:** `readonly [Method.Server | string, Options][]` Each entry is a tuple of a method reference (or string key like `"tempo/charge"`) and the request options for that method. Requires at least one entry. # `Mppx.create` \[Create a server-side payment handler] Creates a server-side payment handler from a method. ## Usage ```ts twoslash import { Mppx, tempo } from 'mppx/server' const payment = Mppx.create({ methods: [tempo()], }) ``` ### With custom transport Use a custom transport for non-HTTP environments like MCP servers. ```ts twoslash import { Mppx, tempo, Transport } from 'mppx/server' const payment = Mppx.create({ methods: [tempo()], transport: Transport.mcpSdk(), }) ``` ### With payment hooks Register hooks on the returned `payment` instance to observe Challenges, successful payments, and failures. ```ts twoslash import { Mppx, tempo } from 'mppx/server' const payment = Mppx.create({ methods: [tempo.charge()], }) payment.onChallengeCreated(({ challenge, request }) => { console.log('challenge created:', challenge.id, request.amount) }) payment.onPaymentFailed(({ challenge, error }) => { console.error('payment failed:', challenge.id, error.name) }) payment.onPaymentSuccess(({ receipt, request }) => { console.log('payment success:', receipt.reference, request.amount) }) ``` ## Return type ```ts import type { Method } from 'mppx' import type { Mppx, Transport } from 'mppx/server' type ReturnType = Mppx<[Method.Server], Transport.Http> ``` The returned object includes the method's intent functions (for example, `charge`), `challenge`, `compose`, payment hooks, and `verifyCredential`. ## Parameters ### methods * **Type:** `readonly Method.Server[]` Array of payment methods (for example, `[tempo()]`). ```ts twoslash import { Mppx, tempo } from 'mppx/server' const payment = Mppx.create({ // [!code focus:start] methods: [tempo()], // [!code focus:end] }) ``` ### realm (optional) * **Type:** `string` * **Default:** Auto-detected from environment variables (`MPP_REALM`, `FLY_APP_NAME`, `HEROKU_APP_NAME`, `HOST`, `HOSTNAME`, `RAILWAY_PUBLIC_DOMAIN`, `RENDER_EXTERNAL_HOSTNAME`, `VERCEL_URL`, `WEBSITE_HOSTNAME`), falling back to `"MPP Payment"`. Server realm (for example, hostname). Auto-detected from common platform environment variables. Set explicitly to override. ```ts twoslash import { Mppx, tempo } from 'mppx/server' const payment = Mppx.create({ methods: [tempo()], realm: 'mpp.dev', // [!code focus] }) ``` ### secretKey (optional) * **Type:** `string` * **Default:** Auto-detected from `MPP_SECRET_KEY` environment variable. Throws if neither provided nor set. Secret key for HMAC-bound challenge IDs. Enables stateless verification—the server verifies that a Challenge was issued by itself without storing state. Treat it as root-of-trust material: store it in your secret manager, keep it server-side, never log it, and rotate it immediately if it is exposed. See [Security](/advanced/security). ```ts twoslash import { Mppx, tempo } from 'mppx/server' const payment = Mppx.create({ methods: [tempo()], secretKey: process.env.MPP_SECRET_KEY!, // [!code focus] }) ``` ### transport (optional) * **Type:** `Transport` * **Default:** `Transport.http()` Transport to use for handling payment requests. ```ts twoslash import { Mppx, tempo, Transport } from 'mppx/server' const payment = Mppx.create({ methods: [tempo()], transport: Transport.mcp(), // [!code focus] }) ``` # `Mppx.toNodeListener` \[Adapt payments for Node.js HTTP] Wraps a payment handler to create a Node.js HTTP listener. ## Usage ```ts twoslash import * as http from 'node:http' import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo()], }) http.createServer(async (req, res) => { const result = await Mppx.toNodeListener( mppx.charge({ amount: '0.1', currency: '0x...', recipient: '0x...', }), )(req, res) if (result.status === 402) return res.end('OK') }) ``` ## Behavior * **On `402`:** Writes the Challenge response headers and body, then ends the connection. * **On `200`:** Sets the `Payment-Receipt` header; the caller writes the response body. ## Return type ```ts type ReturnType = ( req: IncomingMessage, res: ServerResponse, ) => Promise> ``` ## Parameters ### handler * **Type:** `(input: Request) => Promise>` The payment handler function returned by calling an intent on the payment object. ```ts twoslash import * as http from 'node:http' import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo()], }) http.createServer(async (req, res) => { const result = await Mppx.toNodeListener( // [!code focus:start] mppx.charge({ amount: '0.1', currency: '0x...', recipient: '0x...', }), // [!code focus:end] )(req, res) if (result.status === 402) return res.end('OK') }) ``` # `mppx.verifyCredential` \[Verify a Credential directly] Verifies a serialized or parsed Credential on an `Mppx.create` instance. ## Usage Use `verifyCredential` when you receive a Credential through a custom transport or job boundary and need the same method verification as an `mppx` route. ```ts twoslash import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo.charge()], }) const receipt = await mppx.verifyCredential('Payment credential="..."', { request: { amount: '0.10', currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', }, scope: 'GET /v1/report', }) console.log(receipt.status) // @log: success ``` ## Return type ```ts type ReturnType = Promise ``` ## Parameters ### capturedRequest (optional) * **Type:** `Method.CapturedRequest` Authoritative request snapshot used by method verification hooks. ### credential * **Type:** `string | Credential` Serialized `Authorization` header value or parsed Credential object. ### meta (optional) * **Type:** `Record` Expected Challenge metadata. `mppx` compares this with the metadata echoed by the Credential. ### realm (optional) * **Type:** `string` Expected Challenge realm. ### request (optional) * **Type:** `Record` Expected method request parameters. Pass the same route options used to issue the Challenge. ### scope (optional) * **Type:** `string` Expected route or resource scope. Use this when the original Challenge was issued with `scope`. # `Transport.from` \[Create a custom transport] Creates a custom server-side transport. ## Usage ```ts twoslash import { Challenge, Credential, Receipt } from 'mppx' import { Transport } from 'mppx/server' const http = Transport.from({ name: 'http', getCredential(request) { const header = request.headers.get('Authorization') if (!header) return null const payment = Credential.extractPaymentScheme(header) if (!payment) return null return Credential.deserialize(payment) }, respondChallenge({ challenge, error }) { const headers: Record = { 'WWW-Authenticate': Challenge.serialize(challenge), 'Cache-Control': 'no-store', } let body: string | null = null if (error) { headers['Content-Type'] = 'application/problem+json' body = JSON.stringify(error.toProblemDetails(challenge.id)) } return new Response(body, { status: 402, headers }) }, respondReceipt({ receipt, response }) { const headers = new Headers(response.headers) headers.set('Payment-Receipt', Receipt.serialize(receipt)) return new Response(response.body, { status: response.status, statusText: response.statusText, headers, }) }, }) ``` ## Return type ```ts type ReturnType = Transport ``` ## Parameters ### getCredential * **Type:** `(input: Input) => Credential | null` Extracts Credential from the transport input. Returns `null` if no Credential was provided, or throws if malformed. ### name * **Type:** `string` Transport name for identification. ### respondChallenge * **Type:** `(options: { challenge: Challenge; error?: PaymentError; input: Input }) => ChallengeOutput | Promise` Creates a transport response for a payment challenge. ### respondReceipt * **Type:** `(options: { challengeId: string; receipt: Receipt; response: ReceiptOutput }) => ReceiptOutput` Attaches a receipt to a successful response. # `Transport.http` \[HTTP server-side transport] HTTP transport for server-side payment handling. ## Usage ```ts twoslash import { Mppx, tempo, Transport } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo()], transport: Transport.http(), }) ``` ## Behavior * Reads credentials from the `Authorization` header * Issues challenges in the `WWW-Authenticate` header with `402` status * Attaches receipts in the `Payment-Receipt` header ## Return type ```ts type ReturnType = Transport ``` # `Transport.mcp` \[Raw JSON-RPC MCP transport] MCP transport for server-side payment handling with raw JSON-RPC. ## Usage ```ts twoslash import { Mppx, tempo, Transport } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo()], transport: Transport.mcp(), }) ``` ## Behavior * Reads credentials from `_meta["org.paymentauth/credential"]` * Issues challenges in a JSON-RPC error with code `-32042`/`-32043` * Attaches receipts in `_meta["org.paymentauth/receipt"]` Use this transport when handling raw JSON-RPC messages directly. For use with [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk), use [`Transport.mcpSdk()`](/sdk/typescript/server/Transport.mcpSdk) instead. ## Return type ```ts import type { Mcp } from 'mppx' type ReturnType = Transport ``` # `Transport.mcpSdk` \[MCP SDK server-side transport] MCP SDK transport for server-side payment handling with [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk). ## Usage ```ts import { McpServer } from '@modelcontextprotocol/sdk/server/mcp' import { Mppx, tempo, Transport } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo()], transport: Transport.mcpSdk(), }) const server = new McpServer({ name: 'example', version: '1.0.0' }) server.registerTool('premium', { description: 'A premium tool' }, async (extra) => { const result = await mppx.charge({ amount: '0.1', currency: '0x...', recipient: '0x...', })(extra) if (result.status === 402) throw result.challenge return result.withReceipt({ content: [{ type: 'text', text: 'Success!' }] }) }) ``` ## Behavior * Reads credentials from `_meta["org.paymentauth/credential"]` * Issues challenges as `McpError` with code `-32042` and challenge in `error.data` * Attaches receipts in `_meta["org.paymentauth/receipt"]` on tool results ## Return type ```ts import type { CallToolResult, McpError } from '@modelcontextprotocol/sdk/types.js' type ReturnType = Transport ``` Where `Extra` is the MCP SDK tool handler "extra" parameter compatible with [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk) `RequestHandlerExtra`. # `tempo.Ws.serve` \[WebSocket session payments] Bridges a WebSocket connection to a Tempo session payment flow. Credential verification is performed by routing each in-band authorization frame through `route` as a synthetic POST request that carries only the Authorization header. The synthetic request does not include cookies, bodies, query parameters, or other headers from the original WebSocket upgrade request. ## Usage ```ts twoslash import { Mppx, Store, tempo } from 'mppx/server' import { Session } from 'mppx/tempo' import { privateKeyToAccount } from 'viem/accounts' const words = ['hello', 'world'] const mppx = Mppx.create({ methods: [ tempo.session({ account: privateKeyToAccount('0x...'), }), ], }) declare const socket: Session.Ws.Socket // In your WebSocket handler: tempo.Ws.serve({ generate: async function* (stream) { for (const word of words) { await stream.charge() yield word } }, route: (request) => mppx.session({ amount: '0.001', currency: 'pathUSD', unitType: 'word' })(request), socket, store: Store.memory(), url: 'wss://api.example.com/stream', }) ``` ## Return type ```ts type ReturnType = Promise ``` Resolves when the WebSocket session completes or the connection closes. ## Parameters ### amount (optional) * **Type:** `string` Expected per-tick amount. When set, Credentials with mismatched amounts are rejected. ### generate * **Type:** `AsyncIterable | ((stream: SessionController) => AsyncIterable)` Async iterable that produces application messages. When passed as a function, receives a `SessionController` with a `charge()` method for requesting payment before yielding each value. Each yielded string is sent to the client as an application message frame. ### pollIntervalMs (optional) * **Type:** `number` * **Default:** `100` Polling interval in milliseconds for voucher balance checks. ### route * **Type:** `SessionRoute` Session route handler. Receives synthetic POST requests constructed from in-band authorization frames. The synthetic request carries only the `Authorization` header—no cookies, bodies, query parameters, or other headers from the original WebSocket upgrade request. ```ts twoslash import { Mppx, tempo } from 'mppx/server' import { Session } from 'mppx/tempo' import { privateKeyToAccount } from 'viem/accounts' const mppx = Mppx.create({ methods: [tempo.session({ account: privateKeyToAccount('0x...') })], }) const route: Session.Ws.SessionRoute = (request) => mppx.session({ amount: '0.001', currency: 'pathUSD', unitType: 'word' })(request) ``` ### socket * **Type:** `Session.Ws.Socket` WebSocket instance. Accepts a browser `WebSocket`, a `ws` library socket, or any object implementing `send`/`close` with either `addEventListener`/`removeEventListener` or `on`/`off`. ### store * **Type:** `Store.Store | ChannelStore` Channel store for persisting session and channel state. ### url * **Type:** `string` URL used for constructing synthetic route requests. `ws://` and `wss://` schemes are normalized to `http://` and `https://`. ## Helper functions ### `Session.Ws.parseMessage` Parses a raw WebSocket message string into a typed `Message`. ```ts const message = Session.Ws.parseMessage(raw) ``` ### `Session.Ws.formatAuthorizationMessage` Formats an authorization string into a message frame. ```ts const frame = Session.Ws.formatAuthorizationMessage(authorization) ``` ### `Session.Ws.formatApplicationMessage` Formats application data into a message frame. ```ts const frame = Session.Ws.formatApplicationMessage(data) ``` ### `Session.Ws.formatCloseRequestMessage` Formats a close request message frame. ```ts const frame = Session.Ws.formatCloseRequestMessage() ``` ### `Session.Ws.formatReceiptMessage` Formats a Receipt into a message frame. ```ts const frame = Session.Ws.formatReceiptMessage(receipt) ``` ### `Session.Ws.formatErrorMessage` Formats an error into a message frame. ```ts const frame = Session.Ws.formatErrorMessage({ message, status }) ``` ## Types ### Message ```ts type Message = | { mpp: 'authorization'; authorization: string } | { mpp: 'message'; data: string } | { mpp: 'payment-close-request' } | { mpp: 'payment-close-ready'; data: SessionReceipt } | { mpp: 'payment-error'; status: number; message: string } | { mpp: 'payment-need-voucher'; data: NeedVoucherEvent } | { mpp: 'payment-receipt'; data: SessionReceipt } ``` ### Socket ```ts type Socket = { close(code?: number, reason?: string): unknown send(data: string): unknown addEventListener?: (type: string, listener: (event: any) => void) => unknown removeEventListener?: (type: string, listener: (event: any) => void) => unknown on?: (type: string, listener: (...args: any[]) => void) => unknown off?: (type: string, listener: (...args: any[]) => void) => unknown } ``` ### SessionRoute ```ts type SessionRouteResult = | { status: 402; challenge: Response } | { status: 200; withReceipt(response?: Response): Response } type SessionRoute = (request: Request) => Promise ``` # `Method.tempo.renewSubscription` \[Renew subscriptions outside requests] Renews an overdue Tempo subscription from a background worker or cron job. ## Usage ```ts twoslash import { Store, tempo } from 'mppx/server' const store = Store.memory() const result = await tempo.renewSubscription({ store, subscriptionId: 'sub_abc123', }) console.log(result?.receipt.status) // @log: success ``` The function returns `null` when the subscription is already current. ### With custom renewal Pass `renew` when your application owns settlement. ```ts twoslash import { Store, tempo } from 'mppx/server' const store = Store.memory() const result = await tempo.renewSubscription({ renew: async ({ inFlightReference, periodIndex, subscription }) => { const timestamp = new Date().toISOString() return { receipt: { method: 'tempo', reference: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', status: 'success', subscriptionId: subscription.subscriptionId, timestamp, }, subscription: { ...subscription, inFlightReference, lastChargedPeriod: periodIndex, reference: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', timestamp, }, } }, store, subscriptionId: 'sub_abc123', }) ``` ## Return type ```ts type ReturnType = Promise< | { receipt: SubscriptionReceipt subscription: SubscriptionRecord } | null > ``` ## Parameters ### getClient (optional) * **Type:** `(parameters: { chainId?: number }) => MaybePromise` Function that returns a viem client for the subscription's Tempo chain ID. ### renew (optional) * **Type:** `(parameters: { inFlightReference: string; periodIndex: number; subscription: SubscriptionRecord }) => Promise` Custom renewal hook. The `inFlightReference` is stable for the subscription period and works as an idempotency key. ### renewalTimeoutMs (optional) * **Type:** `number` * **Default:** `900000` Milliseconds before an in-flight renewal lock can be replaced. ### store * **Type:** `Store.AtomicStore>` Atomic store containing subscription records. ### subscriptionId * **Type:** `string` Subscription to renew. ### waitForConfirmation (optional) * **Type:** `boolean` Whether to wait for the renewal transfer to confirm before returning a Receipt. # `Response.requirePayment` \[Create a 402 response] Creates a `402` Payment Required response with a `WWW-Authenticate: Payment` header. Optionally includes RFC 9457 Problem Details in the response body when an error is provided. ## Usage ```ts twoslash import { Challenge } from 'mppx' import { Response } from 'mppx/server' const challenge = Challenge.from({ id: 'challenge-123', method: 'tempo', intent: 'charge', realm: 'api.example.com', request: { amount: '1.00', currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', }, }) const response = Response.requirePayment({ challenge }) // @log: Response { status: 402, headers: { 'WWW-Authenticate': 'Payment ...' } } ``` ## Return type ```ts type ReturnType = Response ``` A standard `Response` with status `402` and a `WWW-Authenticate: Payment` header containing the serialized Challenge. When an `error` is provided, the body contains a JSON problem details object with `Content-Type: application/problem+json`. ## Parameters ### challenge * **Type:** `Challenge` The Challenge to serialize into the `WWW-Authenticate` header. ### error (optional) * **Type:** `PaymentError` An error to include as RFC 9457 Problem Details in the response body. # `Request.toNodeListener` \[Convert Fetch handlers to Node.js] Converts a Fetch API handler into a Node.js HTTP request listener. Useful for running an MPP server on bare `node:http` without a framework. ## Usage ```ts import http from 'node:http' import { Mppx, Request, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo()] }) const server = http.createServer( Request.toNodeListener(mppx.request), ) server.listen(3000) ``` ### `Request.fromNodeListener` Converts a Node.js `IncomingMessage`/`ServerResponse` pair into a Fetch API `Request`. Useful when you need to manually construct a `Request` inside existing Node.js middleware. ```ts import type { IncomingMessage, ServerResponse } from 'node:http' import { Request } from 'mppx/server' function middleware(req: IncomingMessage, res: ServerResponse) { const request = Request.fromNodeListener(req, res) // handle as a Fetch API Request } ``` ## Parameters ### handler * **Type:** `(request: Request) => Promise | Response` A Fetch API handler that receives a `Request` and returns a `Response`. ### options (optional) * **Type:** `RequestListenerOptions` Options forwarded to the underlying adapter, including an optional error handler. # Elysia \[Payment middleware for Elysia] Native [Elysia](https://elysiajs.com) middleware that gates routes behind payment intents. ## Install :::code-group ```bash [npm] $ npm install mppx elysia ``` ```bash [pnpm] $ pnpm add mppx elysia ``` ```bash [bun] $ bun add mppx elysia ``` ::: ## Usage Import `Mppx` and `tempo` from `mppx/elysia` to create an Elysia-aware payment handler. Each intent (for example, `charge`) returns an Elysia `beforeHandle` hook you can use with `.guard()` to scope payment to specific routes. ```ts [server.ts] import { Elysia } from 'elysia' import { Mppx, tempo } from 'mppx/elysia' const mppx = Mppx.create({ methods: [tempo()] }) const app = new Elysia() .guard( { beforeHandle: mppx.charge({ amount: '1' }) }, (app) => app.get('/premium', () => ({ data: 'paid content' })), ) ``` ### Global application Use `.onBeforeHandle()` to apply payment to all routes. ```ts [server.ts] import { Elysia } from 'elysia' import { Mppx, tempo } from 'mppx/elysia' const mppx = Mppx.create({ methods: [tempo()] }) // [!code hl] const app = new Elysia() .onBeforeHandle(mppx.charge({ amount: '1' })) // [!code hl] .get('/premium', () => ({ data: 'paid content' })) .get('/another', () => ({ data: 'also paid' })) ``` ### Session payments Use `mppx.session()` to gate routes behind session-based payment intents. ```ts [server.ts] import { Elysia } from 'elysia' import { Mppx, tempo } from 'mppx/elysia' const mppx = Mppx.create({ methods: [tempo()] }) const app = new Elysia() .guard( { beforeHandle: mppx.session({ amount: '1', unitType: 'token' }) }, (app) => app.get('/content', () => ({ data: 'session content' })), ) ``` # Express \[Payment middleware for Express] Native [Express](https://expressjs.com) middleware that gates routes behind payment intents. ## Install :::code-group ```bash [npm] $ npm install mppx express ``` ```bash [pnpm] $ pnpm add mppx express ``` ```bash [bun] $ bun add mppx express ``` ::: ## Usage Import `Mppx` and `tempo` from `mppx/express` to create an Express-aware payment handler. Each intent (for example, `charge`) returns an Express `RequestHandler` you can slot directly into your route. ```ts [server.ts] import express from 'express' import { Mppx, tempo } from 'mppx/express' const app = express() const mppx = Mppx.create({ methods: [tempo()] }) // [!code hl] app.get( '/premium', mppx.charge({ amount: '1' }), // [!code hl] (req, res) => res.json({ data: 'paid content' }), ) ``` ### Session payments Use `mppx.session()` to gate routes behind session-based payment intents. ```ts [server.ts] import express from 'express' import { Mppx, tempo } from 'mppx/express' const app = express() const mppx = Mppx.create({ methods: [tempo()] }) app.get( '/content', mppx.session({ amount: '1', unitType: 'token' }), (req, res) => res.json({ data: 'session content' }), ) ``` ### Identifying the payer After the middleware verifies payment, the `Authorization` header is still on the request. Parse it with `Credential.deserialize` to read the payer's identity from the `source` field—a DID such as `did:pkh:eip155:1:0x...`. ```ts [server.ts] import express from 'express' import { Credential } from 'mppx' import { Mppx, tempo } from 'mppx/express' const app = express() const mppx = Mppx.create({ methods: [tempo()] }) app.get( '/premium', mppx.charge({ amount: '1' }), (req, res) => { const credential = Credential.deserialize(req.headers.authorization!) const payer = credential.source // "did:pkh:eip155:1:0x..." res.json({ payer }) }, ) ``` # Hono \[Payment middleware for Hono] Native [Hono](https://hono.dev) middleware that gates routes behind payment intents. ## Install :::code-group ```bash [npm] $ npm install mppx hono ``` ```bash [pnpm] $ pnpm add mppx hono ``` ```bash [bun] $ bun add mppx hono ``` ::: ## Usage Import `Mppx` and `tempo` from `mppx/hono` to create a Hono-aware payment handler. Each intent (for example, `charge`) returns a Hono `MiddlewareHandler` you can slot directly into your route. ```ts [server.ts] import { Hono } from 'hono' import { Mppx, tempo } from 'mppx/hono' const app = new Hono() const mppx = Mppx.create({ methods: [tempo()] }) // [!code hl] app.get( '/premium', mppx.charge({ amount: '1' }), // [!code hl] (c) => c.json({ data: 'paid content' }), ) ``` ### Session payments Use `mppx.session()` to gate routes behind session-based payment intents. ```ts [server.ts] import { Hono } from 'hono' import { Mppx, tempo } from 'mppx/hono' const app = new Hono() const mppx = Mppx.create({ methods: [tempo()] }) app.get( '/content', mppx.session({ amount: '1', unitType: 'token' }), (c) => c.json({ data: 'session content' }), ) ``` ### Identifying the payer After the middleware verifies payment, the `Authorization` header is still on the request. Parse it with `Credential.deserialize` to read the payer's identity from the `source` field—a DID such as `did:pkh:eip155:1:0x...`. ```ts [server.ts] import { Hono } from 'hono' import { Credential } from 'mppx' import { Mppx, tempo } from 'mppx/hono' const app = new Hono() const mppx = Mppx.create({ methods: [tempo()] }) app.get( '/premium', mppx.charge({ amount: '1' }), (c) => { const credential = Credential.deserialize(c.req.header('Authorization')!) const payer = credential.source // "did:pkh:eip155:1:0x..." return c.json({ payer }) }, ) ``` # Next.js \[Payment middleware for Next.js] Native [Next.js](https://nextjs.org) route handler wrapper that gates routes behind payment intents. ## Install :::code-group ```bash [npm] $ npm install mppx ``` ```bash [pnpm] $ pnpm add mppx ``` ```bash [bun] $ bun add mppx ``` ::: ## Usage Import `Mppx` and `tempo` from `mppx/nextjs` to create a Next.js-aware payment handler. Each intent (for example, `charge`) returns a wrapper that accepts a route handler. ```ts twoslash [app/api/premium/route.ts] import { Mppx, tempo } from 'mppx/nextjs' const mppx = Mppx.create({ methods: [tempo()] }) // [!code hl] export const GET = mppx.charge({ amount: '1' }) // [!code hl] (() => Response.json({ data: 'paid content' })) ``` ### Session payments Use `mppx.session()` to gate routes behind session-based payment intents. ```ts twoslash [app/api/content/route.ts] import { Mppx, tempo } from 'mppx/nextjs' const mppx = Mppx.create({ methods: [tempo()] }) export const GET = mppx.session({ amount: '1', unitType: 'token' }) (() => Response.json({ data: 'session content' })) ``` ### Identifying the payer After the handler verifies payment, the `Authorization` header is still on the request. Parse it with `Credential.deserialize` to read the payer's identity from the `source` field—a DID such as `did:pkh:eip155:1:0x...`. ```ts twoslash [app/api/premium/route.ts] import { Credential } from 'mppx' import { Mppx, tempo } from 'mppx/nextjs' const mppx = Mppx.create({ methods: [tempo()] }) export const GET = mppx.charge({ amount: '1' }) ((request) => { const credential = Credential.deserialize(request.headers.get('Authorization')!) const payer = credential.source // "did:pkh:eip155:1:0x..." return Response.json({ payer }) }) ``` # Proxy \[Paid API proxy] Gates upstream API services behind MPP `402` payments. The proxy handles routing, credential injection, and payment verification—you configure which endpoints require payment and which are free passthrough. ## Install :::code-group ```bash [npm] $ npm install mppx ``` ```bash [pnpm] $ pnpm add mppx ``` ```bash [bun] $ bun add mppx ``` ::: ## Usage Import `Proxy` and a service preset from `mppx/proxy`, then create an `Mppx` server instance from `mppx/server` to define payment intents. ```ts twoslash [server.ts] import { Proxy, openai } from 'mppx/proxy' import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo()] }) const proxy = Proxy.create({ services: [ openai({ apiKey: process.env.OPENAI_API_KEY!, routes: { 'POST /v1/chat/completions': mppx.charge({ amount: '0.05' }), 'GET /v1/models': true, }, }), ], }) // Bun / Deno export default { fetch: proxy.fetch } // Node.js import { createServer } from 'node:http' createServer(proxy.listener).listen(3000) ``` The proxy returns two handlers: * **`fetch`** — Fetch API handler. Works with Bun, Deno, Next.js, Hono, Elysia, and SvelteKit. * **`listener`** — Node.js request listener. Works with Express, Fastify, and `http.createServer`. ### Multiple services Pass multiple services to gate several upstream APIs behind a single proxy. ```ts twoslash [server.ts] import { Proxy, anthropic, openai, stripe } from 'mppx/proxy' import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo()] }) const proxy = Proxy.create({ description: 'Multi-service paid API proxy', title: 'My Proxy', services: [ openai({ apiKey: process.env.OPENAI_API_KEY!, routes: { 'POST /v1/chat/completions': mppx.charge({ amount: '0.05' }), }, }), anthropic({ apiKey: process.env.ANTHROPIC_API_KEY!, routes: { 'POST /v1/messages': mppx.charge({ amount: '0.03' }), }, }), stripe({ apiKey: process.env.STRIPE_API_KEY!, routes: { 'POST /v1/charges': mppx.charge({ amount: '1' }), 'GET /v1/customers/:id': true, }, }), ], }) ``` Each service is mounted at `/{serviceId}/`—for example, requests to `/openai/v1/chat/completions` route to `https://api.openai.com/v1/chat/completions`. ## Built-in services ### `openai` Creates an OpenAI service definition. Injects `Authorization: Bearer` header for upstream authentication. ```ts [server.ts] import { openai } from 'mppx/proxy' openai({ apiKey: 'sk-...', routes: { 'POST /v1/chat/completions': mppx.charge({ amount: '0.05' }), 'POST /v1/embeddings': mppx.charge({ amount: '0.01' }), 'POST /v1/images/generations': mppx.charge({ amount: '0.10' }), 'GET /v1/models': true, }, }) ``` | Parameter | Type | Description | |-----------|------|-------------| | `apiKey` | `string` | OpenAI API key. Used as `Authorization: Bearer` header. | | `baseUrl` (optional) | `string` | Base URL override. Defaults to `'https://api.openai.com'`. | | `routes` | `EndpointMap` | Route definitions for OpenAI endpoints. | **Typed routes:** `POST /v1/chat/completions`, `POST /v1/completions`, `POST /v1/embeddings`, `POST /v1/images/generations`, `POST /v1/images/edits`, `POST /v1/images/variations`, `POST /v1/audio/transcriptions`, `POST /v1/audio/translations` ### `anthropic` Creates an Anthropic service definition. Injects `x-api-key` header for upstream authentication. ```ts [server.ts] import { anthropic } from 'mppx/proxy' anthropic({ apiKey: 'sk-ant-...', routes: { 'POST /v1/messages': mppx.charge({ amount: '0.03' }), 'POST /v1/complete': mppx.charge({ amount: '0.02' }), }, }) ``` | Parameter | Type | Description | |-----------|------|-------------| | `apiKey` | `string` | Anthropic API key. Used as `x-api-key` header. | | `baseUrl` (optional) | `string` | Base URL override. Defaults to `'https://api.anthropic.com'`. | | `routes` | `EndpointMap` | Route definitions for Anthropic endpoints. | **Typed routes:** `POST /v1/messages`, `POST /v1/messages/batches`, `GET /v1/messages/batches`, `GET /v1/messages/batches/:batchId`, `POST /v1/complete` ### `stripe` Creates a Stripe service definition. Injects `Authorization: Basic` header (API key as username) for upstream authentication. This is a proxy service for the Stripe API—not a payment method. ```ts [server.ts] import { stripe } from 'mppx/proxy' stripe({ apiKey: 'sk-...', routes: { 'POST /v1/charges': mppx.charge({ amount: '1' }), 'GET /v1/customers/:id': true, }, }) ``` | Parameter | Type | Description | |-----------|------|-------------| | `apiKey` | `string` | Stripe API key. Used as Basic auth username. | | `baseUrl` (optional) | `string` | Base URL override. Defaults to `'https://api.stripe.com'`. | | `routes` | `EndpointMap` | Route definitions for Stripe endpoints. | **Typed routes:** `POST /v1/charges`, `POST /v1/customers`, `GET /v1/customers/:id`, `POST /v1/payment_intents`, `GET /v1/payment_intents/:id`, `POST /v1/subscriptions`, `GET /v1/subscriptions/:id`, `POST /v1/invoices`, `GET /v1/invoices/:id` ## Custom services Use `Service.from` (or its alias `custom`) to define a service for any upstream API. ### With `bearer` shorthand ```ts twoslash [server.ts] import { Proxy, Service } from 'mppx/proxy' import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo()] }) const proxy = Proxy.create({ services: [ Service.from('my-api', { baseUrl: 'https://api.example.com', bearer: process.env.MY_API_KEY!, description: 'Example upstream API', title: 'My API', routes: { 'POST /v1/generate': mppx.charge({ amount: '0.01' }), 'GET /v1/status': true, }, }), ], }) ``` ### With `headers` shorthand ```ts [server.ts] import { Service } from 'mppx/proxy' Service.from('custom-api', { baseUrl: 'https://api.example.com', headers: { 'X-API-Key': process.env.CUSTOM_API_KEY!, 'X-Org-Id': 'org-123', }, routes: { 'POST /v1/query': mppx.charge({ amount: '0.02' }), }, }) ``` ### With `rewriteRequest` For full control over the upstream request, use `rewriteRequest`. The context includes per-endpoint options set via the `options` field on an endpoint definition. ```ts [server.ts] import { Service } from 'mppx/proxy' Service.from('advanced-api', { baseUrl: 'https://api.example.com', rewriteRequest(request, ctx) { request.headers.set('Authorization', `Token ${process.env.API_TOKEN}`) return request }, routes: { 'POST /v1/generate': mppx.charge({ amount: '0.05' }), }, }) ``` ## Discovery endpoints The proxy automatically serves discovery endpoints that describe available services and their routes. Coding agents and CLI tools use these endpoints to understand what the proxy offers. | Endpoint | Content-Type | Description | |----------|--------------|-------------| | `GET /discover` | `application/json` or `text/plain` | Lists all services. Returns JSON by default, markdown for AI user agents and terminal clients. | | `GET /discover/{id}` | `application/json` or `text/markdown` | Details for a single service, including routes and pricing. | | `GET /discover/{id}.md` | `text/markdown` | Markdown description of a single service. | | `GET /discover/all` | `application/json` or `text/markdown` | All services with full route details. | | `GET /discover/all.md` | `text/markdown` | Markdown listing of all services and routes. | | `GET /llms.txt` | `text/plain` | `llms.txt`-formatted overview of the proxy and its services. | | `GET /discover.md` | `text/plain` | Alias for `/llms.txt`. | The proxy returns markdown instead of JSON when the request comes from a known AI user agent (for example, `ChatGPT-User`, `ClaudeBot`, `PerplexityBot`) or a terminal client (for example, `curl`, `HTTPie`, `mppx`). ## Parameters ### `Proxy.create` config ### basePath (optional) * **Type:** `string` Base path prefix to strip before routing (for example, `'/api/proxy'`). Use when the proxy is mounted at a sub-path. ### description (optional) * **Type:** `string` Short description of the proxy shown in `llms.txt` and discovery endpoints. ### fetch (optional) * **Type:** `typeof globalThis.fetch` Custom `fetch` implementation. Defaults to `globalThis.fetch`. ### services * **Type:** `Service[]` Array of service definitions to proxy. Each service is mounted at `/{serviceId}/`. ### title (optional) * **Type:** `string` Human-readable title for the proxy shown in `llms.txt` and discovery endpoints. ## Service type reference ### `Service.from` config ### baseUrl * **Type:** `string` Base URL of the upstream service (for example, `'https://api.openai.com'`). ### bearer (optional) * **Type:** `string` Shorthand: injects `Authorization: Bearer {token}` header on upstream requests. ### description (optional) * **Type:** `string` Short description of the service, shown in discovery endpoints. ### docsLlmsUrl (optional) * **Type:** `string | ((options: { route?: string }) => string | undefined)` Documentation URL for the service. Provide a string for a static URL, or a function that receives an optional route pattern and returns a per-endpoint docs URL. ### headers (optional) * **Type:** `Record` Shorthand: injects custom headers on upstream requests. ### mutate (optional) * **Type:** `(req: Request) => Request | Promise` Shorthand: full request mutation function. Takes priority over `bearer` and `headers`. ### rewriteRequest (optional) * **Type:** `(req: Request, ctx: Context) => Request | Promise` Hook to modify the upstream request before sending. Receives per-endpoint options via `ctx`. ### routes * **Type:** `EndpointMap` Map of `"METHOD /pattern"` keys to endpoint definitions. Each value is one of: * **`IntentHandler`** — Payment required. The handler issues a `402` Challenge or verifies payment. * **`{ pay: IntentHandler, options: EndpointOptions }`** — Payment required with per-endpoint config overrides passed to `rewriteRequest` via `ctx`. * **`true`** — Free passthrough. No payment required; `rewriteRequest` is still applied. ### title (optional) * **Type:** `string` Human-readable title for the service (for example, `'OpenAI'`). # `BodyDigest.compute` \[Compute a body digest hash] Computes a SHA-256 digest of the given body. ## Usage ```ts twoslash import { BodyDigest } from 'mppx' const digest = BodyDigest.compute({ amount: '1000' }) // => 'sha-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE' ``` ## Return type ```ts type ReturnType = `sha-256=${string}` ``` A digest string in the format `sha-256=base64hash`. ## Parameters ### body * **Type:** `Record | string` The body to digest. Can be a JSON object or a string. # `BodyDigest.verify` \[Verify a body digest hash] Verifies that a digest matches the given body. ## Usage ```ts twoslash import { BodyDigest } from 'mppx' const digest = BodyDigest.compute({ amount: '1000' }) const isValid = BodyDigest.verify(digest, '{"amount":"1000"}') // => true ``` ## Return type ```ts type ReturnType = boolean ``` `true` if the digest matches, `false` otherwise. ## Parameters ### body * **Type:** `Record | string` The body to verify against. ### digest * **Type:** `` `sha-256=${string}` `` The digest to verify. # `Challenge.deserialize` \[Deserialize a Challenge from a header] Deserializes a WWW-Authenticate header value to a challenge. ## Usage ```ts twoslash import { Challenge } from 'mppx' const header = 'Payment id="abc123", realm="mpp.dev", method="tempo", intent="charge", request="eyJhbW91bnQiOi..."' const challenge = Challenge.deserialize(header) ``` ### With method type narrowing Use a method definition to get type-safe access to method-specific request fields. ```ts twoslash import { Challenge } from 'mppx' import { Methods } from 'mppx/tempo' const header = 'Payment id="abc123", realm="mpp.dev", method="tempo", intent="charge", request="eyJhbW91bnQiOi..."' const challenge = Challenge.deserialize(header, { methods: [Methods.charge] }) ``` ## Return type ```ts type ReturnType = Challenge ``` The deserialized Challenge object. `mppx` decodes `request` and `opaque` from their base64url-encoded JCS strings back to structured values. If the serialized header contains an `opaque` parameter, the parsed value is accessible via `Challenge.meta`. ## Parameters ### options (optional) * **Type:** `{ methods?: readonly Method.Method[] }` Optional settings to narrow the Challenge type using method intents. ### value * **Type:** `string` The WWW-Authenticate header value. # `Challenge.from` \[Create a new Challenge] Creates a challenge from the given parameters. If `secretKey` option is provided, the challenge ID is computed as HMAC-SHA256 over the challenge parameters (realm|method|intent|request|expires|digest|opaque), cryptographically binding the ID to its contents. When `expires`, `digest`, or `opaque` is absent, that slot is an empty string. Load `secretKey` from your server environment or secret manager. Do not ship it to clients. ## Usage ```ts twoslash import { Challenge } from 'mppx' const secretKey = process.env.MPP_SECRET_KEY! // With HMAC-bound ID (recommended for servers) const challenge = Challenge.from( { intent: 'charge', method: 'tempo', realm: 'mpp.dev', request: { amount: '1000000', currency: '0x...', recipient: '0x...' }, secretKey, }, ) ``` ### With explicit ID Use an explicit ID when you don't need HMAC-bound challenge verification. ```ts twoslash import { Challenge } from 'mppx' const challenge = Challenge.from({ id: 'abc123', intent: 'charge', method: 'tempo', realm: 'mpp.dev', request: { amount: '1000000', currency: '0x...', recipient: '0x...' }, }) ``` ## Return type ```ts type ReturnType = Challenge ``` A challenge object. ## Parameters ### description (optional) * **Type:** `string` Human-readable description of the payment. ### digest (optional) * **Type:** `string` Digest of the request body. ### expires (optional) * **Type:** `string` Expiration timestamp (ISO 8601). ### id (when not using secretKey) * **Type:** `string` Explicit challenge ID. ### intent * **Type:** `string` Intent type (for example, `"charge"`, `"session"`). ### meta (optional) * **Type:** `Record` Server-defined correlation data. `mppx` serializes it as the base64url-encoded `opaque` auth-param in HTTP Challenges. ### method * **Type:** `string` Payment method (for example, "tempo", "stripe"). ### parameters Challenge parameters. Must include either `id` or `secretKey`. ### realm * **Type:** `string` Server realm (for example, hostname). ### request * **Type:** `Record` Method-specific request data. `mppx` serializes it as base64url-encoded JCS JSON in the HTTP `request` auth-param. ### secretKey (when not using id) * **Type:** `string` Server secret for HMAC-bound challenge ID. Keep it server-side and load it from your environment or secret manager. # `Challenge.fromHeaders` \[Extract a Challenge from Headers] Extracts the challenge from a Headers object. ## Usage ```ts twoslash import { Challenge } from 'mppx' const response = await fetch('/resource') const challenge = Challenge.fromHeaders(response.headers) ``` ### With method type narrowing Use a method definition to get type-safe access to method-specific request fields. ```ts twoslash import { Challenge } from 'mppx' import { Methods } from 'mppx/tempo' const response = await fetch('/resource') const challenge = Challenge.fromHeaders(response.headers, { methods: [Methods.charge] }) ``` ## Return type ```ts type ReturnType = Challenge ``` The deserialized challenge object. ## Parameters ### headers * **Type:** `Headers` The HTTP headers object. ### options (optional) * **Type:** `{ methods?: readonly Method.Method[] }` Optional settings to narrow the Challenge type using method intents. # `Challenge.fromMethod` \[Create a Challenge from a method] Creates a validated Challenge from a method definition. Load `secretKey` from your server environment or secret manager. Do not ship it to clients. ## Usage ```ts twoslash import { Challenge } from 'mppx' import { Methods } from 'mppx/tempo' const secretKey = process.env.MPP_SECRET_KEY! const challenge = Challenge.fromMethod( Methods.charge, { realm: 'mpp.dev', request: { amount: '1', currency: '0x20c0000000000000000000000000000000000001', decimals: 6, recipient: '0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00', }, secretKey, }, ) ``` ## Return type ```ts type ReturnType = Challenge ``` A Challenge typed to the method's request schema output. ## Parameters ### description (optional) * **Type:** `string` Human-readable description of the payment. ### digest (optional) * **Type:** `string` Digest of the request body. ### expires (optional) * **Type:** `string` Expiration timestamp (ISO 8601). ### id (when not using secretKey) * **Type:** `string` Explicit challenge ID. ### meta (optional) * **Type:** `Record` Server-defined correlation data. `mppx` serializes it as the base64url-encoded `opaque` auth-param in HTTP Challenges. ### method * **Type:** `Method` The method definition to validate against (for example, `Methods.charge`). ### parameters Challenge parameters. Must include either `id` or `secretKey`. ### realm * **Type:** `string` Server realm (for example, hostname). ### request * **Type:** `z.input` Method-specific request data, validated against the method's schema. `mppx` serializes it as base64url-encoded JCS JSON in the HTTP `request` auth-param. ### secretKey (when not using id) * **Type:** `string` Server secret for HMAC-bound challenge ID. Keep it server-side and load it from your environment or secret manager. # `Challenge.fromResponse` \[Extract a Challenge from a Response] Extracts the challenge from a Response's WWW-Authenticate header. ## Usage ```ts twoslash import { Challenge } from 'mppx' const response = await fetch('/resource') if (response.status === 402) { const challenge = Challenge.fromResponse(response) } ``` ### With method type narrowing Use a method definition to get type-safe access to method-specific request fields. ```ts twoslash import { Challenge } from 'mppx' import { Methods } from 'mppx/tempo' const response = await fetch('/resource') if (response.status === 402) { const challenge = Challenge.fromResponse(response, { methods: [Methods.charge] }) } ``` ## Return type ```ts type ReturnType = Challenge ``` The deserialized challenge object. ## Parameters ### options (optional) * **Type:** `{ methods?: readonly Method.Method[] }` Optional settings to narrow the Challenge type using method intents. ### response * **Type:** `Response` The HTTP response (must be `402` status). # `Challenge.meta` \[Extract correlation data from a Challenge] Extracts server-defined correlation data from a Challenge. ## Usage ```ts twoslash import { Challenge } from 'mppx' declare const challenge: Challenge.Challenge // ---cut--- const data = Challenge.meta(challenge) ``` ## Return type ```ts type ReturnType = Record | undefined ``` The `opaque` field from the Challenge, or `undefined` if not set. On HTTP transport, the same data travels in the `opaque` auth-param as base64url-encoded JCS JSON. ## Parameters ### challenge * **Type:** `Challenge` The Challenge to extract correlation data from. # `Challenge.serialize` \[Serialize a Challenge to a header] Serializes a challenge to the WWW-Authenticate header format. ## Usage ```ts twoslash import { Challenge } from 'mppx' const challenge = Challenge.from({ id: 'abc123', intent: 'charge', method: 'tempo', realm: 'mpp.dev', request: { amount: '1000000', currency: '0x...', recipient: '0x...' }, }) const header = Challenge.serialize(challenge) // @log: 'Payment id="abc123", realm="mpp.dev", method="tempo", intent="charge", request="eyJhbW91bnQiOi..."' ``` ## Return type ```ts type ReturnType = string ``` A string suitable for the WWW-Authenticate header value. The serialized string includes optional fields when present: `description`, `digest`, `expires`, and `opaque` (server-defined correlation data set via `meta` in `Challenge.from`). In HTTP headers, `request` and `opaque` are emitted as base64url-encoded JCS JSON strings. ## Parameters ### challenge * **Type:** `Challenge` The challenge to serialize. # `Challenge.verify` \[Verify a Challenge HMAC] Verifies that a challenge ID matches the expected HMAC for the given parameters. Use the same server-managed secret that produced the Challenge ID. ## Usage ```ts twoslash import { Challenge } from 'mppx' const secretKey = process.env.MPP_SECRET_KEY! const challenge = Challenge.from({ intent: 'charge', method: 'tempo', realm: 'mpp.dev', request: { amount: '1000000', currency: '0x...', recipient: '0x...' }, secretKey, }) const isValid = Challenge.verify(challenge, { secretKey }) // => true ``` ## Return type ```ts type ReturnType = boolean ``` `true` if the challenge ID is valid, `false` otherwise. ## Parameters ### challenge * **Type:** `Challenge` The challenge to verify. ### options ### secretKey * **Type:** `string` Server secret for HMAC-bound challenge ID verification. Keep it server-side and load it from your environment or secret manager. # `Credential.deserialize` \[Deserialize a Credential from a header] Deserializes an Authorization header value to a credential. ## Usage ```ts twoslash import { Credential } from 'mppx' const header = 'Payment eyJjaGFsbGVuZ2UiOnsi...' const credential = Credential.deserialize(header) ``` ## Return type ```ts type ReturnType = Credential ``` The deserialized credential object. `mppx` parses the echoed Challenge inside the Credential, decoding `request` and `opaque` from their base64url-encoded wire strings back to structured values for application code. ## Parameters ### value * **Type:** `string` The Authorization header value. # `Credential.from` \[Create a new Credential] Creates a credential from the given parameters. ## Usage ```ts twoslash import { Credential, Challenge } from 'mppx' const challenge = Challenge.from({ id: 'abc123', intent: 'charge', method: 'tempo', realm: 'mpp.dev', request: { amount: '1000000', currency: '0x...', recipient: '0x...' }, }) const credential = Credential.from({ challenge, payload: { signature: '0x...' }, }) ``` ## Return type ```ts type ReturnType = Credential ``` A credential object containing the challenge and payment proof. ## Parameters ### challenge * **Type:** `Challenge` The challenge from the `402` response. ### parameters ### payload * **Type:** `unknown` Method-specific payment proof. ### source (optional) * **Type:** `string` Payer identifier as a DID (for example, "did:pkh:eip155:1:0x..."). # `Credential.fromRequest` \[Extract a Credential from a Request] Extracts the credential from a Request's Authorization header. ## Usage ```ts twoslash import { Credential } from 'mppx' export async function handler(request: Request) { const credential = Credential.fromRequest(request) // ... } ``` ## Return type ```ts type ReturnType = Credential ``` The deserialized credential object. ## Parameters ### request * **Type:** `Request` The HTTP request. # `Credential.serialize` \[Serialize a Credential to a header] Serializes a credential to the Authorization header format. ## Usage ```ts twoslash import { Credential, Challenge } from 'mppx' const challenge = Challenge.from({ id: 'abc123', intent: 'charge', method: 'tempo', realm: 'mpp.dev', request: { amount: '1000000', currency: '0x...', recipient: '0x...' }, }) const credential = Credential.from({ challenge, payload: { signature: '0x...' }, }) const header = Credential.serialize(credential) // => 'Payment eyJjaGFsbGVuZ2UiOnsi...' ``` ## Return type ```ts type ReturnType = string ``` A string suitable for the Authorization header value. When the Credential includes an echoed Challenge, `mppx` keeps the HTTP wire format intact: `challenge.request` and `challenge.opaque` serialize as the same base64url-encoded strings that came from the Challenge header. ## Parameters ### credential * **Type:** `Credential` The credential to serialize. # `Expires` \[Generate relative expiration timestamps] Utility functions for generating ISO 8601 datetime strings relative to the current time. ## Usage ```ts twoslash import { Expires } from 'mppx' // Expire in 30 seconds const in30Seconds = Expires.seconds(30) // Expire in 5 minutes const in5Minutes = Expires.minutes(5) // Expire in 2 hours const in2Hours = Expires.hours(2) // Expire in 7 days const in7Days = Expires.days(7) // Expire in 2 weeks const in2Weeks = Expires.weeks(2) // Expire in 3 months const in3Months = Expires.months(3) // Expire in 1 year const in1Year = Expires.years(1) ``` ## Functions ### seconds Returns an ISO 8601 datetime string `n` seconds from now. ```ts function seconds(n: number): string ``` ### minutes Returns an ISO 8601 datetime string `n` minutes from now. ```ts function minutes(n: number): string ``` ### hours Returns an ISO 8601 datetime string `n` hours from now. ```ts function hours(n: number): string ``` ### days Returns an ISO 8601 datetime string `n` days from now. ```ts function days(n: number): string ``` ### weeks Returns an ISO 8601 datetime string `n` weeks from now. ```ts function weeks(n: number): string ``` ### months Returns an ISO 8601 datetime string `n` months (30 days) from now. ```ts function months(n: number): string ``` ### years Returns an ISO 8601 datetime string `n` years (365 days) from now. ```ts function years(n: number): string ``` ## Return type All functions return: ```ts type ReturnType = string ``` An ISO 8601 datetime string (for example, `"2025-01-15T12:30:00.000Z"`). ## Parameters ### n * **Type:** `number` The number of time units from now. # `Method.from` \[Create a payment method definition] Creates a payment method definition. ## Usage ```ts twoslash [tempo/methods.ts] import { Method, z } from 'mppx' const charge = Method.from({ intent: 'charge', name: 'tempo', schema: { credential: { payload: z.object({ signature: z.string(), type: z.literal('transaction'), }), }, request: z.object({ amount: z.string(), currency: z.string(), recipient: z.string(), }), }, }) ``` ## Return type ```ts type ReturnType = method ``` The method object passed in (identity function for type inference). ## Parameters ### intent * **Type:** `string` Intent type (for example, `"charge"`, `"session"`). ### method Payment method definition. ### name * **Type:** `string` Payment method name (for example, `"tempo"`, `"stripe"`). ### schema * **Type:** `{ credential: { payload: ZodMiniType }, request: ZodMiniType }` Zod schemas for validating Credential payloads and request parameters. # `Method.toClient` \[Extend a method with client logic] Extends a payment method with client-side Credential creation logic. ## Usage ::::code-group ```ts twoslash [methods.client.ts] import { Mppx } from 'mppx/client' import { Credential, Method } from 'mppx' // [!code focus] import * as Methods from './methods' // [!code focus] // [!code focus:start] // Create client-configured method. const charge = Method.toClient(Methods.charge, { async createCredential({ challenge }) { const payload = { signature: '0x...', type: 'transaction' as const } return Credential.serialize({ challenge, payload }) }, }) // [!code focus:end] // Create Mppx client with the method configured. Mppx.create({ methods: [charge], }) ``` ```ts twoslash [methods.ts] filename="methods.ts" import { Method, z } from 'mppx' export const charge = Method.from({ intent: 'charge', name: 'tempo', schema: { credential: { payload: z.object({ signature: z.string(), type: z.literal('transaction'), }), }, request: z.object({ amount: z.string(), currency: z.string(), recipient: z.string(), }), }, }) ``` :::: ## Return type ```ts type ReturnType = Method.Client ``` A client-configured method that can be passed to `Mppx.create`. ## Parameters ### context (optional) * **Type:** `ZodMiniType` Zod schema for additional context passed to `createCredential`. ### createCredential * **Type:** `(parameters: { challenge: Challenge; context?: context }) => Promise` Function that creates a serialized Credential string from a Challenge. ### method * **Type:** `Method` The base payment method definition (created with `Method.from`). ### options # `Method.toServer` \[Extend a method with server verification] Extends a payment method with server-side verification logic. ## Usage ::::code-group ```ts twoslash [methods.server.ts] import { Mppx } from 'mppx/server' import { Method, Receipt } from 'mppx' // [!code focus] import * as Methods from './methods' // [!code focus] // [!code focus:start] // Create server-configured method. const charge = Method.toServer(Methods.charge, { async verify({ credential, request }) { return Receipt.from({ method: 'tempo', reference: '0x...', status: 'success', timestamp: new Date().toISOString(), }) }, }) // [!code focus:end] // Create Mppx server with the method configured. const mppx = Mppx.create({ methods: [charge], }) ``` ```ts twoslash [methods.ts] filename="methods.ts" import { Method, z } from 'mppx' export const charge = Method.from({ intent: 'charge', name: 'tempo', schema: { credential: { payload: z.object({ signature: z.string(), type: z.literal('transaction'), }), }, request: z.object({ amount: z.string(), currency: z.string(), recipient: z.string(), }), }, }) ``` :::: ## Return type ```ts type ReturnType = Method.Server ``` A server-configured method that can be passed to `Mppx.create`. ## Parameters ### defaults (optional) * **Type:** `Partial` Default request parameters merged into every Challenge issued for this method. ### method * **Type:** `Method` The base payment method definition (created with `Method.from`). ### options ### request (optional) * **Type:** `(options: { credential?: Credential; request: request }) => request` Transform function called before Challenge creation. Use to modify or enrich request parameters. ### respond (optional) * **Type:** `(parameters: { credential: Credential; input: Request; receipt: Receipt; request: request }) => Response | undefined` Called after `verify` succeeds. Return a `Response` to short-circuit the handler (for example, for channel open/close management responses). Return `undefined` to let the server handler serve content via `withReceipt(response)`. HTTP-only—MCP transports do not invoke this hook. ### transport (optional) * **Type:** `Transport` Override the transport for this method. ### verify * **Type:** `(parameters: { credential: Credential; request: request }) => Promise` Function that verifies a Credential and returns a Receipt. # `PaymentRequest.deserialize` \[Deserialize a payment request] Deserializes a base64url JCS string to a payment request. ## Usage ```ts twoslash import { PaymentRequest } from 'mppx' const encoded = 'eyJhbW91bnQiOiIxMDAwMDAwIiwiY3VycmVuY3kiOiIweC4uLiJ9' const request = PaymentRequest.deserialize(encoded) ``` ## Return type ```ts type ReturnType = Request ``` The deserialized request object. ## Parameters ### encoded * **Type:** `string` The base64url-encoded JCS string. # `PaymentRequest.from` \[Create a payment request] Creates a payment request from the given parameters. ## Usage ```ts twoslash import { PaymentRequest } from 'mppx' const request = PaymentRequest.from({ amount: '1000000', currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', }) ``` ## Return type ```ts type ReturnType = request ``` The request object passed in (identity function for type inference). ## Parameters ### request * **Type:** `Record` Request parameters. The shape depends on the intent being used. # `PaymentRequest.serialize` \[Serialize a payment request to a string] Serializes a payment request to a base64url JCS string. ## Usage ```ts twoslash import { PaymentRequest } from 'mppx' const request = PaymentRequest.from({ amount: '1000000', currency: '0x20c0000000000000000000000000000000000000', recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', }) const serialized = PaymentRequest.serialize(request) // => "eyJhbW91bnQiOiIxMDAwMDAwIiwiY3VycmVuY3kiOiIweC4uLiJ9" ``` ## Return type ```ts type ReturnType = string ``` A base64url-encoded JCS string (no padding). ## Parameters ### request * **Type:** `Request` The request to serialize. # `Receipt.deserialize` \[Deserialize a Receipt from a header] Deserializes a Payment-Receipt header value to a receipt. ## Usage ```ts twoslash import { Receipt } from 'mppx' const encoded = 'eyJzdGF0dXMiOiJzdWNjZXNzIiwidGltZXN0YW1wIjoi...' const receipt = Receipt.deserialize(encoded) ``` ## Return type ```ts type ReturnType = Receipt ``` The deserialized receipt object. ## Parameters ### encoded * **Type:** `string` The base64url-encoded header value. # `Receipt.from` \[Create a new Receipt] Creates a receipt from the given parameters. ## Usage ```ts twoslash import { Receipt } from 'mppx' const receipt = Receipt.from({ method: 'tempo', reference: '0x...', status: 'success', timestamp: new Date().toISOString(), }) ``` ## Return type ```ts type ReturnType = Receipt ``` A validated receipt object. ## Parameters ### externalId (optional) * **Type:** `string` External reference ID echoed from the Credential payload. ### method * **Type:** `string` Payment method used (for example, "tempo", "stripe"). ### parameters ### reference * **Type:** `string` Method-specific reference (for example, transaction hash). ### status * **Type:** `'success'` Payment status. ### timestamp * **Type:** `string` RFC 3339 settlement timestamp. # `Receipt.fromResponse` \[Extract a Receipt from a Response] Extracts the receipt from a Response's Payment-Receipt header. ## Usage ```ts twoslash import { Receipt } from 'mppx' const response = await fetch('/resource', { headers: { Authorization: 'Payment ...' }, }) if (response.ok) { const receipt = Receipt.fromResponse(response) } ``` ## Return type ```ts type ReturnType = Receipt ``` The deserialized receipt object. ## Parameters ### response * **Type:** `Response` The HTTP response. # `Receipt.serialize` \[Serialize a Receipt to a string] Serializes a receipt to the Payment-Receipt header format. ## Usage ```ts twoslash import { Receipt } from 'mppx' const receipt = Receipt.from({ method: 'tempo', reference: '0x...', status: 'success', timestamp: new Date().toISOString(), }) const header = Receipt.serialize(receipt) // => "eyJzdGF0dXMiOiJzdWNjZXNzIiwidGltZXN0YW1wIjoi..." ``` ## Return type ```ts type ReturnType = string ``` A base64url-encoded string suitable for the Payment-Receipt header value. ## Parameters ### receipt * **Type:** `Receipt` The receipt to serialize. # `Html.init` \[Initialize a payment UI context] Sets up a context for building payment method UIs in the browser. Returns challenge data, theme tokens, and helpers for error handling and credential submission. For a full guide on adding HTML support to a custom payment method, see [Custom HTML](/sdk/typescript/html/custom). ## Usage ```ts [example.ts] import * as Html from 'mppx/html' const context = Html.init('tempo') // Mount your UI const button = document.createElement('button') button.textContent = context.text.pay context.root.appendChild(button) ``` ### With credential submission Build a complete payment form that handles errors and submits credentials. ```ts [example.ts] import * as Html from 'mppx/html' const c = Html.init('tempo') const button = document.createElement('button') button.textContent = c.text.pay button.onclick = async () => { try { c.error() button.disabled = true const credential = await method.createCredential({ challenge: c.challenge, context: {}, }) await c.submit(credential) } catch (e) { c.error(e instanceof Error ? e.message : 'Payment failed') } finally { button.disabled = false } } c.root.appendChild(button) ``` ### With CSS theming Use `context.vars` for CSS custom property references that respect the server-configured theme. ```ts [example.ts] import * as Html from 'mppx/html' const c = Html.init('tempo') const style = document.createElement('style') style.textContent = ` button { background: ${c.vars.accent}; border-radius: ${c.vars.radius}; color: ${c.vars.background}; font-family: ${c.vars.fontFamily}; padding: calc(${c.vars.spacingUnit} * 4) calc(${c.vars.spacingUnit} * 8); } ` c.root.appendChild(style) ``` ## Return type ```ts type Context = { /** The parsed Challenge object for this payment method. */ challenge: Challenge /** Handler-specific HTML configuration. */ config: Record /** Show or clear an error message below the root element. */ error: (message?: string | null | undefined) => void /** Pre-formatted amount string (for example, "$10.00"). */ formattedAmount: string /** Human-readable payment method label. */ label: string /** The DOM element to mount your payment UI into. */ root: HTMLElement /** Submit a credential and reload the page. */ submit: (credential: string) => Promise /** UI text strings with defaults applied. */ text: { expires: string; pay: string; paymentRequired: string; title: string } /** Resolved theme tokens (colors, spacing, typography). */ theme: Record /** CSS custom property references for theming. */ vars: { accent: CssVar // var(--mppx-accent) background: CssVar // var(--mppx-background) border: CssVar // var(--mppx-border) fontFamily: CssVar // var(--mppx-font-family) fontSizeBase: CssVar // var(--mppx-font-size-base) foreground: CssVar // var(--mppx-foreground) muted: CssVar // var(--mppx-muted) negative: CssVar // var(--mppx-negative) positive: CssVar // var(--mppx-positive) radius: CssVar // var(--mppx-radius) spacingUnit: CssVar // var(--mppx-spacing-unit) surface: CssVar // var(--mppx-surface) } } ``` ## Parameters ### methodName * **Type:** `string` The payment method name to initialize (for example, `'tempo'`, `'stripe'`). Matches against the challenge data the server embeds in the page. ## Context properties ### challenge * **Type:** `Challenge` The parsed Challenge object for this payment method. Contains `intent`, `method`, `realm`, `request`, and other challenge fields. ### config * **Type:** `Record` Method-specific configuration provided by the server. For example, Stripe passes `{ publishableKey: string }`. ### error * **Type:** `(message?: string | null | undefined) => void` Shows or clears an error message. Pass a string to display an error below the root element. Call with no arguments (or `null`/`undefined`) to clear the error. ### formattedAmount * **Type:** `string` Pre-formatted amount string from the server (for example, `"$10.00"`). ### label * **Type:** `string` Payment method label, used for tab labels when multiple methods are available. ### root * **Type:** `HTMLElement` The DOM element to mount your payment UI into. Append your form elements here. ### submit * **Type:** `(credential: string) => Promise` Sends the credential to the server via a Service Worker, then reloads the page. Call after creating a credential from the challenge. ### text * **Type:** `{ expires: string; pay: string; paymentRequired: string; title: string }` UI text strings with defaults applied. | Key | Default | |---|---| | `expires` | `"Expires at"` | | `pay` | `"Pay"` | | `paymentRequired` | `"Payment Required"` | | `title` | `"Payment Required"` | ### theme * **Type:** `Record` Resolved theme tokens with defaults applied. Includes color tokens (`accent`, `background`, `border`, `foreground`, `muted`, `negative`, `positive`, `surface`) and layout tokens (`colorScheme`, `fontFamily`, `fontSizeBase`, `radius`, `spacingUnit`). ### vars * **Type:** `typeof vars` CSS custom property references for use in inline styles or `