# 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
\ncom.stripe\nstripe-java\n{VERSION}\n
```
#### 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
```
### Initialise 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
Initialise the Hono server that hosts both payment endpoints.
### Create the FastAPI app
Initialise 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 back end 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 `Authorisation` 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 {
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)