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

# Tempo charge \[One-time TIP-20 token transfers]

## 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 Tempo implementation of the [charge](/intents/charge) intent.

For non-zero charges, the client signs a TIP-20 `transfer` transaction, the server broadcasts it to Tempo, and settlement completes in ~500ms with deterministic finality.

For zero-amount identity flows, the client sends a `proof` Credential payload instead of a real transaction. The server verifies the signed proof message against the client's `source` identity and returns a Receipt without broadcasting anything on-chain.

This method is best for single API calls, content access, or one-off purchases.

## Server

Use [`mppx.charge`](/sdk/typescript/server/Method.tempo.charge) to gate any endpoint behind a one-time payment. The method handles Challenge generation, Credential verification, transaction broadcast for paid requests, and Receipt creation.

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

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

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

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

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

### With expiry

```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 result = await mppx.charge({
    amount: "0.1",
    currency: "0x20c0000000000000000000000000000000000000",
    expires: Expires.minutes(10), // [!code hl]
    recipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
  })(request);

  if (result.status === 402) return result.challenge;
  return result.withReceipt(Response.json({ data: "..." }));
}
```

### With fee sponsorship

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

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

declare const request: Request;
// ---cut---
const result = await mppx.charge({
  amount: "0.1",
  currency: "0x20c0000000000000000000000000000000000000",
  feePayer: true, // [!code hl]
  recipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
})(request);
```

When `feePayer` is `true`, the server adds a fee payer signature (domain `0x78`) before broadcasting. The client doesn't need gas tokens. See [fee sponsorship](/payment-methods/tempo#fee-sponsorship) for details.

### Zero-dollar auth

Set `amount: "0"` to issue an identity-only Challenge. The client returns a `proof` payload instead of `transaction` or `hash`, and the server verifies the signature against the `source` DID.

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

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

declare const request: Request;
// ---cut---
const result = await mppx.charge({
  amount: "0", // [!code hl]
  currency: "0x20c0000000000000000000000000000000000000",
  recipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
})(request);
```

Use this for polling, free follow-up requests, and unlock flows after an initial payment.

Pass `store` to `tempo.charge()` when you want single-use proofs.

### With split payments

Split a charge across multiple recipients in a single transaction. The primary `recipient` receives `amount` minus the sum of all splits.

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

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

declare const request: Request;
// ---cut---
const result = await mppx.charge({
  amount: "1.00",
  currency: "0x20c0000000000000000000000000000000000000", // pathUSD
  recipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", // seller
  // [!code hl:start]
  splits: [
    {
      amount: "0.10",
      recipient: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", // platform fee
    },
  ],
  // [!code hl:end]
})(request);
```

Up to 10 splits per charge. Each split must have a positive amount, and the sum of all splits must be less than the total `amount`. See the [split payments guide](/guides/split-payments) for more details.

### Payment links

Set `html: true` on the method to render a payment page when a browser navigates to the endpoint. The page shows a "Continue with Tempo" button—after the user pays, the page reloads with the paid resource. Programmatic clients with `Authorization` headers are unaffected.

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

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

declare const request: Request;
// ---cut---
const result = await mppx.charge({
  amount: "0.1",
  currency: "0x20c0000000000000000000000000000000000000",
  recipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
})(request);
```

See the [payment links guide](/guides/payment-links) for a full walkthrough with framework examples and a live demo.

### With Stripe

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

async function createPayToAddress(request: Request): Promise<`0x${string}`> {
  void request;
  return "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266";
}

export async function handler(request: Request) {
  const recipientAddress = await createPayToAddress(request);
  const mppx = Mppx.create({
    methods: [
      tempo.charge({
        currency: "0x20c0000000000000000000000000000000000000",
        recipient: recipientAddress,
      }),
    ],
  });

  const result = await mppx.charge({ amount: "0.01" })(request);
 
  if (result.status === 402) return result.challenge;
 
  return result.withReceipt(Response.json({ data: "..." }));
}
```

Use Stripe to create a dynamic recipient address per payment, each backed by a PaymentIntent. To learn more, read the [Stripe documentation](https://docs.stripe.com/payments/machine/mpp) on accepting MPP.

:::info
See [`tempo.charge` server reference](/sdk/typescript/server/Method.tempo.charge) for the full parameter list.
:::

## Client

Use [`tempo.charge`](/sdk/typescript/client/Method.tempo.charge) with `Mppx.create` to automatically handle `402` responses. For non-zero amounts, the client signs a TIP-20 transfer and retries with the Credential. For zero-amount Challenges, it signs a proof message and retries with a `proof` payload instead.

### Accounts SDK

```ts twoslash
import { Provider } from "accounts";
import { Mppx, tempo } from "mppx/client";

