Skip to main content

Overview

Tempo is a blockchain built for machine-to-machine payments. Its Machine Payment Protocol (MPP) extends the HTTP 402 “Payment Required” status code so that agents and servers can automatically pay for API access in a single round-trip — no checkout flow, no user accounts, no manual approval. This recipe shows the minimum code to:
  1. Create an EVM wallet on Tempo Moderato testnet with Dynamic’s Node SDK
  2. Fund it with test stablecoins from Tempo’s faucet
  3. Wire up a viem LocalAccount backed by Dynamic’s MPC signing that understands Tempo’s custom transaction format
  4. Use mppx to make a paid request to any MPP-protected endpoint
For a full example, see the tempo-ppm-integration example on GitHub. For how HTTP 402 payment flows work in general—and how MPP relates to the separate x402 stack—see the HTTP 402 overview.

Prerequisites

  • Node.js 22+
  • Dynamic Environment ID and API token — find these in the Dynamic Dashboard
  • Embedded wallets enabled in your Dynamic dashboard

Setup

Install the required packages:
npm install @dynamic-labs-wallet/node-evm mppx viem
Create a .env file with your credentials:
.env
DYNAMIC_ENVIRONMENT_ID=your_environment_id
DYNAMIC_AUTH_TOKEN=your_api_token
Use node --env-file=.env to load the file automatically, or a library like dotenv.

Step 1: Initialize the Dynamic wallet client

import { DynamicEvmWalletClient } from '@dynamic-labs-wallet/node-evm';

const client = new DynamicEvmWalletClient({
  environmentId: process.env.DYNAMIC_ENVIRONMENT_ID!,
});
await client.authenticateApiToken(process.env.DYNAMIC_AUTH_TOKEN!);

Step 2: Create a wallet

Wallets are created with a 2-of-2 threshold signature scheme (MPC). The returned externalServerKeyShares are your signing credentials — store them securely alongside the wallet address.
const result = await client.createWalletAccount({
  thresholdSignatureScheme: 'TWO_OF_TWO',
  backUpToClientShareService: true,
});

const walletAddress = result.accountAddress;
const keyShares = result.externalServerKeyShares;

console.log('Wallet created:', walletAddress);

Step 3: Fund with test stablecoins

Tempo’s Moderato testnet faucet distributes test stablecoins (pathUSD, AlphaUSD, BetaUSD, ThetaUSD). These are used as payment tokens for MPP requests.
const FAUCET_URL = 'https://docs.tempo.xyz/api/faucet';

const res = await fetch(FAUCET_URL, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ address: walletAddress.toLowerCase() }),
});

if (!res.ok) throw new Error(`Faucet error: ${await res.text()}`);
console.log('Funded:', await res.json());
You can also visit docs.tempo.xyz to request tokens manually.

Step 4: Build a Tempo-compatible viem account

Dynamic’s Node SDK signs transactions using its internal viem serializer, which doesn’t understand Tempo’s custom transaction format. You need to create a LocalAccount adapter that uses Tempo’s serializer instead.
import { toAccount } from 'viem/accounts';
import { Transaction as TempoTx } from 'viem/tempo';
import type { Address, Hex, SignableMessage } from 'viem';

function createDynamicTempoAccount(address: string, keyShares: any[]) {
  return toAccount({
    address: address as Address,

    async signMessage({ message }: { message: SignableMessage }): Promise<Hex> {
      const msg = typeof message === 'string' ? message : message;
      return client.signMessage({
        message: msg as string,
        accountAddress: address,
        externalServerKeyShares: keyShares,
      });
    },

    async signTransaction(transaction: any, options?: any): Promise<Hex> {
      const serializer = options?.serializer ?? TempoTx.serialize;

      // 1. Serialize the unsigned transaction with Tempo's serializer
      const serializedTx = await serializer(transaction);
      const serializedTxBytes = Uint8Array.from(
        Buffer.from((serializedTx as string).slice(2), 'hex'),
      );

      // 2. Sign raw bytes via Dynamic's MPC
      const signatureEcdsa = await (client as any).sign({
        message: serializedTxBytes,
        accountAddress: address,
        chainName: 'EVM',
        externalServerKeyShares: keyShares,
      });

      // 3. Re-serialize with the ECDSA signature components
      const r = `0x${Buffer.from(signatureEcdsa.r).toString('hex')}` as Hex;
      const s = `0x${Buffer.from(signatureEcdsa.s).toString('hex')}` as Hex;
      const yParity = BigInt(signatureEcdsa.v) === 27n ? 0 : 1;

      return (await serializer(transaction, { r, s, yParity })) as Hex;
    },

    async signTypedData(typedData: any): Promise<Hex> {
      return client.signTypedData({
        accountAddress: address,
        typedData,
        externalServerKeyShares: keyShares,
      });
    },
  });
}

