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

# `Method.tempo.charge` \[One-time stablecoin payments]

## 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 `charge` intent for the Tempo payment method. Requests a one-time payment from the client.

Non-zero charges verify on-chain transfers. Zero-amount charges verify a `proof` payload signed by the client's identity key and return a Receipt without broadcasting a transaction.

For non-zero charges, the client generates a Challenge-bound attribution memo and the server always verifies it. Custom top-level memos aren't supported; split transfers can still include their own memos, but every MPP attribution memo must bind to the same Challenge and realm.

## Usage

Creates OUSD-first offers with USDC.e as the mainnet fallback, or pathUSD with `testnet: true`. Set `currencies` to accept a different list.

```ts twoslash
import { Mppx, tempo } from 'mppx/server'

const mppx = Mppx.create({ methods: [tempo.charge()] })

export async function handler(request: Request) {
  // [!code focus:start]
  const response = await mppx.charge({
    amount: '0.1',
    recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
  })(request)
  // [!code focus:end]

  if (response.status === 402) return response.challenge
  return response.withReceipt(Response.json({ data: '...' }))
}
```

### With expiry

Set a custom expiration time for the charge using the `expires` option.

```ts twoslash
import { Mppx, tempo } from 'mppx/server'

const mppx = Mppx.create({ methods: [tempo.charge()] })
// ---cut---
import { Expires } from 'mppx'

export async function handler(request: Request) {
  const response = await mppx.charge({
    amount: '0.1',
    currency: '0x20c0000000000000000000006a37DA5C996874BE', // OUSD on Tempo
    expires: Expires.minutes(10), // [!code focus]
    recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
  })(request)

  if (response.status === 402) return response.challenge
  return response.withReceipt(Response.json({ data: '...' }))
}
```

### With description

Add a human-readable description for the payment request.

```ts twoslash
import { Mppx, tempo } from 'mppx/server'

const mppx = Mppx.create({ methods: [tempo.charge()] })
// ---cut---
export async function handler(request: Request) {
  const response = await mppx.charge({
    amount: '0.1',
    currency: '0x20c0000000000000000000006a37DA5C996874BE', // OUSD on Tempo
    description: 'API access for /resource', // [!code focus]
    recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
  })(request)

  if (response.status === 402) return response.challenge
  return response.withReceipt(Response.json({ data: '...' }))
}
```

### With MACH funding

Let compatible clients fund a stablecoin charge with MACH through the canonical swapper. Keep `currency` set to the stablecoin you want to receive.

```ts twoslash
import { Mppx, tempo } from 'mppx/server'

const mppx = Mppx.create({
  methods: [tempo.charge({ machineTokenEnabled: true })], // [!code hl]
})

export async function handler(request: Request) {
  const response = await mppx.charge({
    amount: '0.1',
    currency: '0x20c0000000000000000000006a37DA5C996874BE', // OUSD on Tempo
    recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
  })(request)

  if (response.status === 402) return response.challenge
  return response.withReceipt(Response.json({ data: '...' }))
}
```

The client uses MACH only when the canonical route is available, its account holds enough MACH, and the wallet can execute the approval and swap calls. Otherwise, it follows the standard payment path—using `autoSwap` when configured or paying the stablecoin directly. MACH funding doesn't support split charges and uses a funded supported stablecoin for transaction fees.

### With a custom fee payer policy

Override the local fee-sponsor limits when you co-sign charge transactions.

```ts twoslash
import { Mppx, tempo } from 'mppx/server'
import { privateKeyToAccount } from 'viem/accounts'

const mppx = Mppx.create({
  methods: [
    tempo.charge({
      feePayer: privateKeyToAccount(
        '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80',
      ),
      feePayerPolicy: {
        maxPriorityFeePerGas: 50_000_000_000n,
        maxTotalFee: 100_000_000_000_000_000n,
      },
    }),
  ],
})
```

### With a custom sponsor budget

Limit aggregate fee exposure across sponsored transactions. Use a shared `AtomicStore` when more than one process uses the fee payer.

```ts twoslash
import { Mppx, Store, tempo } from 'mppx/server'
import { privateKeyToAccount } from 'viem/accounts'

const mppx = Mppx.create({
  methods: [
    tempo.charge({
      feePayer: privateKeyToAccount(
        '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80',
      ),
      sponsorBudget: {
        maxInFlightReservations: 50,
        maxInFlightTotalFee: 250_000_000_000_000_000n,
        store: Store.memory(),
      },
    }),
  ],
})
```

### With an authenticated remote fee payer

