Skip to main content
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:
MethodPrivy APIPurpose
signMessagewallets().ethereum().signMessage()EIP-191 personal signatures
signTransactionwallets().ethereum().signSecp256k1()Tempo transaction signing
signTypedDatawallets().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

  1. Agent requests content: Your app calls mppx.fetch() or the polyfilled fetch()
  2. Server responds 402: Returns payment requirements (amount, currency, recipient)
  3. MPP client signs credential: Uses the Privy-backed account to sign a payment credential
  4. Retry with credential: Request repeats with the signed credential attached
  5. Server verifies and settles: Verifies the credential and settles payment on Tempo
  6. 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