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

# MPP Charge Guide

> Build a paid API with Node.js and Stellar settlement using the Machine Payments Protocol (MPP).

The [**charge** intent](https://mpp.dev/intents/charge) is for immediate, one-time payments. Each API request triggers a Soroban SAC `transfer` that settles on-chain individually — no channel setup, no pre-funding, and no external facilitator required. This is the simplest way to get started with MPP on Stellar.

## How charge payments work

```
Client (Payer)            Server (Recipient)         Soroban RPC / Network
  |                              |                          |
  | GET /resource                |                          |
  |----------------------------->|                          |
  |                              |                          |
  | 402 Payment Required         |                          |
  | (currency, amount, recipient,|                          |
  |  network)                    |                          |
  |<-----------------------------|                          |
  |                              |                          |
  | Build Soroban SAC transfer   |                          |
  | Simulate (prepareTransaction)|                          |
  |----------------------------------------------------->   |
  |                              |              Simulation  |
  |<-----------------------------------------------------   |
  |                              |                          |
  | Sign transaction envelope    |                          |
  | Send signed XDR credential   |                          |
  |----------------------------->|                          |
  |                              |                          |
  |                              | Verify SAC invocation    |
  |                              | Simulate + validate      |
  |                              |   transfer events        |
  |                              |------------------------->|
  |                              |<-------------------------|
  |                              |                          |
  |                              | Broadcast transaction    |
  |                              |------------------------->|
  |                              |                          |
  |                              | Poll until confirmed     |
  |                              |<-------------------------|
  |                              |                          |
  | 200 OK + receipt             |                          |
  |<-----------------------------|                          |
```

In **pull** mode (default), the client builds and signs the full transaction envelope; the server validates the SAC transfer via simulation, then broadcasts. With **sponsored fees**, the client signs only the Soroban auth entries and the server rebuilds the transaction with its own account as source. In **push** mode, the client broadcasts the transaction itself and sends a `signedHash` credential — the transaction hash plus a signature proving control of the `from` account — for server verification.

This tutorial walks through building a payment-gated API with Node.js and Express using `@stellar/mpp`.

To follow this guide, you will need [Node.js](https://nodejs.org) installed locally. Recommend using the latest LTS version.

## Create a project

Create a new folder for the tutorial and initialize a Node.js project:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
mkdir mpp-quickstart
cd mpp-quickstart
npm init -y
npm pkg set type=module
```

The `type=module` setting lets you use ES module `import` syntax in the examples below.

Install the npm packages used by the server and client:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm install express @stellar/mpp mppx @stellar/stellar-sdk
```

## Create `server.js`

Create a file named `server.js` and paste in the following code:

```js title="server.js" theme={"theme":{"light":"github-light","dark":"github-dark"}}
import express from "express";
import { Mppx, Store } from "mppx/server";
import { stellar } from "@stellar/mpp/charge/server";
import { USDC_SAC_TESTNET } from "@stellar/mpp";

const PORT = 3001;
const RECIPIENT = process.env.STELLAR_RECIPIENT; // Your Stellar public key (G...)
const MPP_SECRET_KEY = process.env.MPP_SECRET_KEY; // Shared secret for MPP credential verification

if (!RECIPIENT) {
  console.error("Set STELLAR_RECIPIENT to a Stellar public key (G...)");
  process.exit(1);
}

if (!MPP_SECRET_KEY) {
  console.error(
    "Set MPP_SECRET_KEY to a strong secret for MPP credential verification",
  );
  process.exit(1);
}

// Create the MPP server instance
const mppx = Mppx.create({
  secretKey: MPP_SECRET_KEY,
  methods: [
    stellar.charge({
      recipient: RECIPIENT,
      currency: USDC_SAC_TESTNET,
      network: "stellar:testnet",
      store: Store.memory(), // Required by v0.7 for replay protection
    }),
  ],
});

const app = express();

// Payment-gated endpoint
app.get("/my-service", async (req, res) => {
  // Convert Node.js IncomingMessage to Web Request
  const headers = new Headers();
  for (const [key, value] of Object.entries(req.headers)) {
    if (value == null) continue;
    if (Array.isArray(value)) {
      for (const entry of value) {
        headers.append(key, entry);
      }
    } else {
      headers.set(key, value);
    }
  }

  const webReq = new Request(`http://localhost:${PORT}${req.url}`, {
    method: req.method,
    headers,
  });

  const result = await mppx.charge({
    amount: "0.01",
    description: "Premium API access",
  })(webReq);

  if (result.status === 402) {
    const challenge = result.challenge;
    challenge.headers.forEach((value, key) => res.setHeader(key, value));
    return res.status(402).send(await challenge.text());
  }

  const response = result.withReceipt(
    Response.json({ secret: "valuable content" }),
  );
  response.headers.forEach((value, key) => res.setHeader(key, value));
  return res.status(response.status).send(await response.text());
});