Pass request headers with the remote service URL. `mppx` sends fill requests through the configured fee-payer transport, then broadcasts the completed transaction through the chain RPC transport.

```ts [server.ts]
import { Mppx, tempo } from 'mppx/server'

const mppx = Mppx.create({
  methods: [
    tempo.charge({
      feePayer: {
        headers: {
          Authorization: `Bearer ${process.env.FEE_PAYER_TOKEN!}`,
        },
        url: 'https://sponsor.example.com',
      },
    }),
  ],
})
```

### With Tempo API relay

Delegate Tempo charge validation and finalization to Tempo API. The relay preserves this method's Challenge configuration and needs an API key with the `mpp:write` scope. It broadcasts `pull` Credentials and recognizes `push` Credentials as already broadcast.

```ts twoslash
import { Mppx, tempo } from 'mppx/server'

const mppx = Mppx.create({
  methods: [
    tempo.charge({
      relay: {
        apiKey: process.env.TEMPO_API_KEY!,
      },
    }),
  ],
})
```

See [Relays](/advanced/relays) for the lifecycle, custom API base URLs, and custom `validate` and `broadcast` hooks.

### With replay protection for zero-dollar auth

Pass `store` when you want zero-dollar proof Credentials to be single-use.

```ts twoslash
import { Mppx, Store, tempo } from 'mppx/server'

const replayStore = Store.memory()

const mppx = Mppx.create({
  methods: [
    tempo.charge({
      store: replayStore,
    }),
  ],
})

export async function handler(request: Request) {
  const response = await mppx.charge({
    amount: '0',
    currency: '0x20c0000000000000000000006a37DA5C996874BE', // OUSD on Tempo
    recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
  })(request)

  if (response.status === 402) return response.challenge
  return response.withReceipt(Response.json({ data: '...' }))
}
```

## Return type

Returns a non-empty, ordered array of server methods, one per accepted currency. `Mppx.create({ methods: [tempo.charge(options)] })` accepts the group directly.

```ts
type ReturnType = readonly [Method.Server, ...Method.Server[]]
```

Code that inspects a single method must destructure the group. For route composition, use the configured `mppx.tempo.charge` handler.

Successful charge Receipts include `fundingCurrency` when `mppx` can verify which token funded a direct, MACH, or auto-swap payment route.

## Configuration

These parameters configure the `tempo.charge()` constructor.

### account (optional)

* **Type:** `Account | Address`

Account or address that receives payments. Use `recipient` when you only need to provide the address.

### allowedFeeTokens (optional)

* **Type:** `readonly Address[]`

Accepted transaction fee tokens for sponsored charges. Local sponsors use the SDK's per-chain defaults when omitted; hosted sponsors choose their fee token unless you restrict this list. An empty list throws. Payment `currencies` and fee tokens are independent.

### canOffer (optional)

* **Type:** `Method.CanOfferFn`

Returns whether this configured charge offer is available when an HTTP handler composes multiple offers. The hook receives the normalized, immutable payment request and a clone of the incoming `Request`.

### chainId (optional)

* **Type:** `number`
* **Default:** `4217`

Tempo chain ID. `testnet: true` selects `42431`; an explicit `chainId` takes precedence. Set it explicitly when `getClient` uses another network.

### currencies (optional)

* **Type:** `readonly (string | Token)[]`
* **Default:** OUSD, then USDC.e on mainnet; OUSD, then pathUSD on Moderato

Ordered accepted token addresses or USD token definitions from `viem/tokens`. An explicit list replaces the defaults; `currencies: [ousd(4217).address]` accepts only OUSD on mainnet. Token definitions resolve against `chainId`; unavailable and non-USD tokens are skipped, and duplicate addresses are removed. An empty resolved list throws. Ordering controls presentation, not client selection or automatic swaps.

### currency (optional, deprecated)

* **Type:** `string`

Accepts one token address. Use `currencies: [currency]` instead. Don't set both options. Wire Challenges and per-handler overrides still use singular `currency`.

### decimals (optional)

* **Type:** `number`
* **Default:** `6`

Decimal places for amount parsing.

### externalId (optional)

* **Type:** `string`

External identifier for the payment.

### feePayer (optional)

* **Type:** `Account | { fetch?: typeof globalThis.fetch; headers?: Readonly<Record<string, string>>; url: string } | string | true`

