> ## Documentation Index
> Fetch the complete documentation index at: https://www.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](/docs/recipes/integrations/x402/implementation) stack, see the [HTTP 402 overview](/docs/recipes/integrations/x402/overview).

***

## Prerequisites

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

***

## Setup

Install the packages for your chain:

<Tabs>
  <Tab title="Tempo">
    <CodeGroup>
      ```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
      ```
    </CodeGroup>
  </Tab>

  <Tab title="Solana">
    <CodeGroup>
      ```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
      ```
    </CodeGroup>

    <Note>
      `@solana/mpp` is the Solana payment method for `mppx`. See the [Solana charge docs](https://mpp.dev/payment-methods/solana/charge).
    </Note>
  </Tab>
</Tabs>

Create a `.env` file with your credentials:

<Tabs>
  <Tab title="Tempo">
    ```env .env theme={"system"}
    DYNAMIC_ENVIRONMENT_ID=your_environment_id
    DYNAMIC_AUTH_TOKEN=your_api_token
    ```
  </Tab>

  <Tab title="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>
  </Tab>
</Tabs>

<Note>
  Use `node --env-file=.env` to load the file automatically, or a library like `dotenv`.
</Note>

***

## Step 1: Initialize the Dynamic wallet client

<Tabs>
  <Tab title="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>

  <Tab title="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!);
    ```
  </Tab>
</Tabs>

***

## 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.

<Tabs>
  <Tab title="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>

  <Tab title="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.
    ```
  </Tab>
</Tabs>

***

## Step 3: Fund the wallet

<Tabs>
  <Tab title="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.
    </Note>
  </Tab>

  <Tab title="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');
    ```
  </Tab>
</Tabs>

***

## Step 4: Build a chain-compatible signer

<Tabs>
  <Tab title="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>

  <Tab title="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.
    </Warning>
  </Tab>
</Tabs>

***

## 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.

<Tabs>
  <Tab title="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.
    </Note>
  </Tab>

  <Tab title="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.
    </Note>
  </Tab>
</Tabs>

***

## Putting it all together

<Tabs>
  <Tab title="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>

  <Tab title="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());
    ```
  </Tab>
</Tabs>

***

## 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](/docs/node/quickstart)
* [Sign SVM transactions](/docs/node/svm/sign-transactions)
* [Dynamic API Reference](/docs/api-reference/overview)