const provider = Provider.create({ mpp: false }); // Avoid double 402 handling; mppx is configured below.
await provider.request({ method: "wallet_connect" });

Mppx.create({
  methods: [tempo.charge({
    account: provider.getAccount({ signable: true }),
    getClient: provider.getClient,
  })],
});

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

### viem

```ts twoslash
import { Mppx, tempo } from "mppx/client";
import { privateKeyToAccount } from "viem/accounts";

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

Mppx.create({
  methods: [tempo.charge({ account })],
});

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

### Without polyfill

If you don't want to patch `globalThis.fetch`, use `mppx.fetch` directly:

#### Accounts SDK

```ts twoslash
import { Provider } from "accounts";
import { Mppx, tempo } from "mppx/client";

const provider = Provider.create({ mpp: false }); // Avoid double 402 handling; mppx is configured below.
await provider.request({ method: "wallet_connect" });

const mppx = Mppx.create({
  methods: [tempo.charge({
    account: provider.getAccount({ signable: true }),
    getClient: provider.getClient,
  })],
  polyfill: false,
});

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

#### viem

```ts twoslash
import { Mppx, tempo } from "mppx/client";
import { privateKeyToAccount } from "viem/accounts";

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

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

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

:::info
See [`tempo.charge` client reference](/sdk/typescript/client/Method.tempo.charge) for the full parameter list.
:::

### Chain pinning

Set `expectedChainId` when the client should only pay on a specific Tempo network. The client rejects Challenges for other chains and uses this chain when the Challenge doesn't include `chainId`.

#### Accounts SDK

```ts twoslash
import { Mppx, tempo } from "mppx/client";
import { Provider } from "accounts";

const provider = Provider.create({ mpp: false }) // Avoid double 402 handling; mppx is configured below.
await provider.request({ method: "wallet_connect" })

Mppx.create({
  methods: [
    tempo.charge({
      account: provider.getAccount({ signable: true }),
      getClient: provider.getClient,
      expectedChainId: 4217, // Tempo mainnet // [!code hl]
    }),
  ],
});
```

#### viem

```ts twoslash
import { Mppx, tempo } from "mppx/client";
import { privateKeyToAccount } from "viem/accounts";

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

Mppx.create({
  methods: [
    tempo.charge({
      account,
      expectedChainId: 4217, // Tempo mainnet // [!code hl]
    }),
  ],
});
```

### Auto-swap

When the client doesn't hold the requested currency, `autoSwap` automatically swaps from a fallback stablecoin (pathUSD, USDC.e) via the Tempo DEX before transferring.

#### Accounts SDK

```ts twoslash
import { Provider } from "accounts";
import { Mppx, tempo } from "mppx/client";

const provider = Provider.create({ mpp: false }); // Avoid double 402 handling; mppx is configured below.
await provider.request({ method: "wallet_connect" });

Mppx.create({
  methods: [
    tempo.charge({
      account: provider.getAccount({ signable: true }),
      autoSwap: true, // [!code hl]
      getClient: provider.getClient,
    }),
  ],
});
```

#### viem

```ts twoslash
import { Mppx, tempo } from "mppx/client";
import { privateKeyToAccount } from "viem/accounts";

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

Mppx.create({
  methods: [
    tempo.charge({
      account,
      autoSwap: true, // [!code hl]
    }),
  ],
});
```

Pass an object for custom fallback tokens or slippage:

##### Accounts SDK

```ts twoslash
import { Provider } from "accounts";
import { Mppx, tempo } from "mppx/client";

const provider = Provider.create({ mpp: false }); // Avoid double 402 handling; mppx is configured below.
await provider.request({ method: "wallet_connect" });

Mppx.create({
  methods: [
    tempo.charge({
      account: provider.getAccount({ signable: true }),
      // [!code hl:start]
      autoSwap: {
        slippage: 2, // max slippage % (default: 1)
        tokenIn: ["0x0000000000000000000000000000000000000001"],
      },
      // [!code hl:end]
      getClient: provider.getClient,
    }),
  ],
});
```

##### viem

```ts twoslash
import { Mppx, tempo } from "mppx/client";
import { privateKeyToAccount } from "viem/accounts";

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

Mppx.create({
  methods: [
    tempo.charge({
      account,
      // [!code hl:start]
      autoSwap: {
        slippage: 2, // max slippage % (default: 1)
        tokenIn: ["0x0000000000000000000000000000000000000001"],
      },
      // [!code hl:end]
    }),
  ],
});
```

See [auto-swap](/payment-methods/tempo#auto-swap) for more details.

## Specification

[IETF Specification](https://paymentauth.org/draft-tempo-charge-00) — Read the full specification
