mppx-solana
v0.2.0
Published
Solana payment method for the Machine Payments Protocol (mppx)
Downloads
218
Maintainers
Readme
mppx-solana
Solana payment method for the Machine Payments Protocol. Accept SOL and SPL token payments on any HTTP endpoint.
Built on top of mppx — the official MPP SDK.
Install
bun add mppx-solana mppx @solana/web3.js @solana/spl-token viemnpm install mppx-solana mppx @solana/web3.js @solana/spl-token viempnpm add mppx-solana mppx @solana/web3.js @solana/spl-token viemHow it works
Client Server
│ │
│ GET /api/resource │
│ ────────────────────────────► │
│ │
│ 402 + Challenge │
│ (amount, recipient, mint) │
│ ◄──────────────────────────── │
│ │
│ Signs & sends Solana tx │
│ Retries with tx signature │
│ ────────────────────────────► │
│ │
│ Verifies tx on-chain │
│ 200 + Resource + Receipt │
│ ◄──────────────────────────── │- Client hits a paid endpoint → server returns
402with a Solana payment challenge - Client builds, signs, and submits a Solana transaction
- Client retries the request with the transaction signature as proof
- Server verifies the transaction on-chain and returns the resource with a receipt
Server
Gate any endpoint behind a Solana payment. Works with Bun.serve, Hono, Express, and Next.js.
Bun
import { Mppx } from "mppx/server";
import { server as solanaServer, NATIVE_SOL_CURRENCY } from "mppx-solana";
const mppx = Mppx.create({
methods: [
solanaServer({
recipient: "YourWalletPublicKeyBase58",
currency: NATIVE_SOL_CURRENCY,
cluster: "mainnet-beta",
decimals: 9,
}),
],
secretKey: process.env.MPP_SECRET_KEY!,
});
Bun.serve({
async fetch(request) {
const result = await mppx.charge({
amount: "1000000", // 0.001 SOL in lamports
description: "API access",
})(request);
if (result.status === 402) return result.challenge;
return result.withReceipt(
Response.json({ data: "your paid content here" }),
);
},
});Hono
import { Hono } from "hono";
import { Mppx } from "mppx/hono";
import { server as solanaServer, NATIVE_SOL_CURRENCY } from "mppx-solana";
const app = new Hono();
const mppx = Mppx.create({
methods: [
solanaServer({
recipient: "YourWalletPublicKeyBase58",
currency: NATIVE_SOL_CURRENCY,
cluster: "mainnet-beta",
decimals: 9,
}),
],
secretKey: process.env.MPP_SECRET_KEY!,
});
app.get(
"/api/resource",
mppx.charge({ amount: "1000000", description: "API access" }),
async (c) => c.json({ data: "your paid content here" }),
);Express
import express from "express";
import { Mppx } from "mppx/express";
import { server as solanaServer, NATIVE_SOL_CURRENCY } from "mppx-solana";
const app = express();
const mppx = Mppx.create({
methods: [
solanaServer({
recipient: "YourWalletPublicKeyBase58",
currency: NATIVE_SOL_CURRENCY,
cluster: "mainnet-beta",
decimals: 9,
}),
],
secretKey: process.env.MPP_SECRET_KEY!,
});
app.get(
"/api/resource",
mppx.charge({ amount: "1000000", description: "API access" }),
async (req, res) => res.json({ data: "your paid content here" }),
);Next.js
// app/api/resource/route.ts
import { Mppx } from "mppx/nextjs";
import { server as solanaServer, NATIVE_SOL_CURRENCY } from "mppx-solana";
const mppx = Mppx.create({
methods: [
solanaServer({
recipient: "YourWalletPublicKeyBase58",
currency: NATIVE_SOL_CURRENCY,
cluster: "mainnet-beta",
decimals: 9,
}),
],
secretKey: process.env.MPP_SECRET_KEY!,
});
export const GET = mppx.charge({
amount: "1000000",
description: "API access",
})(async () => Response.json({ data: "your paid content here" }));Client
Pay for any MPP-gated endpoint automatically. The SDK handles the 402 → pay → retry flow.
import { Connection, Keypair } from "@solana/web3.js";
import { Mppx } from "mppx/client";
import { client as solanaClient } from "mppx-solana";
const mppx = Mppx.create({
methods: [
solanaClient({
connection: new Connection("https://api.mainnet-beta.solana.com"),
signer: Keypair.fromSecretKey(/* your key */),
}),
],
polyfill: false,
});
// Automatically pays when the server returns 402
const response = await mppx.fetch("https://api.example.com/resource");
const data = await response.json();Custom signer
Any object with publicKey and signTransaction works — use this to integrate wallet adapters.
solanaClient({
connection,
signer: {
publicKey: wallet.publicKey,
signTransaction: (tx) => wallet.signTransaction(tx),
},
});Dynamic connection
Use getConnection when you need per-request RPC routing:
solanaClient({
signer: myKeypair,
getConnection: (cluster) => {
if (cluster === "devnet") return new Connection("https://api.devnet.solana.com");
return new Connection("https://my-rpc.example.com");
},
});Payment options
Native SOL
solanaServer({
currency: NATIVE_SOL_CURRENCY, // "solana:native"
decimals: 9,
recipient: "YourWalletPublicKeyBase58",
cluster: "mainnet-beta",
})
// amount is in lamports: "1000000" = 0.001 SOLSPL tokens (USDC, USDT, etc.)
solanaServer({
currency: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC mint
decimals: 6,
recipient: "YourWalletPublicKeyBase58",
cluster: "mainnet-beta",
})
// amount is in smallest unit: "10000" = 0.01 USDCThe client automatically creates the recipient's associated token account if it doesn't exist.
Memo verification
Attach a memo to bind payments to specific invoices or orders:
mppx.charge({
amount: "1000000",
memo: "invoice-abc-123",
})The server will reject transactions that don't contain the expected memo.
Sponsored fees (gasless for users)
Enable gasless transactions so users only need USDC (or any SPL token) — no SOL required. The server sponsors the Solana transaction fee and the user reimburses the server in the payment token.
Client Server
│ │
│ GET /api/resource │
│ ────────────────────────────► │
│ │
│ 402 + Challenge │
│ (sponsored=true, sponsorPath)│
│ ◄──────────────────────────── │
│ │
│ POST /sponsor │
│ { publicKey, request } │
│ ────────────────────────────► │
│ │
│ Partially signed tx │
│ (feePayer=server) │
│ ◄──────────────────────────── │
│ │
│ Co-signs & sends tx │
│ Retries with tx signature │
│ ────────────────────────────► │
│ │
│ Verifies tx on-chain │
│ 200 + Resource + Receipt │
│ ◄──────────────────────────── │How it works
- Server returns a
402challenge withsponsored: trueand asponsorPath - Client POSTs its public key to the sponsor endpoint
- Server builds the transaction with itself as
feePayer, includes the payment transfer + a small fee reimbursement transfer from user → server - Server partially signs as fee payer and returns the serialized transaction
- Client co-signs as the payment authority and submits
- Server verifies the payment on-chain as usual
The user pays the API cost + a small fee surcharge (e.g., 0.01 USDC) in the same token. The server pays the Solana network fee in SOL and receives the token reimbursement.
Server setup
import { Keypair } from "@solana/web3.js";
import { Mppx } from "mppx/server";
import {
createSponsorHandler,
server as solanaServer,
} from "mppx-solana";
const sponsorKeypair = Keypair.fromSecretKey(/* server fee payer key */);
const mppx = Mppx.create({
methods: [
solanaServer({
recipient: "MerchantWalletPublicKeyBase58",
currency: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC
decimals: 6,
cluster: "mainnet-beta",
sponsor: {
feePayer: sponsorKeypair,
feeTokenAmount: "10000", // 0.01 USDC reimbursement
sponsorPath: "/sponsor",
},
}),
],
realm: "api.example.com",
secretKey: process.env.MPP_SECRET_KEY!,
});
const handleSponsor = createSponsorHandler({
feePayer: sponsorKeypair,
feeTokenAmount: "10000",
connection: new Connection("https://api.mainnet-beta.solana.com"),
});
Bun.serve({
routes: {
"/api/resource": {
GET: async (request) => {
const result = await mppx.charge({
amount: "100000", // 0.10 USDC
})(request);
if (result.status === 402) return result.challenge;
return result.withReceipt(Response.json({ data: "paid content" }));
},
},
"/sponsor": {
POST: handleSponsor,
},
},
});Client
No changes needed — the client automatically detects sponsored: true in the challenge and uses the sponsor endpoint.
import { Connection, Keypair } from "@solana/web3.js";
import { Mppx } from "mppx/client";
import { client as solanaClient } from "mppx-solana";
const mppx = Mppx.create({
methods: [
solanaClient({
connection: new Connection("https://api.mainnet-beta.solana.com"),
signer: Keypair.fromSecretKey(/* user key */),
}),
],
polyfill: false,
});
// User pays 0.11 USDC (0.10 payment + 0.01 fee) — zero SOL needed
const response = await mppx.fetch("https://api.example.com/api/resource");Cost breakdown
For a 0.10 USDC API call with sponsored fees:
| | Without sponsorship | With sponsorship | |---|---|---| | User pays (USDC) | 0.10 | 0.11 (0.10 + 0.01 fee) | | User pays (SOL) | ~0.002 SOL (tx fee + ATA rent) | 0 | | Server pays (SOL) | 0 | ~0.004 SOL (tx fee + ATA rent) | | Server receives (USDC) | 0 | 0.01 (fee reimbursement) |
The server accumulates USDC reimbursements and needs to periodically swap USDC → SOL to stay funded. Initial SOL seed required (~0.1 SOL ≈ 20,000+ transactions).
Sponsor configuration
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| sponsor.feePayer | Keypair | Yes | Server keypair that pays Solana tx fees |
| sponsor.feeTokenAmount | string | Yes | Amount in token smallest unit to reimburse (e.g., "10000" = 0.01 USDC) |
| sponsor.sponsorPath | string | Yes | HTTP path for the sponsor endpoint (e.g., "/sponsor") |
Server configuration
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| recipient | string | Yes | Wallet address receiving payments |
| currency | string | Yes | "solana:native" for SOL, or SPL mint address |
| decimals | number | Yes | Token decimals (9 for SOL, 6 for USDC) |
| cluster | string | No | "mainnet-beta", "devnet", "testnet", "localnet" |
| commitment | string | No | "confirmed" (default) or "finalized" |
| connection | Connection | No | Custom RPC connection |
| getConnection | function | No | Dynamic connection factory |
| memo | string | No | Required memo string on transactions |
| description | string | No | Human-readable description |
| externalId | string | No | External reference ID for receipts |
| sponsor | SolanaSponsorConfig | No | Enable sponsored/gasless transactions |
Client configuration
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| signer | Keypair \| SolanaSigner | Yes | Signs transactions |
| connection | Connection | No | RPC connection |
| getConnection | function | No | Dynamic connection factory |
Running the examples
Full end-to-end examples using a local Solana validator are included. This project uses Solforge as the local validator.
# Install solforge
bun add -g solforge
# Start the local validator (picks up sf.config.json automatically)
solforge
# In another terminal, run the e2e tests
bun run example:e2e # SOL payment
bun run example:usdc-e2e # USDC payment
bun run example:sponsored-e2e # Sponsored SOL payment
bun run example:sponsored-usdc-e2e # Sponsored USDC payment (gasless)Or run server and client separately:
# Terminal 1 — server
MPP_SECRET_KEY=dev-secret \
EXAMPLE_RECIPIENT=<wallet> \
EXAMPLE_CLUSTER=localnet \
bun run example:server
# Terminal 2 — client
EXAMPLE_PAYER_SECRET_KEY='[1,2,3,...]' \
EXAMPLE_CLUSTER=localnet \
bun run example:clientType checking
bun run checkLicense
MIT