Account or remote service for sponsoring transaction fees. Pass a viem `Account` to co-sign locally, a URL string to use a remote [fee payer service](https://docs.tempo.xyz/sdk/typescript/server/handler.feePayer), an object with `fetch`, `headers`, and `url` to customize remote requests, or `true` when the `account` parameter doubles as the fee payer. The custom `fetch` handles only fee-payer JSON-RPC traffic. `mppx` reserves the `Content-Type` header for JSON.

This setting only applies to non-zero charges. Zero-amount proof flows do not create a transaction.

### feePayerPolicy (optional)

* **Type:** `Partial<{ allowKeyAuthorization: boolean; maxFeePerGas: bigint; maxGas: bigint; maxInFlightReservations: number; maxInFlightTotalFee: bigint; maxPriorityFeePerGas: bigint; maxTotalFee: bigint; maxValidityWindowSeconds: number }>`

Override the local fee-sponsor policy used when the server co-signs Tempo charge transactions. Remote fee payer services enforce their own policy.

`allowKeyAuthorization` defaults to `false`. Set it to `true` only when this sponsor may fund transactions that install a new access key.

`mppx` resolves the remaining defaults per chain automatically. On mainnet (`4217`), the defaults are `maxFeePerGas: 100_000_000_000n`, `maxGas: 2_000_000n`, `maxPriorityFeePerGas: 10_000_000_000n`, `maxTotalFee: 50_000_000_000_000_000n`, and `maxValidityWindowSeconds: 900`. On Moderato (`42431`), `maxPriorityFeePerGas` increases to `50_000_000_000n` and the other limits stay the same.

The deprecated `maxInFlightReservations` and `maxInFlightTotalFee` properties still work here. Configure them with `sponsorBudget` instead.

```ts twoslash
import { Mppx, tempo } from 'mppx/server'
import { privateKeyToAccount } from 'viem/accounts'

const mppx = Mppx.create({
  methods: [
    tempo.charge({
      feePayer: privateKeyToAccount(
        '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80',
      ),
      feePayerPolicy: {
        allowKeyAuthorization: true,
        maxPriorityFeePerGas: 50_000_000_000n,
        maxTotalFee: 100_000_000_000_000_000n,
      },
    }),
  ],
})
```

### feeToken (optional)

* **Type:** `Address`

Token used by a local sponsor to pay transaction fees. It must appear in `allowedFeeTokens` when that list is configured. Omit it to select a funded allowed token. Remote fee payers choose their own fee token.

### getClient (optional)

* **Type:** `(parameters: { chainId?: number }) => MaybePromise<Client>`

Function that returns a viem client for the given chain ID. Overrides the default RPC configuration.

### html (optional)

* **Type:** `boolean | Html.Config`

Renders a payment page when the request accepts `text/html`. Pass an object to customize the page.

### machineTokenEnabled (optional)

* **Type:** `boolean`

Advertises canonical MACH funding for compatible single-recipient charges. Clients can spend MACH while the server receives the requested stablecoin. The server rejects MACH itself as the advertised `currency`.

### onPaymentSuccess (optional)

* **Type:** `Method.OnPaymentSuccessFn<typeof tempo.Methods.charge>`

Runs after this Tempo charge succeeds. The hook receives the optional associated Challenge, canonical request, its Receipt, the HTTP input when available, and the resolved pre-transform `requestInput` when route options are available. Errors don't change payment handling.

### relay (optional)

* **Type:** `{ apiBaseUrl?: string; apiKey: string; fetch?: typeof globalThis.fetch }`

Delegates Tempo charge Credential validation and finalization to a compatible MPP relay. `apiKey` needs the `mpp:write` scope. Defaults to `https://api.tempo.xyz`; set `apiBaseUrl` for a compatible relay, including one behind a path prefix. The relay broadcasts `pull` Credentials and verifies `push` Credential hashes without broadcasting them again.

### sponsorBudget (optional)

* **Type:** `{ maxInFlightReservations?: number; maxInFlightTotalFee?: bigint; store?: Store.AtomicStore } | false`
* **Default:** `{}`

Limits aggregate declared fee exposure for sponsored transactions. `maxInFlightReservations` defaults to `100`, and `maxInFlightTotalFee` defaults to ten times `feePayerPolicy.maxTotalFee`. Both values must be greater than zero.

The `store` defaults to the charge method's `store` and must be shared by every process using the sponsor. Budget keys don't use `storeKeyPrefix`, so every tenant and process sees the sponsor-wide capacity.

Set `false` only when another system enforces the aggregate sponsor budget.

### store (optional)

* **Type:** `Store.AtomicStore`

Pass a store when you want replay protection for charge Credentials. A `Store` provides async key-value operations (`get`, `put`, `delete`). An `AtomicStore` extends `Store` with an atomic `update(key, fn)` method and an optional optimized `tryClaim(key, expires)` operation.

For non-zero charges, `mppx` falls back to an in-memory store when you omit this parameter. For zero-dollar proof auth, replay prevention is disabled unless you pass a store.

`mppx` claims transaction hashes and proof Challenge IDs through the Challenge expiration time. It calls [`Store.tryClaim`](/sdk/typescript/core/Store.tryClaim), which uses the store's optimized operation when present and otherwise falls back to `update`.

Use `Store.memory()` for local development, tests, or a single long-lived server process. For multi-instance deployments, use `Store.redis()`, `Store.upstash()`, or `Store.cloudflare()`. All built-in factories return `AtomicStore`—for custom backends, provide an `update` function alongside `get`, `put`, and `delete`.

### storeKeyPrefix (optional)

* **Type:** `string`

Prefix prepended to replay-protection keys. Use distinct prefixes when multiple applications share one store.

### testnet (optional)

* **Type:** `boolean`

Testnet mode. Defaults the chain ID to `42431` (Tempo testnet).

### validateSender (optional)

* **Type:** `(parameters: { expectedSender: Address; sender: Address; source?: { address: Address; chainId: number } }) => boolean | Promise<boolean>`

Allows a verified TIP-20 transfer sender to differ from the Credential source. Core verification still checks the amount, Challenge-bound attribution memo, currency, recipient, replay state, and transaction success.

### waitForConfirmation (optional)

* **Type:** `boolean`
* **Default:** `true`

Whether to wait for the charge transaction to confirm on-chain before responding. When `false`, the transaction is simulated via `eth_estimateGas` and broadcast without waiting for inclusion. The Receipt optimistically reports `status: 'success'` based on simulation alone.

This option applies only to non-zero charges. Zero-amount proof flows return immediately after signature verification.

```ts twoslash
import { Mppx, tempo } from 'mppx/server'

const mppx = Mppx.create({
  methods: [tempo.charge({
    waitForConfirmation: false, // [!code focus]
  })],
})
```

## Request parameters

### amount

* **Type:** `string`

Payment amount in human-readable units. For example, `'0.1'` represents $0.10 USD.

Set `'0'` to require identity-only zero-dollar auth. In that case, the client submits a `proof` payload instead of a transaction or hash.

By default, zero-dollar proof Credentials remain reusable until the Challenge expires. Pass `store` to treat proofs as single-use across the scope of that store.

### currency (optional)

* **Type:** `string`

Overrides the configured payment currency for this route. Omit it to offer the configured currencies.

### description (optional)

* **Type:** `string`

Human-readable description of the payment request.

### expires (optional)

* **Type:** `string`
* **Default:** 5 minutes from now

ISO 8601 timestamp for when the payment Challenge expires.

### meta (optional)

* **Type:** `Record<string, string>`

Server-defined correlation data. `mppx` serializes it as the base64url-encoded `opaque` auth-param on the Challenge, and clients echo that same string back in the Credential.

### machineTokenEnabled (optional)

* **Type:** `boolean`

Overrides the method default for this charge. Set `true` to advertise the canonical MACH funding route.

### recipient

* **Type:** `string`

Address to receive the payment.

### scope (optional)

* **Type:** `string`

Route or resource scope bound into the Challenge metadata. Use this to prevent a Credential issued for one route from being replayed against another route with the same payment terms.

### splits (optional)

* **Type:** `Array<{ amount: string; memo?: string; recipient: string }>`

Split the charge across additional recipients. Each entry specifies an `amount` (in human-readable units) and a `recipient` address. The primary `recipient` receives `amount` minus the sum of all split amounts.

| Constraint | Value |
|---|---|
| Array length | 1–10 |
| Each split amount | Must be > 0 |
| Sum of splits | Must be strictly less than `amount` |
| Split memo | Optional, 32-byte hex hash |

At least one matched transfer must include the Challenge-bound attribution memo. Any other matched transfer that contains an MPP attribution memo must bind to the same Challenge and realm. Plain transfers and custom non-MPP memos remain valid.

```ts twoslash
import { Mppx, tempo } from 'mppx/server'

const mppx = Mppx.create({ methods: [tempo.charge()] })
// ---cut---
export async function handler(request: Request) {
  const response = await mppx.charge({
    amount: '1.00',
    currency: '0x20c0000000000000000000006a37DA5C996874BE', // OUSD
    recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', // seller
    splits: [ // [!code focus]
      { amount: '0.10', recipient: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8' }, // platform fee // [!code focus]
    ], // [!code focus]
  })(request)

  if (response.status === 402) return response.challenge
  return response.withReceipt(Response.json({ data: '...' }))
}
```
