> **Can't find what you're looking for?** Use `search_docs` on the docs MCP server at `https://mpp.dev/api/mcp` to find what you need.

# Monad charge \[One-time payments on Monad]

## Choose a signing account

### Direct

Create a server-only wallet.ts module, then import account wherever an example creates a local signing account.

```ts
import { privateKeyToAccount } from 'viem/accounts'

export const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)
```

### Privy

Create an EVM wallet in Privy, fund it with the required currency on this page's network, and keep PRIVY\_APP\_SECRET server-side.

```bash
pnpm add @privy-io/node
```

Create a server-only privy.ts module, then import its account wherever an example configures account or feePayer.

```ts
import { PrivyClient } from '@privy-io/node'
import { createViemAccount } from '@privy-io/node/viem'

const privy = new PrivyClient({
  appId: process.env.PRIVY_APP_ID!,
  appSecret: process.env.PRIVY_APP_SECRET!,
})

export const account = createViemAccount(privy, {
  address: process.env.PRIVY_WALLET_ADDRESS as `0x${string}`,
  walletId: process.env.PRIVY_WALLET_ID!,
})
```

createViemAccount delegates signatures to the Privy wallet, so it replaces any local viem account in the examples on this page.

The Monad implementation of the [charge](/intents/charge) intent.

The server issues a charge Challenge describing the expected amount, currency, and recipient. The client either broadcasts an ERC-20 `transfer` and returns the transaction hash (**push** mode), or signs an ERC-3009 `transferWithAuthorization` for the server to broadcast (**pull** mode). The server verifies the transfer on-chain and returns the resource with a Receipt.

This method is best for fixed-price API calls, digital goods, and payments that settle directly on Monad.

## Server

Use `monad.charge` to gate any endpoint behind a one-time ERC-20 payment. The method handles Challenge generation, Credential verification, on-chain settlement, and Receipt creation.

```ts
import { Mppx } from "mppx/server";
import { monad } from "@monad-crypto/mpp/server";

const mppx = Mppx.create({
  methods: [monad()],
});

export async function handler(request: Request) {
  const result = await mppx.charge({
    amount: "0.1",
    currency: "0x754704Bc059F8C67012fEd69BC8A327a5aafb603", // USDC
    recipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
  })(request);

  if (result.status === 402) return result.challenge;

  return result.withReceipt(Response.json({ data: "..." }));
}
```

### With pull mode (ERC-3009)

To accept pull mode Credentials, provide an `account` so the server can broadcast `transferWithAuthorization` and pay gas:

```ts
import { Mppx } from "mppx/server";
import { monad } from "@monad-crypto/mpp/server";
import { privateKeyToAccount } from "viem/accounts";

const account = privateKeyToAccount(process.env.SERVER_PRIVATE_KEY as `0x${string}`);

const mppx = Mppx.create({
  methods: [monad.charge({
    account, // [!code hl]
  })],
});
```

## Client

Use `monad.charge` with `Mppx.create` to automatically handle `402` responses. The client parses the Challenge, creates a Credential (either a signed transfer or an ERC-3009 authorization), and retries with the Credential.

```ts
import { Mppx } from "mppx/client";
import { monad } from "@monad-crypto/mpp/client";
import { privateKeyToAccount } from "viem/accounts";

const account = privateKeyToAccount("0xabc…123");

const mppx = Mppx.create({
  methods: [monad.charge({ account })],
});

const response = await mppx.fetch("https://api.example.com/resource");
```

## Settlement modes

Monad charge supports two settlement modes:

* **Push** — the client broadcasts an ERC-20 `transfer(recipient, amount)` transaction on-chain and includes the transaction hash in the Credential. The server verifies the transfer by reading `Transfer` event logs.
* **Pull** — the client signs an ERC-3009 `transferWithAuthorization` off-chain. The server broadcasts the authorization on-chain via `transferWithAuthorization`, paying the gas. The server account does not need to match the recipient address.
