Solana extension for MPP
solana-mpp is an early-stage TypeScript project in the AI payments / x402 ecosystem. It currently has 13 GitHub stars and 1 forks.
SPL token payments for the Machine Payments Protocol
High-throughput, low-cost token transfers meet HTTP 402 Payment Required
| Feature | Detail | |
|---|---|---|
| ~400ms | Speed | Block times with instant finality — payments settle before the HTTP request times out |
| $0.00025 | Cost | Average transaction fee — viable even for micropayments |
| Any SPL | Token Flexibility | Pay with USDC, USDT, or custom tokens — not locked to a single currency |
| DeFi | Ecosystem | Access to Solana's deep liquidity and wallet infrastructure |
| On-chain | Programmability | Verification via reference keys eliminates external payment processors |
Client Server
│ │
├── GET /api/resource ──────────────►│
│ │
│◄── 402 Payment Required ──────────┤
│ (amount, recipient ATA, │
│ mint, reference key) │
│ │
│ Signs & submits SPL token │
│ transfer on-chain │
│ │
├── GET /api/resource ──────────────►│
│ + payment credential │
│ (tx signature) │
│ │
│ Server verifies on-chain: │
│ ✓ reference key in tx │
│ ✓ correct mint & amount │
│ ✓ transfer to recipient ATA │
│ │
│◄── 200 OK + receipt ──────────────┤
│ (resource data) │
│ │
All verification happens on-chain. The server reads the transaction from Solana and confirms the token transfer — no external payment processor, no webhooks, no polling third-party APIs.
A single payment per request. The client pays the exact amount and gets access to the resource. Simple, stateless, and ideal for:
A client deposits tokens upfront and makes multiple requests against the balance. The server tracks usage and refunds unused tokens when the session closes. Ideal for:
Session lifecycle:
npm install solana-mpp mppx @solana/web3.js @solana/spl-token
@solana/web3.jsand@solana/spl-tokenare peer dependencies — you likely already have them if you're building on Solana.
import { Mppx, Store } from 'solana-mpp/server'
import { solana } from 'solana-mpp/server'
import { Connection, PublicKey } from '@solana/web3.js'
const connection = new Connection('https://api.mainnet-beta.solana.com', 'confirmed')
const mppx = Mppx.create({
methods: [
solana.charge({
recipient: new PublicKey('YOUR_WALLET_ADDRESS'),
mint: new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'), // USDC
decimals: 6,
connection,
store: Store.memory(), // replay protection
}),
],
secretKey: process.env.MPP_SECRET_KEY!,
})
// In your HTTP handler:
async function handler(request: Request): Promise<Response> {
if (new URL(request.url).pathname === '/api/data') {
const result = await mppx.charge({
amount: '0.01', // 0.01 USDC per request
description: 'API call',
})(request)
if (result.status === 402) return result.challenge
return result.withReceipt(
Response.json({ data: 'your paid content here' })
)
}
return new Response('Not found', { status: 404 })
}
import { Mppx } from 'solana-mpp/client'
import { solana } from 'solana-mpp/client'
const mppx = Mppx.create({
methods: [
solana.charge({
wallet, // any object with { publicKey, signTransaction }
}),
],
})
// Payments happen automatically on 402 responses
const response = await mppx.fetch('https://api.example.com/api/data')
const data = await response.json()
That's it. The client automatically intercepts 402 responses, signs and submits the SPL token transfer, and retries the request with the payment proof.
solana.charge(parameters)Creates a one-time payment method for the server.
import { solana } from 'solana-mpp/server'
solana.charge({
recipient: PublicKey, // your wallet address (token recipient)
mint: PublicKey, // SPL token mint (e.g. USDC)
decimals: number, // token decimals (e.g. 6 for USDC)
network?: SolanaNetwork, // 'mainnet-beta' | 'devnet' | 'testnet' | 'localnet'
connection?: Connection, // custom RPC connection (optional)
store?: Store.Store, // for replay protection (recommended)
verifyTimeout?: number, // tx verification timeout in ms (default: 60000)
})
Usage in route handler:
const result = await mppx.charge({
amount: '0.50', // 0.50 tokens
description: 'Premium data', // shown to client
})(request)
if (result.status === 402) return result.challenge // payment needed
return result.withReceipt(Response.json({ ... })) // payment verified
solana.session(parameters)Creates a prepaid session method for the server. Requires a store for session state and a serverKeypair for signing refund transactions.
import { solana } from 'solana-mpp/server'
const sessionMethod = solana.session({
recipient: PublicKey, // your wallet address
mint: PublicKey, // SPL token mint
decimals: number, // token decimals
serverKeypair: Keypair, // signs refund transactions on close
store: Store.Store, // required — stores session state
network?: SolanaNetwork,
connection?: Connection,
verifyTimeout?: number,
})
Usage in route handler:
const result = await mppx.session({
amount: '0.01', // per-request cost
depositAmount: '1.00', // initial deposit (optional)
unitType: 'request', // label for metering
})(request)
if (result.status === 402) return result.challenge
return result.withReceipt(Response.json({ ... }))
Extended server methods:
// Deduct from a session's balance programmatically
await sessionMethod.deduct(sessionId, 500_000n) // raw token units
// Wait for a client to top up their session
await sessionMethod.waitForTopUp(sessionId, 30_000) // timeout in ms
import { Mppx, Store, Expires } from 'solana-mpp/server'
| Export | Description |
|---|---|
Mppx |
Server-side MPP handler (from mppx/server) |
Store |
Pluggable storage interface (Store.memory() for dev, bring your own for production) |
Expires |
TTL helper for store entries |
solana.charge(parameters)Creates a one-time payment method for the client.
import { solana } from 'solana-mpp/client'
solana.charge({
wallet: WalletLike | (() => WalletLike | Promise<WalletLike>),
network?: SolanaNetwork, // default: 'mainnet-beta'
connection?: Connection, // custom RPC connection
})
solana.session(parameters)Creates a prepaid session method for the client.
import { solana } from 'solana-mpp/client'
const sessionMethod = solana.session({
wallet: WalletLike | (() => WalletLike | Promise<WalletLike>),
network?: SolanaNetwork,
connection?: Connection,
})
Session lifecycle methods:
sessionMethod.close() // signal close on next request
sessionMethod.topUp() // signal top-up on next request
sessionMethod.getSession() // { sessionId, bearer } | null
sessionMethod.setSessionId(id) // update sessionId from receipt
sessionMethod.resetSession() // clear session state
sessionMethod.cleanup() // release wallet reference
WalletLikeAny object that implements this interface works — compatible with @solana/wallet-adapter, Phantom, or a raw Keypair:
interface WalletLike {
publicKey: PublicKey
signTransaction<T extends Transaction | VersionedTransaction>(tx: T): Promise<T>
}
import { Keypair, Transaction, VersionedTransaction } from '@solana/web3.js'
const keypair = Keypair.generate()
const wallet: WalletLike = {
publicKey: keypair.publicKey,
async signTransaction<T extends Transaction | VersionedTransaction>(tx: T) {
if ('partialSign' in tx) {
(tx as Transaction).partialSign(keypair)
}
return tx
},
}
import { Mppx } from 'solana-mpp/client'
| Export | Description |
|---|---|
Mppx |
Client-side payment handler with fetch() that auto-handles 402 flows |
A payment-gated joke API with a browser frontend.
import { Mppx, Store } from 'solana-mpp/server'
import { solana } from 'solana-mpp/server'
import { Connection, PublicKey } from '@solana/web3.js'
const connection = new Connection('http://localhost:8899', 'confirmed')
const store = Store.memory()
const mppx = Mppx.create({
methods: [
solana.charge({
recipient: new PublicKey('...'),
mint: new PublicKey('...'),
decimals: 6,
network: 'localnet',
connection,
store,
}),
],
secretKey: 'my-secret-key',
})
// Free endpoint
if (url.pathname === '/api/health') {
return Response.json({ status: 'ok' })
}
// Paid endpoint — 0.001 tokens per joke
if (url.pathname === '/api/joke') {
const result = await mppx.charge({
amount: '0.001',
description: 'A programming joke',
})(request)
if (result.status === 402) return result.challenge
const joke = jokes[Math.floor(Math.random() * jokes.length)]
return result.withReceipt(Response.json({ joke }))
}
import { Mppx } from 'solana-mpp/client'
import { solana } from 'solana-mpp/client'
const mppx = Mppx.create({
methods: [
solana.charge({
wallet,
network: 'localnet',
connection,
}),
],
polyfill: false,
})
const response = await mppx.fetch('/api/joke')
const { joke } = await response.json()
console.log(joke)
A session-gated API where clients deposit once and fetch multiple pages.
import { Mppx, Store } from 'solana-mpp/server'
import { solana } from 'solana-mpp/server'
import { Connection, Keypair, PublicKey } from '@solana/web3.js'
const connection = new Connection('http://localhost:8899', 'confirmed')
const store = Store.memory()
const serverKeypair = Keypair.generate() // for signing refund transactions
const mppx = Mppx.create({
methods: [
solana.session({
recipient: serverKeypair.publicKey,
mint: new PublicKey('...'),
decimals: 6,
serverKeypair,
network: 'localnet',
connection,
store,
}),
],
secretKey: 'my-secret-key',
})
// 0.01 tokens per page, 0.1 token initial deposit
if (url.pathname === '/api/data') {
const result = await mppx.session({
amount: '0.01',
depositAmount: '0.1',
unitType: 'page',
})(request)
if (result.status === 402) return result.challenge
return result.withReceipt(Response.json({ page: 1, content: '...' }))
}
import { Mppx } from 'solana-mpp/client'
import { solana } from 'solana-mpp/client'
const sessionMethod = solana.session({
wallet,
network: 'localnet',
connection,
})
const mppx = Mppx.create({
methods: [sessionMethod],
polyfill: false,
})
// First request opens the session (deposits 0.1 tokens)
const res1 = await mppx.fetch('http://localhost:5173/api/data?page=1')
// Extract sessionId from the receipt header
const receipt = JSON.parse(
Buffer.from(res1.headers.get('payment-receipt')!, 'base64').toString()
)
sessionMethod.setSessionId(receipt.reference)
// Subsequent requests use the bearer token (no new on-chain tx)
const res2 = await mppx.fetch('http://localhost:5173/api/data?page=2')
const res3 = await mppx.fetch('http://localhost:5173/api/data?page=3')
// Close the session — server refunds unused balance
sessionMethod.close()
await mppx.fetch('http://localhost:5173/api/data?page=close')
Both examples run against a local Solana validator.
# Install Solana CLI tools
sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"
# Start a local validator
solana-test-validator
cd examples/charge
npm install
npm run dev
# Opens http://localhost:5173
Click "Get a Joke" — the browser wallet pays 0.001 tokens per joke, with the balance updating after each request.
cd examples/session
npm install
npm run dev # Start server (terminal 1)
npm run client # Run CLI client (terminal 2)
The client deposits 0.1 tokens, fetches 5 pages at 0.01 tokens each, closes the session, and prints a summary showing the refunded balance.
| Network | RPC Endpoint | Use Case |
|---|---|---|
mainnet-beta |
https://api.mainnet-beta.solana.com |
Production |
devnet |
https://api.devnet.solana.com |
Testing |
testnet |
https://api.testnet.solana.com |
Testing |
localnet |
http://localhost:8899 |
Local development |
Pass a custom Connection for private RPC endpoints:
import { Connection } from '@solana/web3.js'
const connection = new Connection('https://your-rpc.example.com', 'confirmed')
solana.charge({
// ...
connection,
})
solana-mpp
├── src/
│ ├── index.ts # Root exports
│ ├── Methods.ts # Zod schemas for charge & session protocols
│ ├── types.ts # WalletLike interface, SolanaNetwork type
│ ├── constants.ts # RPC endpoint URLs
│ ├── client/
│ │ ├── Charge.ts # Signs & submits token transfers
│ │ ├── Session.ts # Manages session lifecycle (open/bearer/topUp/close)
│ │ └── Methods.ts # Client-side solana namespace
│ └── server/
│ ├── Charge.ts # Verifies one-time payments on-chain
│ ├── Session.ts # Manages session state, balance tracking, refunds
│ ├── verify.ts # Core on-chain transaction verification
│ └── Methods.ts # Server-side solana namespace
└── examples/
├── charge/ # Browser-based pay-per-joke demo
└── session/ # CLI-based session lifecycle demo
Payment verification is fully on-chain with no external dependencies:
Reference Key — Each payment challenge includes a unique reference public key. The client appends this key as a non-signer account in the SPL transfer transaction (following the Solana Pay pattern).
Transaction Discovery — The server finds the payment transaction by calling getSignaturesForAddress on the reference key, or uses the client-provided signature directly.
Transfer Validation — The server parses the transaction and verifies:
Replay Protection — Transaction signatures are stored and checked to prevent double-spending the same payment.
This implementation follows the MPP specification for HTTP 402 Payment Required payment flows, extending it with Solana SPL token transfers as the payment rail. It is compatible with any server framework that uses Web-standard Request/Response objects (Node.js, Bun, Deno, Cloudflare Workers, Next.js, etc).
ISC License
TypeScript Interface for Machine Payments Protocol
Alephant is an open-source AI Agent Gateway for routing, tracking, and controlling LLM usage across AI agents, members, and workflows, and for publishing agent capabilities as paid endpoints with x402 and MPP payment rails.
Specifications for the Machine Payments Protocol - powered by the "Payment" HTTP authentication scheme
Rust SDK for the Machine Payments Protocol
Website for the Machine Payments Protocol
Building blocks for Agentic payments (x402, MPP, AP2) for TypeScript, Rust, Go, Python, Ruby, PHP, Lua, Kotlin and Swift.