@lucid-agents/mpp
Standard Machine Payments Protocol authorization with native and custom verifiers.
The MPP extension protects invoke, stream, and Lucid task execution with
Payment-Auth 402 Payment Required challenges. Every adapter delegates to the
same authorization gate.
MPP is currently the individual Internet-Draft
draft-ryan-httpauth-payment-01, not an IETF standard. This package uses
mppx 0.4.11 and implements the documented Tempo, Stripe, and custom-verifier
subset over Lucid HTTP routes; it is not the complete MPP transport,
discovery, rail, session, or subscription surface.
Installation
bun add @lucid-agents/mpp @lucid-agents/core @lucid-agents/httpThis command applies after @lucid-agents/mpp is published. As of the
verification date, MPP is a Next workspace package and is not part of the
clean-room Stable npm set. Do not add it to a Stable lockfile yet.
The package includes mppx. Tempo requires Viem >=2.47.5, declared as a peer dependency.
Tempo setup
import { createAgent } from '@lucid-agents/core';
import { http } from '@lucid-agents/http';
import { mpp, tempo } from '@lucid-agents/mpp';
const agent = await createAgent({
name: 'merchant',
version: '1.0.0',
})
.use(
mpp({
config: {
methods: [
tempo.server({
currency: '0x20c0000000000000000000000000000000000000',
recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
}),
],
secretKey: process.env.MPP_SECRET_KEY,
},
})
)
.use(http())
.addEntrypoint({
key: 'report',
price: '0.05',
paymentProtocol: 'mpp',
async handler() {
return { output: { report: '...' } };
},
})
.build();tempo.server() is materialized as a native mppx charge method. mppx verifies the echoed HMAC challenge, credential schema, transaction, amount, recipient, and settlement before Lucid admits the request. Native mppx sessions require a server signing account that Lucid's current TempoServerConfig does not expose; use a custom method and verifier for session intents.
Set a stable, high-entropy MPP_SECRET_KEY in production. When it is omitted, Lucid generates a new key for each process. Lucid's outstanding-challenge and replay registry is also process-local, so paid retries require sticky routing and do not survive a restart even when the key is configured.
There is currently no injectable durable MPP challenge store. A multi-worker or restart-safe conformance claim would be incorrect until that capability exists.
Stripe setup
import { stripe } from '@lucid-agents/mpp';
stripe.server({
secretKey: process.env.STRIPE_SECRET_KEY!,
networkId: process.env.MPP_STRIPE_NETWORK_ID!,
currency: 'usd',
decimals: 2,
paymentMethodTypes: ['card'],
});Stripe verification is also delegated to its native mppx server method. Human-readable entrypoint prices are converted to base units using the method's decimals value.
Custom verification
Custom and Lightning descriptors cannot prove payment by themselves. They require verifyCredential:
import { custom, mpp } from '@lucid-agents/mpp';
const extension = mpp({
config: {
methods: [
custom.server('acme-pay', {
merchantId: 'merchant-42',
}),
],
currency: 'usd',
defaultIntent: 'charge',
async verifyCredential({ credential, requirement }) {
const verification = await verifyWithAcme({
challenge: credential.challenge,
payload: credential.payload,
amount: requirement.amount,
});
return verification.settled
? {
valid: true,
receipt: verification.receipt,
payer: verification.payer,
network: verification.network,
}
: { valid: false, reason: 'Payment was not settled' };
},
},
});The application verifier is the trust boundary for custom methods. It must validate the signature, amount, currency, recipient, method, settlement, and asserted payer. A custom method without a verifier always fails closed.
The verifier receives:
- the original
Request; - the canonical entrypoint and invoke/stream kind;
- the resolved amount, currency, intent, and allowed methods;
- the decoded standard credential, including its full challenge, payload, and optional payer DID.
Return cryptographically verified payer and network values so the shared payments runtime can enforce incoming sender, total, and rate policies. MPP never treats Origin, Referer, or another caller-controlled header as payer identity.
Verification occurs before target-side idempotency replay. If a custom verifier performs an externally visible settlement, it must deduplicate that operation with Idempotency-Key. Lucid's policy reservation and accounting happen only after the request wins a new target-side claim.
Wire and replay contract
Lucid emits the standard challenge form consumed by mppx clients:
HTTP/1.1 402 Payment Required
WWW-Authenticate: Payment id="...", realm="...", method="tempo", intent="charge", request="...", expires="..."The client retries with:
Authorization: Payment <base64url-credential>Challenge IDs are bounded, expire, bind to the full challenge plus entrypoint and invoke/stream mode, and are consumed before asynchronous verification. Concurrent replay therefore cannot race the verifier. Malformed, unknown, expired, wrong-target, replayed, and rejected credentials fail closed.
A successful application response includes Payment-Receipt.
decodeMppCredential() is decode-only. Never treat successful decoding as authorization.
Pricing and overrides
.addEntrypoint({
key: 'session',
price: {
invoke: '0.001',
stream: '0.0001',
},
paymentProtocol: 'mpp',
metadata: {
mpp: {
intent: 'session',
methods: ['acme-session'],
description: 'Metered research session',
},
},
async handler() {
return { output: {} };
},
async stream(_context, emit) {
await emit({
kind: 'delta',
delta: 'result',
mime: 'text/plain',
});
return { status: 'succeeded' };
},
})The acme-session descriptor must be configured through custom.server() with
an application verifier. Lucid does not materialize a native Tempo session
without the server signing account required by mppx 0.4.
Entrypoint metadata may override intent, amount, currency, methods, and description. The selected method must support the chosen intent; otherwise authorization returns a configuration error without executing the entrypoint.
If x402 and MPP are both installed, paymentProtocol is required on every priced entrypoint. The shared gate rejects ambiguous configuration.
Environment configuration
mppFromEnv(overrides?) creates built-in descriptors and preserves an explicitly supplied custom verifier:
import { mpp, mppFromEnv } from '@lucid-agents/mpp';
const extension = mpp({
config: mppFromEnv({
async verifyCredential(context) {
return verifyCustomPayment(context);
},
}),
});Relevant variables include:
MPP_METHOD,MPP_CURRENCY, andMPP_DEFAULT_INTENT;MPP_CHALLENGE_EXPIRY,MPP_SECRET_KEY, andMPP_REALM;MPP_TEMPO_CURRENCY,MPP_TEMPO_RECIPIENT, andMPP_TEMPO_CHAIN_ID;MPP_STRIPE_SECRET_KEY(orSTRIPE_SECRET_KEY) andMPP_STRIPE_NETWORK_ID.
Missing required method variables cause that method to be omitted. Building an MPP runtime with no methods is a startup error.
Outbound MPP calls
Pass native method intents from mppx/client. Lucid returns mppx's payment-aware Fetch function:
import { tempo } from 'mppx/client';
import { privateKeyToAccount } from 'viem/accounts';
const paidFetch = await agent.mpp.getMppFetch({
methods: [
tempo({
account: privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`),
maxDeposit: '10',
}),
],
});
const response = await paidFetch?.(
'https://merchant.example/entrypoints/report/invoke',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ input: {} }),
}
);getMppFetch() uses polyfill: false, so it never replaces globalThis.fetch. Pass fetch to wrap a custom implementation. Do not pass server descriptors or server secrets to the client API.
Shared payment policies and SIWX
When @lucid-agents/payments is installed:
- a verified SIWX entitlement can bypass an MPP challenge;
- verified MPP payments enter the same incoming policy admission and atomic accounting path as x402;
- verified payer and network data drive per-sender policy checks;
- protocol receipts and SIWX response metadata are composed onto the final response.
Runtime API
type MppRuntime = {
readonly config: MppConfig;
readonly isActive: boolean;
requirements(entrypoint, kind): MppPaymentRequirement;
activate(entrypoint): void;
resolvePrice(entrypoint, kind): string | null;
authorize(
request,
entrypoint,
kind,
requirement?
): Promise<MppAuthorizationResult>;
getMppFetch(config: MppClientConfig): Promise<FetchFunction | null>;
};The MPP package owns this runtime directly; core and adapters do not wrap or reinterpret it.
Exports
export {
mpp,
mppFromEnv,
tempo,
stripe,
lightning,
custom,
buildChallengeSet,
buildChallengeResponse,
resolveEntrypointPrice,
resolveEntrypointMppConfig,
decodeMppCredential,
createReceiptHeader,
buildManifestWithMpp,
} from '@lucid-agents/mpp';MPP contracts, including MppConfig, MppRuntime, MppCredentialVerifier, and MppClientConfig, are defined only in @lucid-agents/types/mpp and are not re-exported.