app.listen(PORT, () => {
  console.log(`MPP server listening on http://localhost:${PORT}/my-service`);
});
```

Set `STELLAR_RECIPIENT` to the Stellar public key (`G...`) for the account that should receive USDC payments. Your account will need a testnet USDC trustline — see [Setting up a testnet wallet](#setting-up-a-testnet-wallet) below.

Start the API locally:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
STELLAR_RECIPIENT=GYOUR_PUBLIC_KEY MPP_SECRET_KEY=replace-me node server.js
```

When a client requests your endpoint, the server responds with `402 Payment Required`, including headers that describe the payment requirements. A compliant client builds a signed Soroban SAC transfer and retries the request — no external facilitator needed. The server verifies and broadcasts the transaction directly.

## Create `client.js`

### Setting up a testnet wallet

Create a fresh account and fund it with testnet XLM and testnet USDC using Stellar Lab:

1. Create a new keypair: [https://lab.stellar.org/account/create](https://lab.stellar.org/account/create)
2. Fund with testnet XLM (Friendbot): [https://lab.stellar.org/account/fund](https://lab.stellar.org/account/fund)
3. Create the USDC trustline (there's a button on the fund page above)
4. Get testnet USDC from the Circle faucet — select **Stellar Testnet** and paste in your public key: [https://faucet.circle.com](https://faucet.circle.com)

Create a `.env` file and add your testnet secret key:

```bash title=".env" theme={"theme":{"light":"github-light","dark":"github-dark"}}
STELLAR_SECRET=S...
```

> [!WARNING]
>
> Secret keys provide full access to any digital assets held in the wallet. Use `.env` files only for hot wallets in testnet deployments.

### Client code

With your server running, create a file named `client.js` and paste in the following code:

```js title="client.js" theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { Keypair } from "@stellar/stellar-sdk";
import { Mppx } from "mppx/client";
import { stellar } from "@stellar/mpp/charge/client";
import { readFileSync } from "node:fs";

// Load .env manually (no dotenv package needed)
const env = Object.fromEntries(
  readFileSync(".env", "utf-8")
    .split("\n")
    .filter((l) => l.includes("="))
    .map((l) => l.split("=")),
);
const STELLAR_SECRET = env.STELLAR_SECRET?.trim();

if (!STELLAR_SECRET) {
  console.error("Add STELLAR_SECRET=S... to .env");
  process.exit(1);
}

const keypair = Keypair.fromSecret(STELLAR_SECRET);
console.log(`Using Stellar account: ${keypair.publicKey()}`);

// Polyfill global fetch — 402 responses are handled automatically
Mppx.create({
  methods: [
    stellar.charge({
      keypair,
      mode: "pull", // server broadcasts the signed transaction
      onProgress(event) {
        console.log(`[${event.type}]`, event);
      },
    }),
  ],
});

// Make the request — payment is handled transparently on 402
const response = await fetch("http://localhost:3001/my-service");
const data = await response.json();

console.log(`Response (${response.status}):`, data);
```

## Run the client

Once the account is funded and the secret key is in `.env`, run the client in a second terminal:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
node client.js
```

The client:

1. Makes a `GET /my-service` request
2. Receives a `402 Payment Required` with payment details in the response headers
3. Builds and signs a Soroban SAC `transfer` on Stellar Testnet
4. Retries the request with the signed credential
5. Receives `200 OK` with the protected content: `{ secret: 'valuable content' }`

The 0.01 USDC settles directly to the `STELLAR_RECIPIENT` wallet. No facilitator, no extra infrastructure.

## Push mode

The examples above use **pull mode** (default), where the server broadcasts the signed transaction. In **push mode**, the client broadcasts the transaction directly and sends a signed hash credential (`signedHash`) to the server for verification.

### Push mode client

To use push mode, set `mode: "push"` in the client configuration and provide the keypair of the `from` account (the account funding the transfer). The SDK handles `signedHash` credential generation automatically — no manual signing code required:

```js title="client.js (push mode)" theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { Keypair } from "@stellar/stellar-sdk";
import { Mppx } from "mppx/client";
import { stellar } from "@stellar/mpp/charge/client";
import { readFileSync } from "node:fs";

// Load .env manually (no dotenv package needed)
const env = Object.fromEntries(
  readFileSync(".env", "utf-8")
    .split("\n")
    .filter((l) => l.includes("="))
    .map((l) => l.split("=")),
);
const STELLAR_SECRET = env.STELLAR_SECRET?.trim();

if (!STELLAR_SECRET) {
  console.error("Add STELLAR_SECRET=S... to .env");
  process.exit(1);
}

const keypair = Keypair.fromSecret(STELLAR_SECRET);
console.log(`Using Stellar account: ${keypair.publicKey()}`);

// Polyfill global fetch — 402 responses are handled automatically
Mppx.create({
  methods: [
    stellar.charge({
      keypair,
      mode: "push", // client broadcasts the transaction
      onProgress(event) {
        console.log(`[${event.type}]`, event);
      },
    }),
  ],
});

// Make the request — payment is handled transparently on 402
const response = await fetch("http://localhost:3001/my-service");
const data = await response.json();

console.log(`Response (${response.status}):`, data);
```

### Push mode server

The server accepts both pull and push credentials automatically — no extra configuration needed. The only change from the pull mode server is moving `store` into `stellar.charge()`:

```js title="server.js (push mode)" theme={"theme":{"light":"github-light","dark":"github-dark"}}
import express from "express";
import { Mppx, Store } from "mppx/server";
import { stellar } from "@stellar/mpp/charge/server";
import { USDC_SAC_TESTNET } from "@stellar/mpp";

const PORT = 3001;
const RECIPIENT = process.env.STELLAR_RECIPIENT;
const MPP_SECRET_KEY = process.env.MPP_SECRET_KEY;

if (!RECIPIENT) {
  console.error("Set STELLAR_RECIPIENT to a Stellar public key (G...)");
  process.exit(1);
}

if (!MPP_SECRET_KEY) {
  console.error(
    "Set MPP_SECRET_KEY to a strong secret for MPP credential verification",
  );
  process.exit(1);
}

const mppx = Mppx.create({
  secretKey: MPP_SECRET_KEY,
  methods: [
    stellar.charge({
      recipient: RECIPIENT,
      currency: USDC_SAC_TESTNET,
      network: "stellar:testnet",
      store: Store.memory(), // Required by v0.7 for replay protection
    }),
  ],
});

const app = express();

app.get("/my-service", async (req, res) => {
  const headers = new Headers();
  for (const [key, value] of Object.entries(req.headers)) {
    if (value == null) continue;
    if (Array.isArray(value)) {
      for (const entry of value) {
        headers.append(key, entry);
      }
    } else {
      headers.set(key, value);
    }
  }

  const webReq = new Request(`http://localhost:${PORT}${req.url}`, {
    method: req.method,
    headers,
  });

  const result = await mppx.charge({
    amount: "0.01",
    description: "Premium API access",
  })(webReq);

  if (result.status === 402) {
    const challenge = result.challenge;
    challenge.headers.forEach((value, key) => res.setHeader(key, value));
    return res.status(402).send(await challenge.text());
  }

  const response = result.withReceipt(
    Response.json({ secret: "valuable content" }),
  );
  response.headers.forEach((value, key) => res.setHeader(key, value));
  return res.status(response.status).send(await response.text());
});

app.listen(PORT, () => {
  console.log(`MPP server listening on http://localhost:${PORT}/my-service`);
});
```

### Signed hash credentials

The SDK generates `signedHash` credentials automatically when `mode: "push"` is set — you do not write any signing code. Under the hood, the client:

1. Computes a hash of the transaction envelope
2. Creates the string `"{challenge.id}:{hash}"` where `challenge.id` is the MPP challenge identifier
3. Signs this string using the keypair of the `from` account

The server verifies the signature against the public key of the `from` account — the account funding the transfer — ensuring the client authorized the specific hash. With sponsored fees the transaction envelope is sourced by the fee payer, so the signature is always checked against the `from` account rather than whoever broadcasts.

## Sponsored fees (optional)

By default, the client pays Stellar network fees. To have the server pay fees on behalf of the client, configure a `feePayer` on the server:

```js theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { Keypair } from "@stellar/stellar-sdk";
import { Mppx, Store } from "mppx/server";
import { stellar } from "@stellar/mpp/charge/server";
import { USDC_SAC_TESTNET } from "@stellar/mpp";

const RECIPIENT = process.env.STELLAR_RECIPIENT;
const MPP_SECRET_KEY = process.env.MPP_SECRET_KEY;
const FEE_PAYER_SECRET = process.env.FEE_PAYER_SECRET;

if (!MPP_SECRET_KEY) {
  throw new Error(
    "Set MPP_SECRET_KEY to a strong secret for MPP credential verification",
  );
}

if (!FEE_PAYER_SECRET) {
  throw new Error("Set FEE_PAYER_SECRET to a Stellar secret key (S...)");
}

const mppx = Mppx.create({
  secretKey: MPP_SECRET_KEY,
  methods: [
    stellar.charge({
      recipient: RECIPIENT,
      currency: USDC_SAC_TESTNET,
      network: "stellar:testnet",
      store: Store.memory(), // Required by v0.7 for replay protection
      feePayer: {
        envelopeSigner: Keypair.fromSecret(FEE_PAYER_SECRET), // pays tx fees
      },
    }),
  ],
});
```

Set `FEE_PAYER_SECRET` before running the server if you use sponsored fees.

When `feePayer` is configured, the server automatically signals fee sponsorship to the client via the challenge. The client then signs only the Soroban auth entries (not the full transaction envelope). The server rebuilds the transaction with the `envelopeSigner`'s account as source and broadcasts it. Optionally, add `feeBumpSigner` inside `feePayer` to wrap the transaction in a fee bump.

> [!NOTE]
>
> **Channel open removal**
>
> The channel `open` MPP action was removed in v0.7 as it was dead code — the Soroban contract has no on-chain open entrypoint. Channels are created on-chain directly by signing and broadcasting a deploy transaction. See the [MPP Session Guide](https://developers.stellar.org/docs/build/agentic-payments/mpp/channel-guide.md) for channel setup details.

## Learn more

* [@stellar/mpp on GitHub](https://github.com/stellar/stellar-mpp-sdk) — Full API reference, channel mode, and examples
* [@stellar/mpp (npm)](https://www.npmjs.com/package/@stellar/mpp) — npm package for MPP on Stellar
* [MPP Specification](https://mpp.dev) — Official MPP protocol specification and whitepaper
* [mppx (npm)](https://www.npmjs.com/package/mppx) — Core MPP framework library used by both server and client
* [Signing Soroban invocations](https://developers.stellar.org/docs/build/guides/transactions/signing-soroban-invocations.md) — Auth-entry signing on Stellar
* [one-way-channel contract](https://github.com/stellar-experimental/one-way-channel) — Soroban contract powering the channel payment mode
* [MPP Session Guide](https://developers.stellar.org/docs/build/agentic-payments/mpp/channel-guide.md) — Set up off-chain payment channels for high-frequency payments
