# MPP quickstart

Build a server that accepts machine payments with crypto or fiat.

## MPP payment endpoint builder 

Learn how to build and deploy a server that accepts both crypto and shared payment token (SPT) payments using the Machine Payments Protocol (MPP).

### Install the Stripe Node library

Install the package and import it in your code. Alternatively, if you’re starting from scratch and need a package.json file, download the project files using the Download link in the code editor.

#### npm

Install the library:

```bash
npm install --save stripe
```

#### GitHub

Or download the stripe-node library source code directly [from GitHub](https://github.com/stripe/stripe-node).

### Install the Stripe Ruby library

Install the Stripe ruby gem and require it in your code. Alternatively, if you’re starting from scratch and need a Gemfile, download the project files using the link in the code editor.

#### Terminal

Install the gem:

```bash
gem install stripe
```

#### Bundler

Add this line to your Gemfile:

```bash
gem 'stripe'
```

#### GitHub

Or download the stripe-ruby gem source code directly [from GitHub](https://github.com/stripe/stripe-ruby).

### Install the Stripe Java library

Add the dependency to your build and import the library. Alternatively, if you’re starting from scratch and need a sample pom.xml file (for Maven), download the project files using the link in the code editor.

#### Maven

Add the following dependency to your POM and replace {VERSION} with the version number you want to use.

```bash
<dependency>\n<groupId>com.stripe</groupId>\n<artifactId>stripe-java</artifactId>\n<version>{VERSION}</version>\n</dependency>
```

#### Gradle

Add the dependency to your build.gradle file and replace {VERSION} with the version number you want to use.

```bash
implementation "com.stripe:stripe-java:{VERSION}"
```

#### GitHub

Download the JAR directly [from GitHub](https://github.com/stripe/stripe-java/releases/latest).

### Install the Stripe Python package

Install the Stripe package and import it in your code. Alternatively, if you’re starting from scratch and need a requirements.txt file, download the project files using the link in the code editor.

#### pip

Install the package through pip:

```bash
pip3 install stripe
```

#### GitHub

Download the stripe-python library source code directly [from GitHub](https://github.com/stripe/stripe-python).

### Install the Stripe PHP library

Install the library with composer and initialize with your secret API key. Alternatively, if you’re starting from scratch and need a composer.json file, download the files using the link in the code editor.

#### Composer

Install the library:

```bash
composer require stripe/stripe-php
```

#### GitHub

Or download the stripe-php library source code directly [from GitHub](https://github.com/stripe/stripe-php).

### Set up your server

Add the dependency to your build and import the library. Alternatively, if you’re starting from scratch and need a go.mod file, download the project files using the link in the code editor.

#### Go

Make sure to initialize with Go Modules:

```bash
go get -u github.com/stripe/stripe-go/v85
```

#### GitHub

Or download the stripe-go module source code directly [from GitHub](https://github.com/stripe/stripe-go).

### Install the Stripe.net library

Install the package with .NET or NuGet. Alternatively, if you’re starting from scratch, download the files which contains a configured .csproj file.

#### dotnet

Install the library:

```bash
dotnet add package Stripe.net
```

#### NuGet

Install the library:

```bash
Install-Package Stripe.net
```

#### GitHub

Or download the Stripe.net library source code directly [from GitHub](https://github.com/stripe/stripe-dotnet).

### Install the Stripe libraries

Install the packages and import them in your code. Alternatively, if you’re starting from scratch and need a `package.json` file, download the project files using the link in the code editor.

Install the libraries:

```bash
npm install --save stripe @stripe/stripe-js next
```

### Initialize Stripe

Set up the Stripe client with your secret key, and set the API version to `2026-03-04.preview`. The client processes payment for both crypto and SPT payment methods.

Make sure you have [crypto payins enabled](https://support.stripe.com/questions/get-started-with-pay-with-crypto) for your Stripe account.

### Create the PaymentIntent handler

Create a function that determines where to send crypto payments. Either extract the address from an existing payment header for retries or verification, or [create a new PaymentIntent](https://docs.stripe.com/api/payment_intents/create.md) to generate a new deposit address.

When you create a [PaymentIntent](https://docs.stripe.com/api/payment_intents.md) with the `crypto` *payment method* (PaymentMethods represent your customer's payment instruments, used with the Payment Intents or Setup Intents APIs) and `deposit` mode, specify the networks you want to support using `deposit_options`. Stripe returns deposit addresses for the requested networks. The function extracts the Tempo network address that clients use to send *stablecoins* (A cryptocurrency that's pegged to the value of a fiat currency or other asset in order to limit volatility).

### Create a server-side payment handler SPTs

Set up a server-side payment handler for SPTs using [Mppx.create](https://mpp.dev/sdk/typescript/server/Mppx.create). Configure it to accept cards and Link payments using Stripe payment processing.

### Create the Hono app

Initialize the Hono server that hosts both payment endpoints.

### Create the FastAPI app

Initialize the FastAPI server that hosts the payment endpoint.

### Create SPT test helper

Add a `POST` endpoint to create SPTs for testing.

In production, the calling client’s backend would issue SPTs, not your protected endpoint.

### Create crypto payment endpoint

Define the `/crypto/paid` endpoint that accepts crypto payments.

- Create a server-side payment handler for Tempo using [Mppx.create](https://mpp.dev/sdk/typescript/server/Mppx.create).
- Returns a `402 Payment Required` response with payment details when the request doesn’t include a valid payment.
- Verifies payment challenges for each request.
- Allow access when the request includes a valid payment credential.

### Create crypto payment endpoint

Define the `/paid` endpoint that accepts crypto payments on Tempo.

- Create a server-side payment handler for Tempo with [Mpp.create](https://mpp.dev/sdk/python/server/Mpp.create).
- Check each request for a valid payment credential.
- If the request doesn’t include a valid payment credential, return a `402 Payment Required` response with the payment details from the Tempo handler.
- If the request includes a payment challenge, verify it with the Tempo handler.
- Allow access only after the payment credential or challenge is valid.

### Create SPT payment endpoint

Define the `/spt/paid` endpoint that accepts SPT payments.

- Returns a `402 Payment Required` response with payment details when the request doesn’t include a valid payment.
- Verifies payment challenges for each request.
- Allow access when the request includes a valid payment credential.

### Configure the charge method

Set up the client-side charge method to create SPTs. The `createToken` function calls your backend’s `/create-spt` endpoint to generate shared payment tokens.

### Get the payment challenge

Fetch the `402` challenge from the protected endpoint to understand the payment requirements.

### Provide a payment method

Specify the payment method to use. In this example, the code uses a test card payment method.

### Create payment credential

Use the charge method to create a payment credential from the challenge and payment method.

### Retry with credential

Make another request to the protected endpoint with the payment credential in the `Authorization` header.

### Run the server

Build and run your server to test both endpoints.

```bash
npm run dev
```

### Run the server

Build and run your server to test the endpoint at `http://localhost:4242/paid`.

```bash
uv run python main.py
```

### Test without payment

Make a request to the crypto endpoint without payment to confirm it returns a `402` status code with payment details.

```bash
curl http://localhost:4242/crypto/paid
```

You’ll receive a `402 Payment Required` response with Machine Payment Protocol (MPP) payment information.

### Test without payment

Make a request to the endpoint without payment to confirm it returns a `402` status code with payment details.

```bash
curl http://localhost:4242/paid
```

You’ll receive a `402 Payment Required` response with Machine Payment Protocol (MPP) payment information.

### Test crypto payments

Use Tempo’s [mppx](https://www.npmjs.com/package/mppx) tool to test the crypto payment flow. The tool handles MPP and completes the payment automatically.

```bash
mppx http://localhost:4242/crypto/paid
```

You can create and fund a test wallet with `mppx account create` and `mppx account fund`. The server returns the content after successful payment. You can confirm the payment in the [Stripe Dashboard](https://dashboard.stripe.com) under **Payments**.

### Test crypto payments

Use Tempo’s [mppx](https://www.npmjs.com/package/mppx) tool to test the crypto payment flow. The tool handles MPP and completes the payment automatically.

```bash
mppx http://localhost:4242/paid
```

You can create and fund a test wallet with `mppx account create` and `mppx account fund`. After a successful payment, the server returns the content. You can confirm the payment in the [Stripe Dashboard](https://dashboard.stripe.com) under **Payments**.

### Test SPT payments

For SPT payments, use the included `client.ts` script that demonstrates the full payment flow with SPTs.

```bash
npx tsx client.ts
```

The client script:

1. Fetches the `402` challenge from `/spt/paid`
1. Creates an SPT using the `/create-spt` helper endpoint
1. Creates a payment credential
1. Retries the request with the credential to access the protected resource

You can confirm the payment in the [Stripe Dashboard](https://dashboard.stripe.com) under **Payments**.

- Node.js 20+
- npm or pnpm package manager
- Stripe account with crypto payments enabled
- Python 3.12+
- [uv](https://github.com/astral-sh/uv) package manager
- Stripe account with crypto payments enabled
1. Install dependencies

~~~
npm install
~~~

2. Configure environment variables

~~~
cp .env.template .env
\# Edit .env with your credentials
~~~

3. Run the server

~~~
npm run dev
~~~
1. Install dependencies

~~~
uv sync
~~~

2. Configure environment variables

~~~
cp .env.template .env
\# Edit .env with your credentials
~~~

3. Run the server

~~~
uv run python main.py
~~~
The server provides two endpoints:

- `/crypto/paid`: - Accepts crypto payments on Tempo
- `/spt/paid`: - Accepts shared payment tokens (SPTs) payments (cards and Link)
The server provides one endpoint:

- `/paid`: - Accepts crypto payments on Tempo
Make a request without payment to see the `402` response:

~~~
curl http://localhost:4242/crypto/paid
curl http://localhost:4242/spt/paid
~~~
Make a request without payment to see the `402` response:

~~~
curl http://localhost:4242/paid
~~~
[mppx](https://www.npmjs.com/package/mppx) is a command-line tool for testing crypto MPP endpoints:

~~~
npx mppx http://localhost:4242/crypto/paid
~~~

You can create and fund a test wallet with:

~~~
npx mppx account create
npx mppx account fund
~~~
[mppx](https://www.npmjs.com/package/mppx) is a command-line tool for testing crypto MPP endpoints:

~~~
npx mppx http://localhost:4242/paid
~~~

You can create and fund a test wallet with:

~~~
npx mppx account create
npx mppx account fund
~~~
import stripe
\# Stripe handles payment processing and provides the crypto deposit address.
STRIPE_SECRET_KEY = os.getenv("STRIPE_SECRET_KEY")
if not STRIPE_SECRET_KEY:
    raise ValueError("STRIPE_SECRET_KEY environment variable is required")

stripe.api_key = STRIPE_SECRET_KEY
stripe.api_version = "2026-03-04.preview"
stripe.set_app_info(
    "stripe-samples/machine-payments",
    url="https://github.com/stripe-samples/machine-payments",
    version="1.0.0",
)

# Secret used to secure payment challenges.
# https://mpp.dev/protocol/challenges#challenge-binding
mpp_secret_key = os.urandom(32).hex()
\# In-memory cache for deposit addresses (TTL: 5 minutes, max 1024 entries)
# NOTE: For production, use a distributed cache like Redis instead of cachetools
payment_cache: TTLCache[str, bool] = TTLCache(maxsize=1024, ttl=300)


def _extract_recipient_from_authorization(authorization: str | None) -> str | None:
    if not authorization or not authorization.startswith("Payment "):
        return None

    credential = Credential.from_authorization(authorization)
    request = _b64_decode(credential.challenge.request)
    to_address = request.get("recipient")

    if to_address and isinstance(to_address, str):
        normalized = to_address.lower()
        if normalized not in payment_cache:
            raise ValueError("Invalid payTo address: not found in server cache")
        return normalized

    raise ValueError("PaymentIntent did not return expected crypto deposit details")


async def create_pay_to_address(request: Request) -> str:
    """
    This function determines where payments should be sent. It either:
    1. Extracts the address from an existing payment header (for retry/verification), or
    2. Creates a new Stripe PaymentIntent to generate a fresh deposit address.
    """
    recipient = _extract_recipient_from_authorization(
        request.headers.get("authorization")
    )
    if recipient:
        return recipient

    # Create a new PaymentIntent to get a fresh crypto deposit address.
    decimals = 6  # USDC has 6 decimals
    amount_in_cents = int(10000 / (10 ** (decimals - 2)))

    payment_intent = stripe.PaymentIntent.create(
        amount=amount_in_cents,
        currency="usd",
        payment_method_types=["crypto"],
        payment_method_data={"type": "crypto"},
        payment_method_options=cast(
            Any,
            {
                "crypto": {
                    "mode": "deposit",
                    "deposit_options": {"networks": ["tempo"]},
                }
            },
        ),
        confirm=True,
    )

    next_action = payment_intent.get("next_action", {})
    deposit_details = next_action.get("crypto_display_details", {})
    if not deposit_details:
        raise ValueError("PaymentIntent did not return expected crypto deposit details")

    deposit_addresses = deposit_details.get("deposit_addresses", {})
    tempo_address = deposit_addresses.get("tempo", {})
    pay_to_address = tempo_address.get("address")

    if not pay_to_address:
        raise ValueError("PaymentIntent did not return expected crypto deposit details")

    print(
        f"Created PaymentIntent {payment_intent['id']} "
        f"for ${amount_in_cents / 100:.2f} -> {pay_to_address}"
    )

    normalized = pay_to_address.lower()
    payment_cache[normalized] = True
    return normalized
\# Tempo token address for USD stablecoin
PATH_USD = "0x" + "20c" + "0" * 37

app = FastAPI(title="MPP REST API")
\# GET /paid - Accept crypto payments on Tempo
@app.get("/paid")
async def get_api(request: Request):
    recipient_address = await create_pay_to_address(request)

    mpp = Mpp.create(
        method=tempo(
            currency=PATH_USD,
            recipient=recipient_address,
            intents={"charge": ChargeIntent()},
            chain_id=42431,
        ),
        secret_key=mpp_secret_key,
    )

    result = await mpp.charge(
        authorization=request.headers.get("authorization"),
        amount="0.01",
    )

    if isinstance(result, Challenge):
        return JSONResponse(
            status_code=402,
            content={"error": "Payment required"},
            headers={"WWW-Authenticate": result.to_www_authenticate(mpp.realm)},
        )

    _credential, receipt = result

    response = JSONResponse(content={"foo": "bar"})
    response.headers["Authentication-Info"] = receipt.to_payment_receipt()
    return response
if __name__ == "__main__":
    print("Server listening at http://localhost:4242")
    uvicorn.run(app, host="0.0.0.0", port=4242)
// Stripe handles payment processing for both crypto and SPT methods
if (!process.env.STRIPE_SECRET_KEY) {
  console.error("STRIPE_SECRET_KEY environment variable is required");
  process.exit(1);
}

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
  apiVersion: "2026-03-04.preview" as any,
  appInfo: {
    name: "stripe-samples/machine-payments",
    url: "https://github.com/stripe-samples/machine-payments",
    version: "1.0.0",
  },
});

// Secret used to secure payment challenges
// https://mpp.dev/protocol/challenges#challenge-binding
const mppSecretKey = crypto.randomBytes(32).toString('base64');
// In-memory cache for deposit addresses (TTL: 5 minutes)
// NOTE: For production, use a distributed cache like Redis instead of node-cache
const paymentCache = new NodeCache({ stdTTL: 300, checkperiod: 60 });

// This function determines where crypto payments should be sent. It either:
// 1. Extracts the address from an existing payment header (for retry/verification), or
// 2. Creates a new Stripe PaymentIntent to generate a fresh deposit address.
async function createPayToAddress(request: Request): Promise<string> {
  const authHeader = request.headers.get('authorization')

  if (authHeader && Credential.extractPaymentScheme(authHeader)) {
    const credential = Credential.fromRequest(request)
    const toAddress = credential.challenge.request.recipient as `0x${string}`
    if (!paymentCache.has(toAddress)) {
      throw new Error('Invalid payTo address: not found in server cache')
    }
    return toAddress
  }

  // Create a new PaymentIntent to get a fresh crypto deposit address
  const decimals = 6; // USDC has 6 decimals
  const amountInCents = Number(1000000) / Math.pow(10, decimals - 2);

  const paymentIntent = await stripe.paymentIntents.create({
    amount: amountInCents,
    currency: "usd",
    payment_method_types: ["crypto"],
    payment_method_data: {
      type: "crypto",
    },
    payment_method_options: {
      crypto: {
        mode: "deposit",
        deposit_options: { networks: ["tempo"] },
      } as any,
    },
    confirm: true,
  });

  if (
    !paymentIntent.next_action ||
    !("crypto_display_details" in paymentIntent.next_action)
  ) {
    throw new Error(
      "PaymentIntent did not return expected crypto deposit details",
    );
  }

  // Extract the Tempo network deposit address from the PaymentIntent
  const depositDetails = paymentIntent.next_action
    .crypto_display_details as any;
  const payToAddress = depositDetails.deposit_addresses["tempo"]
    .address as string;

  console.log(
    `Created PaymentIntent ${paymentIntent.id} for $${(
      amountInCents / 100
    ).toFixed(2)} -> ${payToAddress}`,
  );

  paymentCache.set(payToAddress, true);
  return payToAddress;
}
// Create Mppx instance for SPT payments (cards and Link)
const mppxSpt = Mppx.create({
  methods: [
    mppxStripe.charge({
      networkId: 'internal',
      paymentMethodTypes: ['card', 'link'],
      secretKey: process.env.STRIPE_SECRET_KEY,
    }),
  ],
  secretKey: mppSecretKey
});
const PATH_USD = '0x20c0000000000000000000000000000000000000';

const app = new Hono()
// GET /crypto/paid - Accept crypto payments on Tempo
app.get('/crypto/paid', async (c) => {
  const request = c.req.raw

  const recipientAddress = await createPayToAddress(request)
  const mppxCrypto = Mppx.create({
    methods: [
      tempo.charge({
        currency: PATH_USD,
        recipient: recipientAddress,
        testnet: true,
      }),
    ],
    secretKey: mppSecretKey,
  })

  const response = await mppxCrypto.charge({ amount: '1' })(request)

  if (response.status === 402) {
    return response.challenge
  }

  return response.withReceipt(
    Response.json({
      data: 'Premium content delivered via crypto!',
      timestamp: new Date().toISOString(),
    })
  )
})
// GET /spt/paid - Accept SPT payments (cards and Link)
app.get('/spt/paid', async (c) => {
  const request = c.req.raw

  const result = await mppxSpt.charge({
    amount: '1',
    currency: 'usd',
  })(request);

  if (result.status === 402) {
    return result.challenge
  }

  return result.withReceipt(
    Response.json({
      data: 'Premium content delivered via SPT!',
      timestamp: new Date().toISOString(),
    })
  )
})
serve({
  fetch: app.fetch,
  port: 4242,
});

console.log(`Server listening at http://localhost:4242`);
console.log(`Crypto endpoint: http://localhost:4242/crypto/paid`);
console.log(`SPT endpoint: http://localhost:4242/spt/paid`);
## Next steps

- [Machine payments](https://docs.stripe.com/payments/machine.md)
- [MPP](https://docs.stripe.com/payments/machine/mpp.md)
