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:
- Create an EVM wallet on Tempo Moderato testnet with Dynamic’s Node SDK
- Fund it with test stablecoins from Tempo’s faucet
- Wire up a
viem LocalAccount backed by Dynamic’s MPC signing that understands Tempo’s custom transaction format
- 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:
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());
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