@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-httpauth-payment-00,
not an IETF standard. This package uses pinned mppx 0.8.14 and implements
native Tempo charge/session, Stripe charge, EVM charge, and custom-verifier
subsets over Lucid HTTP routes; it is not the complete MPP transport, rail,
session, or subscription surface.
Installation
bun add @lucid-agents/mpp @lucid-agents/core @lucid-agents/httpThe 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. Use the separate
tempo.session() descriptor for TIP-1034: it requires a signing server account,
an explicit deposit bound, and a durable SQLite/Postgres store in production.
Set a stable, high-entropy MPP_SECRET_KEY in production. When it is omitted,
Lucid generates a new key for each process. The default challenge/replay store
is process-local; inject the SQLite/Postgres challengeStore adapter, or a
custom atomic implementation, for restart recovery and multiple workers.
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 operation, and are atomically leased before asynchronous verification. Lucid renews the active lease and fences consumption, so concurrent replay 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. For native Tempo metering, configure tempo.session()
instead; invoke deducts one unit and SSE reconciles delivered units. Native
Tempo sessions do not support Lucid task admission.
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.
For complete charge and session setup, follow Every MPP payment method.
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.