Enable AI agents to pay for APIs and content using MPP, an open protocol for machine-to-machine payments over HTTP. Privy’s server wallets provide the signing layer, while the mppx SDK handles the 402 payment flow automatically.
What is MPP?
MPP (Machine Payments Protocol) is an open protocol for machine-to-machine payments over HTTP. When a resource requires payment, the server responds with 402 Payment Required and payment details. The client signs a payment credential using the agent’s wallet and retries the request. Settlement happens on the Tempo blockchain using PathUSD.
Installation
npm install @privy-io/node mppx viem
@privy-io/node provides server-side wallet creation and signing
mppx provides the MPP client that handles 402 payment flows
viem provides Ethereum utilities for creating a compatible account interface
Creating a Privy-backed signer
MPP’s tempo() payment method expects a viem Account for signing. Since Privy wallets are server-managed, your app creates a custom viem account that delegates signing to Privy’s API.
import {PrivyClient} from '@privy-io/node';
import {toAccount} from 'viem/accounts';
import {keccak256} from 'viem';
const privy = new PrivyClient({
appId: process.env.PRIVY_APP_ID,
appSecret: process.env.PRIVY_APP_SECRET
});
function createPrivyAccount(walletId: string, address: `0x${string}`) {
async function signHash(hash: `0x${string}`): Promise<`0x${string}`> {
const result = await privy.wallets().ethereum().signSecp256k1(walletId, {params: {hash}});
return result.signature as `0x${string}`;
}
return toAccount({
address,
async signMessage({message}) {
const result = await privy
.wallets()
.ethereum()
.signMessage(walletId, {
message: typeof message === 'string' ? message : message.raw
});
return result.signature as `0x${string}`;
},
async signTransaction(transaction, options) {
const serializer = options?.serializer;
if (!serializer) {
throw new Error('Tempo serializer required');
}
const unsignedSerialized = await serializer(transaction);
const hash = keccak256(unsignedSerialized);
const signature = await signHash(hash as `0x${string}`);
const {SignatureEnvelope} = await import('ox/tempo');
const envelope = SignatureEnvelope.from(signature);
return (await serializer(transaction, envelope)) as `0x${string}`;
},
async signTypedData(typedData) {
const result = await privy.wallets().ethereum().signTypedData(walletId, {params: typedData});
return result.signature as `0x${string}`;
}
});
}
The account implements three signing methods:
| Method | Privy API | Purpose |
|---|
signMessage | wallets().ethereum().signMessage() | EIP-191 personal signatures |
signTransaction | wallets().ethereum().signSecp256k1() | Tempo transaction signing |
signTypedData | wallets().ethereum().signTypedData() | EIP-712 typed data signatures |
Tempo transactions use a custom serialization format. The signTransaction method uses the
Tempo-provided serializer and Privy’s raw signSecp256k1 endpoint rather than the higher-level
signTransaction API.
Making MPP payments
Pass the Privy-backed account to the MPP client’s tempo() method. The client automatically handles 402 responses, signs payment credentials, and retries requests.
Using mppx.fetch
import {Mppx, tempo} from 'mppx/client';
async function makePayment(walletId: string, address: `0x${string}`, url: string) {
const account = createPrivyAccount(walletId, address);
const mppx = Mppx.create({
polyfill: false,
methods: [tempo({account})]
});
const response = await mppx.fetch(url);
const data = await response.json();
return data;
}
mppx.fetch is a drop-in replacement for fetch. When a server returns 402 Payment Required, the client reads the payment requirements, signs a credential with the Privy wallet, and retries the request automatically.
Using polyfill mode
Your app can also polyfill the global fetch so all HTTP requests handle 402 challenges automatically:
import {Mppx, tempo} from 'mppx/client';
const account = createPrivyAccount(walletId, address);
Mppx.create({
polyfill: true,
methods: [tempo({account})]
});
// All fetch calls now handle 402 responses automatically
const response = await fetch('https://api.example.com/weather');
How it works
- Agent requests content: Your app calls
mppx.fetch() or the polyfilled fetch()
- Server responds 402: Returns payment requirements (amount, currency, recipient)
- MPP client signs credential: Uses the Privy-backed account to sign a payment credential
- Retry with credential: Request repeats with the signed credential attached
- Server verifies and settles: Verifies the credential and settles payment on Tempo
- Server delivers: Returns content with
200 OK
Creating an MPP-enabled service
The mppx/nextjs package provides middleware for adding payment requirements to API routes:
// app/api/weather/route.ts
import {Mppx, tempo} from 'mppx/nextjs';
const mppx = Mppx.create({
methods: [
tempo.charge({
currency: '0x20c0000000000000000000000000000000000000', // PathUSD
recipient: process.env.MPP_RECIPIENT as `0x${string}`
})
]
});
export const GET = mppx.charge({amount: '0.1'})(() =>
Response.json({
temperature: 72,
condition: 'Sunny',
location: 'San Francisco, CA'
})
);
When a client calls this route without a payment credential, the middleware responds with 402 Payment Required. With a valid credential, it verifies the payment, settles on Tempo, and returns the data.
Full example
import {PrivyClient} from '@privy-io/node';
import {Mppx, tempo} from 'mppx/client';
import {toAccount} from 'viem/accounts';
import {keccak256} from 'viem';
const privy = new PrivyClient({
appId: process.env.PRIVY_APP_ID!,
appSecret: process.env.PRIVY_APP_SECRET!
});
// 1. Create a wallet for the agent
const wallet = await privy.wallets().create({chain_type: 'ethereum'});
// 2. Build a viem account backed by Privy
function createPrivyAccount(walletId: string, address: `0x${string}`) {
async function signHash(hash: `0x${string}`): Promise<`0x${string}`> {
const result = await privy.wallets().ethereum().signSecp256k1(walletId, {params: {hash}});
return result.signature as `0x${string}`;
}
return toAccount({
address,
async signMessage({message}) {
const result = await privy
.wallets()
.ethereum()
.signMessage(walletId, {
message: typeof message === 'string' ? message : message.raw
});
return result.signature as `0x${string}`;
},
async signTransaction(transaction, options) {
const serializer = options?.serializer;
if (!serializer) throw new Error('Tempo serializer required');
const unsignedSerialized = await serializer(transaction);
const hash = keccak256(unsignedSerialized);
const signature = await signHash(hash as `0x${string}`);
const {SignatureEnvelope} = await import('ox/tempo');
const envelope = SignatureEnvelope.from(signature);
return (await serializer(transaction, envelope)) as `0x${string}`;
},
async signTypedData(typedData) {
const result = await privy.wallets().ethereum().signTypedData(walletId, {params: typedData});
return result.signature as `0x${string}`;
}
});
}
const account = createPrivyAccount(wallet.id, wallet.address as `0x${string}`);
// 3. Create the MPP client
const mppx = Mppx.create({
polyfill: false,
methods: [tempo({account})]
});
// 4. Make a paid request
const response = await mppx.fetch('https://api.example.com/weather');
const weather = await response.json();
Learn more