const account = createDynamicTempoAccount(walletAddress, keyShares);

Step 5: Make an MPP payment

Initialize mppx with the Tempo payment method and use mppx.fetch() in place of the global fetch for any 402-protected URL. The client handles the negotiation, signs the payment, and resends the request automatically.
import { Mppx, tempo } from 'mppx/client';

// polyfill: false prevents mppx from wrapping globalThis.fetch,
// which can interfere with other API clients in the same process.
const mppx = Mppx.create({
  methods: [tempo({ account })],
  polyfill: false,
});

const response = await mppx.fetch('https://mpp.dev/api/ping/paid');

console.log('Status:', response.status);
console.log('Body:', await response.text());

// The payment receipt is returned in a response header
const receipt = response.headers.get('x-payment-receipt');
if (receipt) console.log('Receipt:', receipt);
https://mpp.dev/api/ping/paid is a public test endpoint that accepts any valid MPP payment. Use it to verify your setup before pointing at a real resource.

Putting it all together

import { DynamicEvmWalletClient } from '@dynamic-labs-wallet/node-evm';
import { toAccount } from 'viem/accounts';
import { Transaction as TempoTx } from 'viem/tempo';
import { Mppx, tempo } from 'mppx/client';
import type { Address, Hex, SignableMessage } from 'viem';

// 1. Authenticate
const client = new DynamicEvmWalletClient({
  environmentId: process.env.DYNAMIC_ENVIRONMENT_ID!,
});
await client.authenticateApiToken(process.env.DYNAMIC_AUTH_TOKEN!);

// 2. Create wallet
const { accountAddress, externalServerKeyShares } = await client.createWalletAccount({
  thresholdSignatureScheme: 'TWO_OF_TWO',
  backUpToClientShareService: true,
});
console.log('Wallet:', accountAddress);

// 3. Fund from faucet
const faucetRes = await fetch('https://docs.tempo.xyz/api/faucet', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ address: accountAddress.toLowerCase() }),
});
if (!faucetRes.ok) throw new Error(`Faucet: ${await faucetRes.text()}`);

// 4. Build Tempo account adapter
const account = toAccount({
  address: accountAddress as Address,
  async signMessage({ message }: { message: SignableMessage }) {
    return client.signMessage({
      message: message as string,
      accountAddress,
      externalServerKeyShares,
    });
  },
  async signTransaction(transaction: any, options?: any) {
    const serializer = options?.serializer ?? TempoTx.serialize;
    const serializedTx = await serializer(transaction);
    const bytes = Uint8Array.from(Buffer.from((serializedTx as string).slice(2), 'hex'));
    const sig = await (client as any).sign({
      message: bytes,
      accountAddress,
      chainName: 'EVM',
      externalServerKeyShares,
    });
    const r = `0x${Buffer.from(sig.r).toString('hex')}` as Hex;
    const s = `0x${Buffer.from(sig.s).toString('hex')}` as Hex;
    const yParity = BigInt(sig.v) === 27n ? 0 : 1;
    return (await serializer(transaction, { r, s, yParity })) as Hex;
  },
  async signTypedData(typedData: any) {
    return client.signTypedData({ accountAddress, typedData, externalServerKeyShares });
  },
});

// 5. Make an MPP payment
const mppx = Mppx.create({ methods: [tempo({ account })], polyfill: false });
const response = await mppx.fetch('https://mpp.dev/api/ping/paid');
console.log('Status:', response.status);
console.log(await response.text());

Additional Resources