Package Exports
- @stellar/mpp
- @stellar/mpp/channel
- @stellar/mpp/channel/client
- @stellar/mpp/channel/server
- @stellar/mpp/charge
- @stellar/mpp/charge/client
- @stellar/mpp/charge/server
- @stellar/mpp/env
Readme
@stellar/mpp
Stellar blockchain payment method for the Machine Payments Protocol (MPP). Enables machine-to-machine payments using Soroban SAC token transfers on the Stellar network, with optional support for one-way payment channels for high-frequency off-chain payments.
Payment modes
Charge (one-time transfers)
Each payment is a Soroban SAC transfer settled on-chain individually.
Client Server Stellar
| | |
| GET /resource | |
|------------------------------>| |
| | |
| 402 Payment Required | |
| (challenge: pay 0.01 USDC) | |
|<------------------------------| |
| | |
| prepareTransaction ----------------- (simulate) ------------>|
| Sign SAC transfer | |
| Send credential (XDR) | |
|------------------------------>| |
| | sendTransaction ------------>|
| | getTransaction (poll) ------>|
| 200 OK + data | |
|<------------------------------| |Two credential modes:
- Pull (default) — client prepares the transaction, server submits it:
- Sponsored (
feePayerconfigured on server): client signs only Soroban auth entries using an all-zeros placeholder source; server rebuilds the tx with its own account as source, signs, and broadcasts - Unsponsored: client builds and signs the full transaction; server broadcasts as-is
- Sponsored (
- Push — client broadcasts the transaction itself, sends the tx hash for server verification (not compatible with
feePayer)
Channel (off-chain commitments)
Uses a one-way payment channel contract. The funder deposits tokens into a channel once, then makes many off-chain payments by signing cumulative commitments — no per-payment on-chain transactions.
Client (Funder) Server (Recipient) Stellar
| | |
| [Channel opened on-chain | |
| with initial deposit] | |
| (see "open" action below) | |
| | |
| GET /resource | |
|------------------------------>| |
| | |
| 402 Payment Required | |
| (pay 1 XLM, cumulative: 0) | |
|<------------------------------| |
| | |
| simulate prepare_commitment------------------------------------>|
| Sign commitment off-chain | |
| (cumulative: 1 XLM + sig) | |
|------------------------------>| |
| | simulate prepare_commitment --->|
| | Verify ed25519 signature |
| 200 OK + data | |
|<------------------------------| |
| | |
| GET /resource (again) | |
|------------------------------>| |
| | |
| 402 (pay 1 XLM, | |
| cumulative: 1 XLM) | |
|<------------------------------| |
| | |
| simulate prepare_commitment------------------------------------>|
| Sign commitment | |
| (cumulative: 2 XLM + sig) | |
|------------------------------>| |
| | simulate prepare_commitment --->|
| | Verify, 200 OK |
|<------------------------------| |
| | |
| | [close channel when convenient] |
| | sendTransaction (close) ------->|Prerequisites
corepack enableInstall
corepack enable
pnpm installFor end users:
npm install @stellar/mpp mppx @stellar/stellar-sdkQuick start
Server (charge)
import { Mppx, stellar } from '@stellar/mpp/charge/server'
import { USDC_SAC_TESTNET } from '@stellar/mpp'
const mppx = Mppx.create({
secretKey: process.env.MPP_SECRET_KEY,
methods: [
stellar.charge({
recipient: process.env.STELLAR_RECIPIENT!, // your Stellar public key (G...)
currency: USDC_SAC_TESTNET,
network: 'testnet',
}),
],
})
// Express (example servers use helmet and rate limiting)
export async function handler(request: Request) {
const result = await mppx.charge({
amount: '0.01',
description: 'Premium API access',
})(request)
if (result.status === 402) return result.challenge
return result.withReceipt(Response.json({ data: 'paid content here' }))
}Client (charge)
import { Keypair } from '@stellar/stellar-sdk'
import { Mppx, stellar } from '@stellar/mpp/charge/client'
// Polyfills global fetch — 402 responses are handled automatically
Mppx.create({
methods: [
stellar.charge({
keypair: Keypair.fromSecret('S...'),
}),
],
})
const response = await fetch('https://api.example.com/paid-resource')
const data = await response.json()Server (channel)
import { Mppx, stellar, Store } from '@stellar/mpp/channel/server'
const mppx = Mppx.create({
secretKey: process.env.MPP_SECRET_KEY,
methods: [
stellar.channel({
channel: 'CABC...', // deployed one-way-channel contract address
commitmentKey: 'GFUNDER...', // ed25519 public key for verifying commitments
store: Store.memory(), // tracks cumulative amounts + replay protection
network: 'testnet',
}),
],
})
export async function handler(request: Request) {
const result = await mppx.channel({
amount: '1', // 1 XLM per request (human-readable)
description: 'API call',
})(request)
if (result.status === 402) return result.challenge
return result.withReceipt(Response.json({ data: 'paid content here' }))
}Client (channel)
import { Keypair } from '@stellar/stellar-sdk'
import { Mppx, stellar } from '@stellar/mpp/channel/client'
Mppx.create({
methods: [
stellar.channel({
commitmentKey: Keypair.fromSecret('S...'), // ed25519 key matching the channel's commitment_key
}),
],
})
const response = await fetch('https://api.example.com/paid-resource')
const data = await response.json()API
Exports
| Path | Exports |
|---|---|
@stellar/mpp |
ChargeMethods, ChannelMethods, constants (USDC_SAC_TESTNET, XLM_SAC_MAINNET, etc.), toBaseUnits, fromBaseUnits, resolveKeypair, Env, Logger (type), error types (StellarMppError, PaymentVerificationError, ChannelVerificationError, SettlementError) |
@stellar/mpp/charge |
charge (method schema) |
@stellar/mpp/charge/client |
stellar, charge, Mppx |
@stellar/mpp/charge/server |
stellar, charge, Mppx, Store, Expires, resolveKeypair |
@stellar/mpp/channel |
channel (method schema) |
@stellar/mpp/channel/client |
stellar, channel, Mppx |
@stellar/mpp/channel/server |
stellar, channel, close, getChannelState, watchChannel, resolveKeypair, Mppx, Store, Expires, ChannelState (type), ChannelEvent (type) |
@stellar/mpp/env |
parseRequired, parseOptional, parsePort, parseStellarPublicKey, parseStellarSecretKey, parseContractAddress, parseHexKey, parseCommaSeparatedList, parseNumber |
Server options (charge)
stellar.charge({
recipient: string, // Stellar public key (G...) or contract (C...)
currency: string, // SAC contract address
network?: 'testnet' | 'public', // default: 'testnet'
decimals?: number, // default: 7
rpcUrl?: string, // custom Soroban RPC URL
signer?: Keypair | string, // source account for sponsored tx signing
feeBumpSigner?: Keypair | string, // wraps all txs in FeeBumpTransaction
store?: Store.Store, // replay protection
maxFeeBumpStroops?: number, // max fee bump in stroops (default: 10,000,000)
pollMaxAttempts?: number, // max polling attempts (default: 30)
pollDelayMs?: number, // delay between poll attempts in ms (default: 1,000)
pollTimeoutMs?: number, // overall poll timeout in ms (default: 30,000)
simulationTimeoutMs?: number, // simulation timeout in ms (default: 10,000)
logger?: Logger, // structured logger (default: no-op)
})Client options (charge)
stellar.charge({
keypair?: Keypair, // Stellar Keypair (or use secretKey)
secretKey?: string, // Stellar secret key (S...)
mode?: 'push' | 'pull', // default: 'pull'
timeout?: number, // tx timeout in seconds (default: 180)
decimals?: number, // default: 7
rpcUrl?: string, // custom Soroban RPC URL
pollMaxAttempts?: number, // max polling attempts (default: 30)
pollDelayMs?: number, // delay between poll attempts in ms (default: 1,000)
pollTimeoutMs?: number, // overall poll timeout in ms (default: 30,000)
simulationTimeoutMs?: number, // simulation timeout in ms (default: 10,000)
onProgress?: (event) => void, // lifecycle callback
})Server options (channel)
stellar.channel({
channel: string, // on-chain channel contract address (C...)
commitmentKey: string | Keypair,// ed25519 public key for verifying commitments
network?: 'testnet' | 'public', // default: 'testnet'
decimals?: number, // default: 7
rpcUrl?: string, // custom Soroban RPC URL
sourceAccount?: string, // funded G... address for simulations
store?: Store.Store, // replay protection + cumulative amount tracking
signer?: Keypair | string, // keypair for signing close/open transactions
feeBumpSigner?: Keypair | string, // fee bump signer for close/open transactions
checkOnChainState?: boolean, // detect on-chain disputes (default: false)
onDisputeDetected?: (state) => void, // callback when close_start detected
maxFeeBumpStroops?: number, // max fee bump in stroops (default: 10,000,000)
pollMaxAttempts?: number, // max polling attempts (default: 30)
pollDelayMs?: number, // delay between poll attempts in ms (default: 1,000)
pollTimeoutMs?: number, // overall poll timeout in ms (default: 30,000)
simulationTimeoutMs?: number, // simulation timeout in ms (default: 10,000)
logger?: Logger, // structured logger (default: no-op)
})Client options (channel)
stellar.channel({
commitmentKey?: Keypair, // ed25519 Keypair for signing commitments
commitmentSecret?: string, // ed25519 secret key (S...)
rpcUrl?: string, // custom Soroban RPC URL
simulationTimeoutMs?: number, // simulation timeout in ms (default: 10,000)
sourceAccount?: string, // funded G... address for simulations
onProgress?: (event) => void, // lifecycle callback
})Progress events
The onProgress callback receives events at each stage:
Charge events:
| Event | Fields | When |
|---|---|---|
challenge |
recipient, amount, currency |
Challenge received |
signing |
--- | Before signing |
signed |
transaction |
After signing |
paying |
--- | Before broadcast (push mode) |
confirming |
hash |
Polling for confirmation (push mode) |
paid |
hash |
Transaction confirmed (push mode) |
Channel events:
| Event | Fields | When |
|---|---|---|
challenge |
channel, amount, cumulativeAmount |
Challenge received |
signing |
--- | Before signing commitment |
signed |
cumulativeAmount |
Commitment signed |
Fee sponsorship
The server can decouple sequence-number management from fee payment:
signer--- keypair providing the source account and sequence number for sponsored transactions.feeBumpSigner--- optional dedicated fee payer. When set, all submitted transactions are wrapped in aFeeBumpTransactionsigned by this key.
import { stellar } from '@stellar/mpp/charge/server'
stellar.charge({
recipient: 'G...',
currency: USDC_SAC_TESTNET,
signer: Keypair.fromSecret('S...'), // source account
feeBumpSigner: Keypair.fromSecret('S...'), // pays all fees
})The client is automatically informed of fee sponsorship via methodDetails.feePayer in the challenge.
Replay protection
Provide an mppx Store to prevent challenge reuse:
import { Store } from '@stellar/mpp/charge/server'
stellar.charge({
recipient: 'G...',
currency: USDC_SAC_TESTNET,
store: Store.memory(), // or Store.upstash(), Store.cloudflare()
})Logger
Pass a logger to charge or channel servers for structured debug and warning output. The Logger interface is compatible with pino out of the box:
import pino from 'pino'
import { stellar } from '@stellar/mpp/charge/server'
const logger = pino({ level: 'debug' })
stellar.charge({
recipient: 'G...',
currency: USDC_SAC_TESTNET,
logger, // pino satisfies the Logger interface natively
})One-way payment channels
Payment channels allow many off-chain micro-payments with minimal on-chain transactions. The one-way-channel contract is deployed on Soroban --- no additional npm dependency is needed.
Prerequisites:
- Deploy the channel contract on Stellar (see one-way-channel repo)
- The funder opens the channel with an initial token deposit, a
commitment_key(ed25519 public key), the recipient address, and a refund waiting period - Both client (funder) and server (recipient) use the channel contract address
How it works:
- The client signs cumulative commitment amounts off-chain using the ed25519 commitment key
- The server verifies signatures by simulating
prepare_commitmenton the channel contract and checking the ed25519 signature - A
Storeis required on the server to track cumulative amounts across requests - The server can call
close()on-chain at any time to settle accumulated payments
Opening a channel via the SDK:
The SDK also supports opening a channel through the MPP 402 flow using the open action. The client builds the deploy transaction externally (e.g., stellar contract deploy --send=no), then passes it as openTransaction context alongside an initial commitment. The server verifies the commitment signature and broadcasts the deploy transaction on-chain. See examples/channel-open.ts for a complete example.
On-chain close (server-side):
import { close } from '@stellar/mpp/channel/server'
await close({
channel: 'CABC...', // channel contract address
amount: 8000000n, // commitment amount to close with
signature: commitmentSigBytes, // ed25519 signature from the latest commitment
signer: recipientKeypair, // keypair to sign the close transaction
network: 'testnet',
})Constants
| Constant | Value |
|---|---|
USDC_SAC_MAINNET |
CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI |
USDC_SAC_TESTNET |
CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA |
XLM_SAC_MAINNET |
CAS3J7GYLGVE45MR3HPSFG352DAANEV5GGMFTO3IZIE4JMCDALQO57Y |
XLM_SAC_TESTNET |
CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC |
Breaking changes from 0.1.0
- Package renamed:
stellar-mpp-sdkis now@stellar/mpp. All import paths change accordingly. - Import paths changed:
stellar-mpp-sdk/clientandstellar-mpp-sdk/serverare no longer valid. Use@stellar/mpp/charge/clientand@stellar/mpp/charge/serverinstead. - Root export renamed:
Methodsis nowChargeMethods. - Store keys changed: Store key format updated to
stellar:{intent}:{type}:{id}. Existing stored data is not backwards compatible. resolveKeypairmoved: Now exported from the root (@stellar/mpp) and from@stellar/mpp/charge/server, no longer fromstellar-mpp-sdk/server.
Environment variables
Example .env files are provided for each demo:
| File | Purpose |
|---|---|
examples/.env.charge-server.example |
Charge server (recipient key, security settings) |
examples/.env.charge-client.example |
Charge client (secret key, server URL) |
examples/.env.channel-server.example |
Channel server (contract, commitment key, security) |
examples/.env.channel-client.example |
Channel client (commitment secret, server URL) |
Copy the relevant .example file, remove the .example suffix, and fill in your values.
Demo
See demo/README.md for full instructions. Quick start:
# All-in-one (prompts for keys)
./demo/run.sh
# Or two terminals:
STELLAR_RECIPIENT=G... npx tsx examples/charge-server.ts # Terminal 1
STELLAR_SECRET=S... npx tsx examples/charge-client.ts # Terminal 2
# Or npm scripts:
STELLAR_RECIPIENT=G... pnpm demo:charge-server # Terminal 1
STELLAR_SECRET=S... pnpm demo:charge-client # Terminal 2Browser UI available at http://localhost:3000/demo once the server is running.
Channel end-to-end (with on-chain settlement)
Run the full channel lifecycle --- deploy, off-chain payments, and on-chain close --- in a single command:
# Build the one-way-channel WASM first (see https://github.com/stellar-experimental/one-way-channel)
WASM_PATH=path/to/channel.wasm ./demo/run-channel-e2e.shSee demo/channel-e2e-output.txt for example output with Stellar Expert links.
Project structure
Source lives in sdk/src/ with colocated tests (*.test.ts). Each payment mode follows a twin client/server pattern: Methods.ts (Zod schema) + client/ + server/. Shared utilities in shared/ are internal. See examples/ for runnable servers/clients and demo/ for shell scripts.
Development
make help # Show all available commands
make check # Run full quality pipeline (format, lint, typecheck, test, build)
make test # Run tests once
make test-watch # Run tests in watch modeSee the Makefile for all targets.
pnpm install # Install deps
pnpm run build # Compile TypeScript
pnpm run check:types # Type-check without emitting
pnpm run lint # Run ESLint
pnpm run format:check # Check formatting
pnpm test # Run tests (vitest)License
MIT