> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dynamic.xyz/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Machine Payments with MPP (Node.js)

> Create a server wallet, fund it, and make MPP payments to 402-protected APIs on Tempo or Solana using Dynamic's Node SDK

## Overview

The [Machine Payments Protocol (MPP)](https://mpp.dev) 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 a server wallet with Dynamic's Node SDK (Tempo or Solana)
2. Fund it for test payments
3. Adapt Dynamic's MPC signing to the MPP client for that chain
4. Use `mppx` to make a paid request to any MPP-protected endpoint

On **Tempo**, settlement uses Tempo's custom transaction format via `mppx`'s `tempo` method. On **Solana**, settlement uses native SOL or SPL tokens via [`@solana/mpp`](https://mpp.dev/payment-methods/solana/).

For a full Tempo example, see the [tempo-mpp-example on GitHub](https://github.com/dynamic-labs-oss/tempo-mpp-example).

For how HTTP 402 payment flows work in general, and how MPP relates to the separate [x402](https://docs.dynamic.xyz/docs/recipes/integrations/x402/implementation.md) stack, see the [HTTP 402 overview](https://docs.dynamic.xyz/docs/recipes/integrations/x402/overview.md).

***

## Prerequisites

* Node.js 22+
* Dynamic Environment ID and API token. Find these in the [Dynamic Console](https://console.dynamic.xyz/dashboard/developer/api)
* Embedded wallets enabled in your Dynamic Console
* For Solana: Solana enabled under [Chains](https://console.dynamic.xyz/dashboard/chains-and-networks)

***

## Setup

Install the packages for your chain:

> [tab] Tempo
>
> ```bash npm theme={"system"}
> npm install @dynamic-labs-wallet/node-evm mppx viem
> ```
>
> ```bash pnpm theme={"system"}
> pnpm add @dynamic-labs-wallet/node-evm mppx viem
> ```
>
> ```bash yarn theme={"system"}
> yarn add @dynamic-labs-wallet/node-evm mppx viem
> ```

> [tab] Solana
>
> ```bash npm theme={"system"}
> npm install @dynamic-labs-wallet/node-svm @dynamic-labs-wallet/node @solana/mpp @solana/kit @solana/web3.js mppx
> ```
>
> ```bash pnpm theme={"system"}
> pnpm add @dynamic-labs-wallet/node-svm @dynamic-labs-wallet/node @solana/mpp @solana/kit @solana/web3.js mppx
> ```
>
> ```bash yarn theme={"system"}
> yarn add @dynamic-labs-wallet/node-svm @dynamic-labs-wallet/node @solana/mpp @solana/kit @solana/web3.js mppx
> ```
>
> > [!NOTE]
> >
> > `@solana/mpp` is the Solana payment method for `mppx`. See the [Solana charge docs](https://mpp.dev/payment-methods/solana/charge).

Create a `.env` file with your credentials:

> [tab] Tempo
>
> ```env .env theme={"system"}
> DYNAMIC_ENVIRONMENT_ID=your_environment_id
> DYNAMIC_AUTH_TOKEN=your_api_token
> ```

> [tab] Solana
>
> ```env .env theme={"system"}
> DYNAMIC_ENVIRONMENT_ID=your_environment_id
> DYNAMIC_AUTH_TOKEN=your_api_token
> WALLET_PASSWORD=your_secure_password
> SOLANA_RPC_URL=https://402.surfnet.dev:8899
> SOLANA_NETWORK=localnet
> ```
>
> > [!NOTE]
> >
> > Public Solana [devnet](https://api.devnet.solana.com) airdrops are often rate-limited. For a reliable test RPC with airdrops, use the [Surfpool](https://402.surfnet.dev) sandbox (`localnet`) as shown above.

> [!NOTE]
>
> Use `node --env-file=.env` to load the file automatically, or a library like `dotenv`.

***

## Step 1: Initialize the Dynamic wallet client

> [tab] Tempo
>
> ```ts theme={"system"}
> 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!);
>
> ```

> [tab] Solana
>
> ```ts theme={"system"}
> import { DynamicSvmWalletClient } from '@dynamic-labs-wallet/node-svm';
>
> const client = new DynamicSvmWalletClient({
>   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). Persist the returned metadata (and key shares, when you hold them) securely alongside the wallet address.

> [tab] Tempo
>
> ```ts theme={"system"}
> const result = await client.createWalletAccount({
>   thresholdSignatureScheme: 'TWO_OF_TWO',
>   backUpToClientShareService: true,
> });
>
> const walletAddress = result.accountAddress;
> const keyShares = result.externalServerKeyShares;
>
> console.log('Wallet created:', walletAddress);
>
> ```

> [tab] Solana
>
> ```ts theme={"system"}
> import { ThresholdSignatureScheme } from '@dynamic-labs-wallet/node';
>
> const { accountAddress, externalServerKeyShares } = await client.createWalletAccount({
>   thresholdSignatureScheme: ThresholdSignatureScheme.TWO_OF_TWO,
>   password: process.env.WALLET_PASSWORD!,
>   backUpToClientShareService: true,
> });
>
> console.log('Wallet created:', accountAddress);
> // Persist accountAddress and externalServerKeyShares for later runs.
> ```

***

## Step 3: Fund the wallet

> [tab] Tempo
>
> Tempo's Moderato testnet faucet distributes test stablecoins (pathUSD, AlphaUSD, BetaUSD, ThetaUSD). These are used as payment tokens for MPP requests.
>
> ```ts theme={"system"}
> 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());
> ```
>
> > [!NOTE]
> >
> > You can also visit [docs.tempo.xyz](https://docs.tempo.xyz/quickstart/faucet) to request tokens manually.

> [tab] Solana
>
> Request a SOL airdrop for fees and native SOL charges. If the endpoint charges an SPL token (for example USDC), fund that mint as well. When the server sponsors fees (`feePayer`), the wallet may not need SOL for gas, but it still needs a balance (and usually an associated token account) for the payment asset.
>
> ```ts theme={"system"}
> import { Connection, PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js';
>
> const rpcUrl = process.env.SOLANA_RPC_URL ?? 'https://402.surfnet.dev:8899';
> const connection = new Connection(rpcUrl, 'confirmed');
>
> const airdropSig = await connection.requestAirdrop(
>   new PublicKey(accountAddress),
>   LAMPORTS_PER_SOL,
> );
> await connection.confirmTransaction(airdropSig, 'confirmed');
> console.log('Funded with 1 SOL');
> ```

***

## Step 4: Build a chain-compatible signer

> [tab] Tempo
>
> 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.
>
> ```ts theme={"system"}
> 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);
> ```

> [tab] Solana
>
> `@solana/mpp` expects an `@solana/kit` `TransactionSigner`. Dynamic's Node SVM SDK signs [web3.js](https://solana-labs.github.io/solana-web3.js/) transactions and returns a base58 signature. Create a `TransactionPartialSigner` that converts the Kit transaction message, signs with Dynamic, and returns the signature dictionary.
>
> ```ts theme={"system"}
> import { decodeBase58 } from '@dynamic-labs-wallet/node-svm';
> import { address } from '@solana/kit';
> import type { TransactionPartialSigner, SignatureDictionary } from '@solana/kit';
> import { VersionedMessage, VersionedTransaction } from '@solana/web3.js';
>
> function createDynamicSolanaSigner(
>   accountAddress: string,
>   keyShares: unknown[],
> ): TransactionPartialSigner {
>   const addr = address(accountAddress);
>
>   return {
>     address: addr,
>
>     async signTransactions(transactions): Promise<readonly SignatureDictionary[]> {
>       return Promise.all(
>         transactions.map(async (transaction) => {
>           const message = VersionedMessage.deserialize(
>             Buffer.from(transaction.messageBytes),
>           );
>           const vtx = new VersionedTransaction(message);
>
>           // Returns a base58 Ed25519 signature (not a full signed tx)
>           const signatureBase58 = await client.signTransaction({
>             senderAddress: accountAddress,
>             transaction: vtx,
>             password: process.env.WALLET_PASSWORD!,
>             externalServerKeyShares: keyShares,
>           });
>
>           return Object.freeze({
>             [addr]: decodeBase58(signatureBase58),
>           });
>         }),
>       );
>     },
>   };
> }
>
> const signer = createDynamicSolanaSigner(accountAddress, externalServerKeyShares);
> ```
>
> > [!WARNING]
> >
> > `signTransaction` returns the raw signature only. Do not pass that string to `sendRawTransaction`. For MPP pull mode, `@solana/mpp` merges the signature dictionary into the transaction bytes it sends to the server. This recipe uses push mode (`broadcast: true`) so the client broadcasts and returns a signature credential.

***

## Step 5: Make an MPP payment

Initialize `mppx` with the payment method for your chain 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.

> [tab] Tempo
>
> ```ts theme={"system"}
> 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);
>
> ```
>
> > [!NOTE]
> >
> > `https://mpp.dev/api/ping/paid` is a public Tempo test endpoint that accepts any valid MPP payment. Use it to verify your setup before pointing at a real resource.

> [tab] Solana
>
> ```ts theme={"system"}
> import { Mppx } from 'mppx/client';
> import { solana } from '@solana/mpp/client';
>
> const rpcUrl = process.env.SOLANA_RPC_URL ?? 'https://402.surfnet.dev:8899';
> const network = process.env.SOLANA_NETWORK ?? 'localnet';
>
> const mppx = Mppx.create({
>   methods: [
>     solana.charge({
>       signer,
>       rpcUrl,
>       expectedNetwork: network,
>       broadcast: true,
>     }),
>   ],
>   polyfill: false,
> });
>
> // Point at your MPP-protected URL (local server or a public Solana MPP endpoint)
> const response = await mppx.fetch('http://127.0.0.1:8787/api/ping/paid');
>
> console.log('Status:', response.status);
> console.log('Body:', await response.text());
>
> const receipt = response.headers.get('payment-receipt');
> if (receipt) console.log('Receipt:', receipt);
> ```
>
> > [!NOTE]
> >
> > Run a local Solana MPP server for testing (see the [Solana charge docs](https://mpp.dev/payment-methods/solana/charge)), or call a public Solana MPP endpoint. This recipe was verified end-to-end against a local `@solana/mpp` server on the Surfpool sandbox RPC.

***

## Putting it all together

> [tab] Tempo
>
> ```ts theme={"system"}
> 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());
>
> ```

> [tab] Solana
>
> ```ts theme={"system"}
> import { DynamicSvmWalletClient, decodeBase58 } from '@dynamic-labs-wallet/node-svm';
> import { ThresholdSignatureScheme } from '@dynamic-labs-wallet/node';
> import { address } from '@solana/kit';
> import type { TransactionPartialSigner, SignatureDictionary } from '@solana/kit';
> import { Connection, PublicKey, LAMPORTS_PER_SOL, VersionedMessage, VersionedTransaction } from '@solana/web3.js';
> import { Mppx } from 'mppx/client';
> import { solana } from '@solana/mpp/client';
>
> const rpcUrl = process.env.SOLANA_RPC_URL ?? 'https://402.surfnet.dev:8899';
> const network = process.env.SOLANA_NETWORK ?? 'localnet';
>
> // 1. Authenticate
> const client = new DynamicSvmWalletClient({
>   environmentId: process.env.DYNAMIC_ENVIRONMENT_ID!,
> });
> await client.authenticateApiToken(process.env.DYNAMIC_AUTH_TOKEN!);
>
> // 2. Create wallet
> const { accountAddress, externalServerKeyShares } = await client.createWalletAccount({
>   thresholdSignatureScheme: ThresholdSignatureScheme.TWO_OF_TWO,
>   password: process.env.WALLET_PASSWORD!,
>   backUpToClientShareService: true,
> });
> console.log('Wallet:', accountAddress);
>
> // 3. Fund
> const connection = new Connection(rpcUrl, 'confirmed');
> const airdropSig = await connection.requestAirdrop(
>   new PublicKey(accountAddress),
>   LAMPORTS_PER_SOL,
> );
> await connection.confirmTransaction(airdropSig, 'confirmed');
>
> // 4. Build Kit TransactionPartialSigner backed by Dynamic MPC
> const addr = address(accountAddress);
> const signer: TransactionPartialSigner = {
>   address: addr,
>   async signTransactions(transactions): Promise<readonly SignatureDictionary[]> {
>     return Promise.all(
>       transactions.map(async (transaction) => {
>         const message = VersionedMessage.deserialize(
>           Buffer.from(transaction.messageBytes),
>         );
>         const vtx = new VersionedTransaction(message);
>         const signatureBase58 = await client.signTransaction({
>           senderAddress: accountAddress,
>           transaction: vtx,
>           password: process.env.WALLET_PASSWORD!,
>           externalServerKeyShares,
>         });
>         return Object.freeze({
>           [addr]: decodeBase58(signatureBase58),
>         });
>       }),
>     );
>   },
> };
>
> // 5. Make an MPP payment
> const mppx = Mppx.create({
>   methods: [
>     solana.charge({
>       signer,
>       rpcUrl,
>       expectedNetwork: network,
>       broadcast: true,
>     }),
>   ],
>   polyfill: false,
> });
> const response = await mppx.fetch('http://127.0.0.1:8787/api/ping/paid');
> console.log('Status:', response.status);
> console.log(await response.text());
> ```

***

## Additional Resources

* [MPP documentation](https://mpp.dev)
* [Tempo Machine Payments guide](https://docs.tempo.xyz/guide/machine-payments/)
* [Solana charge (MPP)](https://mpp.dev/payment-methods/solana/charge)
* [mppx client SDK](https://github.com/tempohq/mppx)
* [Dynamic Node SDK Quickstart](https://docs.dynamic.xyz/docs/node/quickstart.md)
* [Sign SVM transactions](https://docs.dynamic.xyz/docs/node/svm/sign-transactions.md)
* [Dynamic API Reference](https://docs.dynamic.xyz/docs/api-reference/overview.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.