From tenequm-skills
Build with MPP (Machine Payments Protocol) - the open protocol for machine-to-machine payments over HTTP 402. Use when developing paid APIs, payment-gated content, AI agent payment flows, MCP tool payments, pay-per-token streaming, or any service using HTTP 402 Payment Required. Covers the mppx TypeScript SDK with Hono/Express/Next.js/Elysia middleware, pympp Python SDK, and mpp Rust SDK. Supports Tempo stablecoins, Stripe cards, Lightning Bitcoin, and custom payment methods. Includes charge (one-time) and session (streaming pay-as-you-go) intents. Make sure to use this skill whenever the user mentions mpp, mppx, machine payments, HTTP 402 payments, Tempo payments, payment channels, pay-per-token, paid API endpoints, or payment-gated services.
npx claudepluginhub tenequm/skills --plugin gh-cliThis skill uses the workspace's default tool permissions.
MPP is an open protocol (co-authored by Tempo and Stripe) that standardizes HTTP `402 Payment Required` for machine-to-machine payments. Clients pay in the same HTTP request - no accounts, API keys, or checkout flows needed.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Checks Next.js compilation errors using a running Turbopack dev server after code edits. Fixes actionable issues before reporting complete. Replaces `next build`.
MPP is an open protocol (co-authored by Tempo and Stripe) that standardizes HTTP 402 Payment Required for machine-to-machine payments. Clients pay in the same HTTP request - no accounts, API keys, or checkout flows needed.
The core protocol spec is submitted to the IETF as the Payment HTTP Authentication Scheme.
Three primitives power every MPP payment:
WWW-Authenticate: Payment header)Authorization: Payment header)Payment-Receipt header)Client Server
│ (1) GET /resource │
├──────────────────────────────────────────────>│
│ (2) 402 + WWW-Authenticate: Payment │
│<──────────────────────────────────────────────┤
│ (3) Sign payment proof │
│ (4) GET /resource + Authorization: Payment │
├──────────────────────────────────────────────>│
│ (5) Verify + settle │
│ (6) 200 OK + Payment-Receipt │
│<──────────────────────────────────────────────┤
MPP is payment-method agnostic. Each method defines its own settlement rail:
| Method | Rail | SDK Package | Status |
|---|---|---|---|
| Tempo | TIP-20 stablecoins on Tempo chain | mppx (built-in) | Production |
| Stripe | Cards, wallets via Shared Payment Tokens | mppx (built-in) | Production |
| Lightning | Bitcoin over Lightning Network | @buildonspark/lightning-mpp-sdk | Production |
| Stellar | SEP-41 tokens on Stellar | @stellar/mpp | Production |
| Card | Encrypted network tokens (Visa) | mpp-card | Production |
| Custom | Any rail | Method.from() + Method.toClient/toServer | Extensible |
Two payment intents:
| Intent | Pattern | Best For |
|---|---|---|
| charge | One-time payment per request | API calls, content access, fixed-price endpoints |
| session | Pay-as-you-go over payment channels | LLM streaming, metered billing, high-frequency APIs |
import { Mppx, tempo } from 'mppx/server'
const mppx = Mppx.create({
methods: [tempo({
currency: '0x20c0000000000000000000000000000000000000', // pathUSD
recipient: '0xYourAddress',
})],
})
export async function handler(request: Request) {
const result = await mppx.charge({ amount: '0.01' })(request)
if (result.status === 402) return result.challenge
return result.withReceipt(Response.json({ data: '...' }))
}
Install: npm install mppx viem
import { privateKeyToAccount } from 'viem/accounts'
import { Mppx, tempo } from 'mppx/client'
// Polyfills globalThis.fetch to handle 402 automatically
Mppx.create({
methods: [tempo({ account: privateKeyToAccount('0x...') })],
})
const res = await fetch('https://api.example.com/paid')
// Payment happens transparently when server returns 402
from fastapi import FastAPI, Request
from mpp.server import Mpp
from mpp.methods.tempo import tempo, ChargeIntent
app = FastAPI()
mpp = Mpp.create(method=tempo(
currency="0x20c0000000000000000000000000000000000000",
recipient="0xYourAddress", intents={"charge": ChargeIntent()},
))
@app.get("/resource")
async def get_resource(request: Request):
result = await mpp.charge(authorization=request.headers.get("Authorization"), amount="0.50")
if isinstance(result, Challenge):
return JSONResponse(status_code=402, content={"error": "Payment required"},
headers={"WWW-Authenticate": result.to_www_authenticate(mpp.realm)})
return {"data": "paid content"}
Install: pip install "pympp[tempo]". See references/python-sdk.md for full patterns.
Install: cargo add mpp --features tempo,server. See references/rust-sdk.md for full patterns.
Each framework has its own import (mppx/nextjs, mppx/hono, mppx/express, mppx/elysia):
// Next.js
import { Mppx, tempo } from 'mppx/nextjs'
const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0...', recipient: '0x...' })] })
export const GET = mppx.charge({ amount: '0.1' })(() => Response.json({ data: '...' }))
// Hono
import { Mppx, tempo } from 'mppx/hono'
app.get('/resource', mppx.charge({ amount: '0.1' }), (c) => c.json({ data: '...' }))
See references/typescript-sdk.md for Express and Elysia examples.
Sessions open a payment channel once, then use off-chain vouchers for each request - no blockchain transaction per request. Sub-100ms latency, near-zero per-request fees.
// Server - session endpoint
const result = await mppx.session({
amount: '0.001',
unitType: 'token',
})(request)
if (result.status === 402) return result.challenge
return result.withReceipt(Response.json({ data: '...' }))
// Server - SSE streaming with per-word billing
const mppx = Mppx.create({
methods: [tempo({ currency: '0x20c0...', recipient: '0x...', sse: true })],
})
export const GET = mppx.session({ amount: '0.001', unitType: 'word' })(
async () => {
const words = ['hello', 'world']
return async function* (stream) {
for (const word of words) {
await stream.charge()
yield word
}
}
}
)
// Client - session with auto-managed channel
import { Mppx, tempo } from 'mppx/client'
Mppx.create({
methods: [tempo({ account, maxDeposit: '1' })], // Lock up to 1 pathUSD
})
const res = await fetch('http://localhost:3000/api/resource')
// 1st request: opens channel on-chain
// 2nd+ requests: off-chain vouchers (no on-chain tx)
WebSocket transport: Sessions also support WebSocket streaming via Ws.serve(). The WebSocket protocol uses typed messages (mpp: 'authorization', mpp: 'message', mpp: 'payment-close-request', etc.) for payment negotiation alongside data delivery. See references/sessions.md for the full session lifecycle, escrow contracts, SSE patterns, and WebSocket integration.
Accept Tempo stablecoins, Stripe cards, and Lightning Bitcoin on a single endpoint:
import Stripe from 'stripe'
import { Mppx, tempo, stripe } from 'mppx/server'
import { spark } from '@buildonspark/lightning-mpp-sdk/server'
const mppx = Mppx.create({
methods: [
tempo({ currency: '0x20c0...', recipient: '0x...' }),
stripe.charge({ client: new Stripe(key), networkId: 'internal', paymentMethodTypes: ['card'] }),
spark.charge({ mnemonic: process.env.MNEMONIC! }),
],
})
Use compose() to present multiple methods in a single 402 response with per-route pricing. See references/typescript-sdk.md for compose patterns.
Payment links render a browser-friendly payment UI when a 402 endpoint is visited in a browser. Set html: true on the payment method config. Supports theming, multi-method compose (Tempo + Stripe tabs), and Solana wallets.
import { Mppx, tempo } from 'mppx/nextjs'
const mppx = Mppx.create({
methods: [tempo.charge({
currency: '0x20c0...', recipient: '0x...',
html: true, // auto-renders payment page for browsers
})],
})
export const GET = mppx.charge({ amount: '0.1' })(() => Response.json({ data: '...' }))
Customize via mppx/html exports (Config, Text, Theme). Service workers handle credential submission - the page reloads with the paid response. For multi-method compose, tabs auto-switch between payment options.
Authenticate agent identity without payment. Clients sign an EIP-712 proof over the challenge ID instead of creating a transaction - no gas burned, no funds transferred.
// Server - zero-dollar charge (amount: '0')
const result = await mppx.charge({ amount: '0' })(request)
// Enable replay protection (makes each proof single-use)
const mppx = Mppx.create({
methods: [tempo.charge({ currency: '0x20c0...', recipient: '0x...', store: Store.memory() })],
})
Use cases: identity verification, long-running job polling (prove identity once, poll freely), paid unlock with free subsequent access, multi-step agent pipelines. See mpp.dev/advanced/identity for full patterns.
Gate existing APIs behind MPP payments:
import { openai, Proxy } from 'mppx/proxy'
import { Mppx, tempo } from 'mppx/server'
const mppx = Mppx.create({ methods: [tempo()] })
const proxy = Proxy.create({
title: 'My API Gateway',
description: 'Paid access to AI APIs',
services: [
openai({
apiKey: 'sk-...', // pragma: allowlist secret
routes: {
'POST /v1/chat/completions': mppx.charge({ amount: '0.05' }),
'GET /v1/models': mppx.free(), // mppx.free() marks a route as free (no payment)
},
}),
],
})
// Discovery: GET /openapi.json (canonical), GET /llms.txt (legacy /discover* returns 410)
MCP tool calls can require payment using JSON-RPC error code -32042:
// Server - MCP with payment (import tempo from mppx/server, NOT mppx/tempo)
import { McpServer } from 'mppx/mcp-sdk/server'
import { tempo } from 'mppx/server'
const server = McpServer.wrap(baseServer, {
methods: [tempo.charge({ ... })],
secretKey: '...',
})
// Client - payment-aware MCP client (import tempo from mppx/client)
import { McpClient } from 'mppx/mcp-sdk/client'
import { tempo } from 'mppx/client'
const mcp = McpClient.wrap(client, { methods: [tempo({ account })] })
const result = await mcp.callTool({ name: 'premium_tool', arguments: {} })
See references/transports.md for the full MCP encoding (challenge in error.data.challenges, credential in _meta).
Use Privy server wallets as MPP signers for agentic payment flows. The pattern: create a custom viem Account via toAccount() that delegates signMessage, signTransaction, and signTypedData to Privy's API (@privy-io/node), then pass it to tempo({ account }). Tempo's custom serializer requires using signSecp256k1 (raw hash signing) for transactions instead of Privy's higher-level signTransaction.
Install: npm install @privy-io/node mppx viem. See references/typescript-sdk.md for the full implementation, Privy agentic wallets docs, and the demo app.
# Create an account (stored in keychain, auto-funded on testnet)
npx mppx account create
# Make a paid request
npx mppx http://localhost:3000/resource
# Inspect challenge without paying
npx mppx --inspect http://localhost:3000/resource
CLI config file: Extend the CLI with custom payment methods via mppx.config.(js|mjs|ts):
// mppx.config.ts
import { defineConfig } from 'mppx/cli'
export default defineConfig({ plugins: [myCustomMethod()] })
| Language | Package | Install |
|---|---|---|
| TypeScript | mppx | npm install mppx |
| Python | pympp | pip install pympp or pip install "pympp[tempo]" |
| Rust | mpp | cargo add mpp --features tempo,client,server |
| Go | mppx (community) | go get github.com/cp0x-org/mppx |
| Stellar | @stellar/mpp | npm install @stellar/mpp |
TypeScript subpath exports:
mppx/server (generic), mppx/hono, mppx/express, mppx/nextjs, mppx/elysia (framework middleware)mppx/clientmppx/proxymppx/mcp-sdk/server, mppx/mcp-sdk/clientmppx/html (exports Config, Text, Theme types and init() for payment link customization)mppx/discovery (OpenAPI-first discovery tooling)mppx/tempo (exports Session with Session.Sse.iterateData for SSE stream parsing)Always import Mppx and tempo from the appropriate subpath for your context (e.g. mppx/hono for Hono, mppx/server for generic/MCP server, mppx/client for client). Note: Mppx and tempo are NOT exported from mppx/tempo - that subpath only exports Session.
references/protocol-spec.mdcharge (one-time) and session (streaming). See references/sessions.md for session detailshtml: true. Supports theming and multi-method compose tabsproof credential type for identity without payment. Amount '0' triggers EIP-712 proof signing. Add store for replay protectionreferences/transports.mdfeeToken transaction field. Accounts must either set feeToken per-transaction or call setUserToken on the FeeManager precompile to set a default. Without this, transactions fail with gas_limit: 0. See references/tempo-method.mdreferences/tempo-method.mdreferences/tempo-method.mdMethod.from(). See references/custom-methods.mdTempo has no native gas token. Unlike Ethereum (ETH for gas) or Solana (SOL for fees), Tempo charges transaction fees in stablecoins. Every transaction must specify which stablecoin pays for gas. There are two ways:
feeToken - set in the transaction itself:const prepared = await prepareTransactionRequest(client, {
account,
calls: [{ to, data }],
feeToken: '0x20C000000000000000000000b9537d11c60E8b50', // USDC mainnet
} as never)
setUserToken - one-time setup, applies to all future transactions:import { setUserToken } from 'viem/tempo'
await client.fee.setUserTokenSync({
token: '0x20C000000000000000000000b9537d11c60E8b50', // USDC mainnet
})
Without either, transactions fail silently with gas_limit: 0. The mppx SDK handles this internally for payment transactions, but any direct on-chain calls (settle, close, custom contract interactions) must set feeToken explicitly or ensure setUserToken was called for the account.
Fee token addresses:
| Network | Token | Address |
|---|---|---|
| Mainnet | USDC.e (Bridged USDC) | 0x20C000000000000000000000b9537d11c60E8b50 |
| Testnet | pathUSD | 0x20c0000000000000000000000000000000000000 |
Self-payment trap: The payer and recipient cannot be the same wallet address. When testing with npx mppx, create a separate client account (npx mppx account create -a client) and fund it separately.
Recipient wallet initialization: TIP-20 token accounts on Tempo must be initialized before they can receive tokens (similar to Solana ATAs). Send a tiny amount (e.g. 0.01 USDC) to the recipient address first: tempo wallet transfer 0.01 0x20C000000000000000000000b9537d11c60E8b50 <recipient>.
Set realm explicitly for mppscan attribution. The realm value is hashed into Tempo's attribution memo (bytes 5-14 of the 32-byte transferWithMemo data) and is how mppscan correlates on-chain transactions to registered servers. Mppx.create() auto-detects realm from env vars (MPP_REALM, FLY_APP_NAME, HEROKU_APP_NAME, HOST, HOSTNAME, RAILWAY_PUBLIC_DOMAIN, RENDER_EXTERNAL_HOSTNAME, VERCEL_URL, WEBSITE_HOSTNAME). On PaaS platforms (Vercel, Railway, Heroku) these are stable app names and work fine. In Kubernetes, HOSTNAME is the pod name (e.g. web-69d986c8d8-6dtdx) which rotates on every deploy - causing a new server fingerprint each time, so mppscan can't track your transactions. Fix by setting MPP_REALM env var to your stable public domain or passing realm directly:
Mppx.create({
methods: [tempo({ ... })],
realm: 'web.surf.cascade.fyi', // or process.env.MPP_REALM
secretKey,
})
tempo() vs explicit registration: tempo({ ... }) registers both charge and session intents with shared config. When you need different config per intent (e.g. session needs store and sse: { poll: true } but charge doesn't), register them explicitly:
import { Mppx, Store, tempo } from 'mppx/server'
Mppx.create({
methods: [
tempo.charge({ currency, recipient }),
tempo.session({ currency, recipient, store: Store.memory(), sse: { poll: true } }),
],
secretKey,
})
Hono multiple headers: c.header(name, value) replaces by default. When emitting multiple WWW-Authenticate values (e.g. charge + session intents), the second call silently overwrites the first. Prefer using mppx.compose() which handles multi-header emission correctly. If composing manually, use { append: true }:
c.header('WWW-Authenticate', chargeWwwAuth)
c.header('WWW-Authenticate', sessionWwwAuth, { append: true })
CORS headers: WWW-Authenticate and Payment-Receipt must be listed in access-control-expose-headers or browsers/clients won't see them.
SSE utilities import path: Session.Sse.iterateData is exported from mppx/tempo, NOT mppx/server:
import { Mppx, Store, tempo } from 'mppx/server'
import { Session } from 'mppx/tempo'
const iterateSseData = Session.Sse.iterateData
Never use Store.memory() in production. It loses all channel state on server restart/redeploy. When state is lost, the server can't close channels or settle funds - client deposits stay locked in escrow indefinitely. Use a persistent store.
Built-in store adapters (all handle BigInt serialization via ox's Json module):
import { Store } from 'mppx/server'
Store.memory() // development only
Store.redis(redisClient) // ioredis, node-redis, Valkey (added in 0.4.9)
Store.upstash(upstashClient) // Upstash Redis / Vercel KV
Store.cloudflare(kvNamespace) // Cloudflare KV
Store.from({ get, put, delete }) // custom adapter
AtomicStore (0.5.7+): Extends Store with an update(key, fn) method for safe concurrent read-modify-write. Used internally for replay protection and channel state. All built-in adapters (redis, upstash, cloudflare) support atomic updates. Custom adapters via Store.from() get an optimistic-retry implementation automatically.
Polling mode: If your store doesn't implement the optional waitForUpdate() method (e.g. custom adapters via Store.from()), pass sse: { poll: true } to tempo.session(). Otherwise SSE streams will hang waiting for event-driven wakeups that never come.
Pass channelId to mppx.session() so returning clients recover existing on-chain channels instead of opening new ones (which locks more funds in escrow). See references/sessions.md for the full pattern with Credential.fromRequest() and tryRecoverChannel().
Session voucher POSTs have no body. Mid-stream voucher POSTs carry only Authorization: Payment - no JSON body. If your middleware decides charge vs session based on body.stream, vouchers will hit the charge path. Check the credential's intent instead. As of mppx 0.4.9, the SDK skips route amount/currency/recipient validation for topUp and voucher credentials (the on-chain voucher signature is the real validation), so body-derived pricing mismatches no longer cause spurious 402 rejections.
Clone the request before reading the body. request.json() consumes the Request body. If you parse the body first and then pass the original request to mppx.session() or mppx.charge(), the mppx handler gets an empty body and returns 402. Clone before reading.
Cheap model zero-charge floor: Tempo USDC has 6-decimal precision. For very cheap models, per-token cost like (0.10 / 1_000_000) * 1.3 = 0.00000013 rounds to "0.000000" via toFixed(6) - effectively zero. Add a minimum tick cost floor:
const MIN_TICK_COST = 0.000001 // smallest Tempo USDC unit (6 decimals)
const tickCost = Math.max((outputRate / 1_000_000) * margin, MIN_TICK_COST)
SSE chunks != tokens: Per-SSE-event stream.charge() is an acceptable approximation. stream.charge() is serial (Redis GET + SET per call, per-channelId mutex) - no bulk API exists yet.
Add upstream timeouts: Always use AbortSignal.timeout() on upstream fetches. A stalled upstream holds the payment channel open, locking client funds.
Nginx proxy buffer overflow: Large 402 headers can exceed nginx's default 4k proxy_buffer_size, causing 502 Bad Gateway. Fix: nginx.ingress.kubernetes.io/proxy-buffer-size: "16k". Debug: port-forward directly to the pod - if you get 402, the issue is in the ingress layer.
CLI defaults to mainnet (0.5.4+): The mppx CLI now defaults to Tempo mainnet when --rpc-url is omitted. Previously it defaulted to testnet. Use --rpc-url or set MPPX_RPC_URL/RPC_URL env vars for testnet.
Stale sessions after redeploy: When the server redeploys and loses in-memory session state, clients get "Session invalidation claim for channel 0x... was not confirmed on-chain". Fix: tempo wallet sessions close or tempo wallet sessions sync. Dispute window is 4-15 min.
| File | Content |
|---|---|
references/protocol-spec.md | Core protocol: Challenge/Credential/Receipt structure, status codes, error handling, security, caching, extensibility |
references/typescript-sdk.md | mppx TypeScript SDK: server/client/middleware, proxy, MCP SDK, CLI, AtomicStore, Privy wallets |
references/tempo-method.md | Tempo: charge + session, fee sponsorship, push/pull, auto-swap, split payments, config |
references/stripe-method.md | Stripe payment method: SPT flow, server/client config, Stripe Elements, createToken proxy, metadata |
references/sessions.md | Sessions: payment channels, vouchers, SSE/WebSocket streaming, escrow, channel recovery |
references/transports.md | HTTP, MCP, and WebSocket transport bindings: header/message encoding, comparison |
references/python-sdk.md | pympp Python SDK: FastAPI/server patterns, async client, streaming sessions |
references/rust-sdk.md | mpp Rust SDK: server/client, feature flags, reqwest middleware |
references/lightning-method.md | Lightning payment method: charge (BOLT11), session (bearer tokens), Spark SDK |
references/custom-methods.md | Custom payment methods: Method.from, Method.toClient, Method.toServer patterns |