From 14ddd1ed9c7963218a28ec911b1ae2c5c1a00efb Mon Sep 17 00:00:00 2001 From: nitishxyz Date: Thu, 19 Mar 2026 05:16:17 +0530 Subject: [PATCH] feat: add mppx-solana docs to payment methods and SDKs - Add Solana payment method overview page with fee sponsorship docs - Add Solana charge page with server/client examples and framework guides --- src/components/cards.tsx | 22 ++ src/pages.gen.ts | 3 + src/pages/payment-methods/index.mdx | 9 +- src/pages/payment-methods/solana/charge.mdx | 351 ++++++++++++++++++++ src/pages/payment-methods/solana/index.mdx | 219 ++++++++++++ src/pages/sdk/index.mdx | 12 +- src/pages/sdk/solana/index.mdx | 38 +++ vocs.config.ts | 14 + 8 files changed, 665 insertions(+), 3 deletions(-) create mode 100644 src/pages/payment-methods/solana/charge.mdx create mode 100644 src/pages/payment-methods/solana/index.mdx create mode 100644 src/pages/sdk/solana/index.mdx diff --git a/src/components/cards.tsx b/src/components/cards.tsx index 8397f9d6..b853f69c 100644 --- a/src/components/cards.tsx +++ b/src/components/cards.tsx @@ -121,6 +121,17 @@ export function RustSdkCard() { ); } +export function SolanaSdkCard() { + return ( + + ); +} + export function MppxCreateReferenceCard({ to }: { to: string }) { return ( + ); +} + export function LightningChargeCard() { return ( + +```http +HTTP/1.1 402 Payment Required +WWW-Authenticate: Payment method="solana", intent="charge", ... ``` @@ -43,5 +49,6 @@ WWW-Authenticate: Payment method="lightning", intent="charge", ... + diff --git a/src/pages/payment-methods/solana/charge.mdx b/src/pages/payment-methods/solana/charge.mdx new file mode 100644 index 00000000..1598001f --- /dev/null +++ b/src/pages/payment-methods/solana/charge.mdx @@ -0,0 +1,351 @@ +# Solana charge [One-time SOL and SPL token transfers] + +The Solana implementation of the [charge](/intents/charge) intent. + +The client signs and submits a Solana transaction (native SOL or SPL token transfer), the server verifies it on-chain, and returns the resource with a Receipt. Settlement completes in ~400ms. + +## Server + +Use `mppx.charge` with the Solana server method to gate any endpoint behind a one-time payment. The method handles Challenge generation, on-chain transaction verification, and Receipt creation. + +### Native SOL + +```ts [server.ts] +import { Mppx } from "mppx/server"; +import { server as solanaServer, NATIVE_SOL_CURRENCY } from "mppx-solana"; + +const mppx = Mppx.create({ + methods: [ + solanaServer({ + cluster: "mainnet-beta", + currency: NATIVE_SOL_CURRENCY, + decimals: 9, + recipient: "YourWalletPublicKeyBase58", + }), + ], + secretKey: process.env.MPP_SECRET_KEY!, +}); + +export async function handler(request: Request) { + const result = await mppx.charge({ + amount: "1000000", + description: "API access", + })(request); + + if (result.status === 402) return result.challenge; + + return result.withReceipt(Response.json({ data: "your paid content here" })); +} +``` + +### SPL tokens (USDC) + +```ts [server-usdc.ts] +import { Mppx } from "mppx/server"; +import { server as solanaServer } from "mppx-solana"; + +const mppx = Mppx.create({ + methods: [ + solanaServer({ + cluster: "mainnet-beta", + currency: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + decimals: 6, + recipient: "YourWalletPublicKeyBase58", + }), + ], + secretKey: process.env.MPP_SECRET_KEY!, +}); +``` + +The `currency` parameter accepts the SPL mint address. Common tokens: + +| Token | Mint address | Decimals | +|-------|-------------|----------| +| SOL (native) | `solana:native` | 9 | +| USDC | `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` | 6 | +| USDT | `Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB` | 6 | + +### With memo verification + +Attach a memo to bind payments to specific invoices or orders. The server rejects transactions that don't contain the expected memo. + +```ts [server-memo.ts] +import { Mppx } from "mppx/server"; +import { server as solanaServer, NATIVE_SOL_CURRENCY } from "mppx-solana"; + +const mppx = Mppx.create({ + methods: [ + solanaServer({ + cluster: "mainnet-beta", + currency: NATIVE_SOL_CURRENCY, + decimals: 9, + recipient: "YourWalletPublicKeyBase58", + }), + ], + secretKey: process.env.MPP_SECRET_KEY!, +}); + +export async function handler(request: Request) { + const result = await mppx.charge({ + amount: "1000000", + memo: "invoice-abc-123", + })(request); + + if (result.status === 402) return result.challenge; + + return result.withReceipt(Response.json({ data: "paid" })); +} +``` + +### Framework examples + +::::steps + +#### Hono + +```ts [hono.ts] +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({ + cluster: "mainnet-beta", + currency: NATIVE_SOL_CURRENCY, + decimals: 9, + recipient: "YourWalletPublicKeyBase58", + }), + ], + 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 + +```ts [express.ts] +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({ + cluster: "mainnet-beta", + currency: NATIVE_SOL_CURRENCY, + decimals: 9, + recipient: "YourWalletPublicKeyBase58", + }), + ], + 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 + +```ts [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({ + cluster: "mainnet-beta", + currency: NATIVE_SOL_CURRENCY, + decimals: 9, + recipient: "YourWalletPublicKeyBase58", + }), + ], + secretKey: process.env.MPP_SECRET_KEY!, +}); + +export const GET = mppx.charge({ + amount: "1000000", + description: "API access", +})(async () => Response.json({ data: "your paid content here" })); +``` + +:::: + +### With fee sponsorship + +Enable gasless transactions so clients only need the payment token—no SOL required. The server sponsors the Solana transaction fee and the client reimburses the server in the same token. + +```ts [sponsored-server.ts] +import { Connection, 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 connection = new Connection("https://api.mainnet-beta.solana.com"); + +const mppx = Mppx.create({ + methods: [ + solanaServer({ + cluster: "mainnet-beta", + connection, + currency: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + decimals: 6, + recipient: "MerchantWalletPublicKeyBase58", + sponsor: { + feePayer: sponsorKeypair, + feeTokenAmount: "10000", + sponsorPath: "/sponsor", + }, + }), + ], + realm: "api.example.com", + secretKey: process.env.MPP_SECRET_KEY!, +}); + +const handleSponsor = createSponsorHandler({ + connection, + feePayer: sponsorKeypair, + feeTokenAmount: "10000", +}); + +Bun.serve({ + routes: { + "/api/resource": { + GET: async (request) => { + const result = await mppx.charge({ + amount: "100000", + })(request); + + if (result.status === 402) return result.challenge; + return result.withReceipt(Response.json({ data: "paid content" })); + }, + }, + "/sponsor": { + POST: handleSponsor, + }, + }, +}); +``` + +The `sponsor` config adds `sponsored: true` and `sponsorPath` to the Challenge. The client detects this automatically and uses the sponsor endpoint—no client-side changes needed. + +See [fee sponsorship](/payment-methods/solana#fee-sponsorship) for the full flow diagram and cost breakdown. + +## Client + +Use the Solana client method with `Mppx.create` to automatically handle `402` responses. The client parses the Challenge, builds and signs a Solana transaction, submits it, and retries with the transaction signature as proof. + +```ts [client.ts] +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, +}); + +const response = await mppx.fetch("https://api.example.com/resource"); +const data = await response.json(); +``` + +### With polyfill + +Patch `globalThis.fetch` so every `fetch` call automatically handles Solana payments: + +```ts [client-polyfill.ts] +import { Connection, Keypair } from "@solana/web3.js"; +import { Mppx } from "mppx/client"; +import { client as solanaClient } from "mppx-solana"; + +Mppx.create({ + methods: [ + solanaClient({ + connection: new Connection("https://api.mainnet-beta.solana.com"), + signer: Keypair.fromSecretKey(/* your key */), + }), + ], +}); + +const response = await fetch("https://api.example.com/resource"); +``` + +### Custom signer + +Any object with `publicKey` and `signTransaction` works—use this to integrate wallet adapters: + +```ts [client-wallet.ts] +import { client as solanaClient } from "mppx-solana"; + +solanaClient({ + connection, + signer: { + publicKey: wallet.publicKey, + signTransaction: (tx) => wallet.signTransaction(tx), + }, +}); +``` + +### Dynamic connection + +Use `getConnection` when you need per-request RPC routing: + +```ts [client-dynamic.ts] +import { Connection } from "@solana/web3.js"; +import { client as solanaClient } from "mppx-solana"; + +solanaClient({ + getConnection: (cluster) => { + if (cluster === "devnet") + return new Connection("https://api.devnet.solana.com"); + return new Connection("https://my-rpc.example.com"); + }, + signer: myKeypair, +}); +``` + +## How 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 │ + │ ◄─────────────────────────────│ +``` + +1. Client hits a paid endpoint—server returns `402` with a Solana payment Challenge +2. Client builds, signs, and submits a Solana transaction +3. Client retries the request with the transaction signature as a Credential +4. Server verifies the transaction on-chain and returns the resource with a Receipt diff --git a/src/pages/payment-methods/solana/index.mdx b/src/pages/payment-methods/solana/index.mdx new file mode 100644 index 00000000..f0daa224 --- /dev/null +++ b/src/pages/payment-methods/solana/index.mdx @@ -0,0 +1,219 @@ +import * as SdkBadge from '../../../components/SdkBadge' + +# Solana [SOL and SPL token payments on Solana] + +The Solana payment method enables payments using native SOL and SPL tokens on the Solana blockchain. Solana currently supports the **charge** intent for one-time payments—gate any HTTP endpoint behind a Solana transaction that settles in ~400ms. + +## Payments on Solana + +Solana's architecture makes it a natural fit for machine-to-machine payments: + +- **Fast finality**—Transactions confirm in ~400ms, keeping payment overhead low +- **Low fees**—Transaction costs under $0.01, viable for micropayments and per-request billing +- **SPL token support**—Accept USDC, USDT, or any SPL token alongside native SOL +- **Auto-created token accounts**—The client automatically creates the recipient's associated token account if it doesn't exist +- **Memo verification**—Bind payments to specific invoices or orders with on-chain memos +- **Fee sponsorship**—Servers can sponsor Solana transaction fees so clients only need the payment token, no SOL required + +## Choosing a token + +| | **Native SOL** | **SPL tokens** | +|---|---|---| +| **Currency identifier** | `solana:native` | SPL mint address | +| **Decimals** | 9 | Varies (6 for USDC) | +| **Best for** | Simple payments, SOL-native users | Stablecoin billing, predictable pricing | +| **Token account** | Not needed | Auto-created if missing | + +## Intents + + + +## Fee sponsorship + +Solana supports server-sponsored transaction fees for charge payments. When enabled, the client signs only the payment authorization—the server pays the Solana network fee in SOL and the client reimburses the server in the payment token (for example, USDC). + +This means clients don't need to hold SOL at all. They pay the API cost plus a small fee surcharge in the same token they are already using. + +### How it works + +``` +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 │ + │ ◄─────────────────────────────│ +``` + +1. Server returns a `402` Challenge with `sponsored: true` and a `sponsorPath` +2. Client POSTs its public key to the sponsor endpoint +3. Server builds the transaction with itself as `feePayer`, includes the payment transfer and a small fee reimbursement transfer from user to server +4. Server partially signs as fee payer and returns the serialized transaction +5. Client co-signs as the payment authority and submits +6. Server verifies the payment on-chain as usual + +### Server setup + +Pass a `sponsor` config to the Solana server method and expose a sponsor endpoint using `createSponsorHandler`: + +```ts [server.ts] +import { Connection, 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 connection = new Connection("https://api.mainnet-beta.solana.com"); + +const mppx = Mppx.create({ + methods: [ + solanaServer({ + cluster: "mainnet-beta", + connection, + currency: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + decimals: 6, + recipient: "MerchantWalletPublicKeyBase58", + sponsor: { + feePayer: sponsorKeypair, + feeTokenAmount: "10000", + sponsorPath: "/sponsor", + }, + }), + ], + realm: "api.example.com", + secretKey: process.env.MPP_SECRET_KEY!, +}); + +const handleSponsor = createSponsorHandler({ + connection, + feePayer: sponsorKeypair, + feeTokenAmount: "10000", +}); + +Bun.serve({ + routes: { + "/api/resource": { + GET: async (request) => { + const result = await mppx.charge({ + amount: "100000", + })(request); + + if (result.status === 402) return result.challenge; + return result.withReceipt(Response.json({ data: "paid content" })); + }, + }, + "/sponsor": { + POST: handleSponsor, + }, + }, +}); +``` + +### Client + +No changes needed on the client side. The client automatically detects `sponsored: true` in the Challenge and uses the sponsor endpoint: + +```ts [client.ts] +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, +}); + +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 token reimbursements and needs to periodically swap them for SOL to stay funded. An initial SOL seed of ~0.1 SOL supports approximately 20,000+ transactions. + +### Sponsor configuration + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `sponsor.feePayer` | `Keypair` | Yes | Server keypair that pays Solana transaction fees | +| `sponsor.feeTokenAmount` | `string` | Yes | Amount in token smallest unit to reimburse (for example, `"10000"` = 0.01 USDC) | +| `sponsor.sponsorPath` | `string` | Yes | HTTP path for the sponsor endpoint (for example, `"/sponsor"`) | + +See [Solana charge](/payment-methods/solana/charge#with-fee-sponsorship) for the full charge integration with sponsorship. + +## Package + +The Solana payment method is provided by the [`mppx-solana`](https://github.com/nitishxyz/mppx-solana) package, a community extension for `mppx`. + +
+ + +
+ +### Install + +```bash [install.sh] +$ pnpm add mppx-solana mppx @solana/web3.js @solana/spl-token viem +``` + +## Server configuration + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `cluster` | `string` | No | `"mainnet-beta"`, `"devnet"`, `"testnet"`, `"localnet"` | +| `commitment` | `string` | No | `"confirmed"` (default) or `"finalized"` | +| `connection` | `Connection` | No | Custom RPC connection | +| `currency` | `string` | Yes | `"solana:native"` for SOL, or SPL mint address | +| `decimals` | `number` | Yes | Token decimals (9 for SOL, 6 for USDC) | +| `getConnection` | `function` | No | Dynamic connection factory | +| `recipient` | `string` | Yes | Wallet address receiving payments | +| `sponsor` | `SolanaSponsorConfig` | No | Enable sponsored/gasless transactions | + +## Client configuration + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `connection` | `Connection` | No | RPC connection | +| `getConnection` | `function` | No | Dynamic connection factory | +| `signer` | `Keypair \| SolanaSigner` | Yes | Signs transactions | diff --git a/src/pages/sdk/index.mdx b/src/pages/sdk/index.mdx index 8603d0fc..1c25d0f9 100644 --- a/src/pages/sdk/index.mdx +++ b/src/pages/sdk/index.mdx @@ -1,10 +1,18 @@ import { Cards } from 'vocs' -import { TypeScriptSdkCard, PythonSdkCard, RustSdkCard, WalletCliCard } from '../../components/cards' +import { PythonSdkCard, RustSdkCard, SolanaSdkCard, TypeScriptSdkCard } from '../../components/cards' # SDKs [Official implementations in multiple languages] +## Official SDKs + - + + + +## Ecosystem packages + + + diff --git a/src/pages/sdk/solana/index.mdx b/src/pages/sdk/solana/index.mdx new file mode 100644 index 00000000..03b8c67a --- /dev/null +++ b/src/pages/sdk/solana/index.mdx @@ -0,0 +1,38 @@ +import * as SdkBadge from '../../../components/SdkBadge' + +# Solana SDK [Solana payments for MPP] + +## Overview + +The `mppx-solana` package adds Solana payment methods to `mppx` so you can accept SOL and SPL token payments in the standard MPP Challenge, Credential, and Receipt flow. + +
+ + +
+ +## Install + +```bash [install.sh] +$ pnpm add mppx-solana mppx @solana/spl-token @solana/web3.js +``` + +## Package details + +- **Package:** `mppx-solana` +- **Repository:** `nitishxyz/mppx-solana` +- **Works with:** `mppx` +- **Supports:** SOL and SPL token payments + +## What you can build + +- Protect HTTP endpoints with Solana-based payment Challenges +- Accept native SOL payments +- Accept SPL token payments such as USDC +- Verify transactions on-chain before you return a Receipt + +## Next steps + +- [Solana payment method](/payment-methods/solana) — how Solana fits into MPP +- [Solana charge](/payment-methods/solana/charge) — server and client integration guide +- [GitHub repository](https://github.com/nitishxyz/mppx-solana) — source code and examples diff --git a/vocs.config.ts b/vocs.config.ts index fb19a2bd..2e9cedf5 100644 --- a/vocs.config.ts +++ b/vocs.config.ts @@ -172,6 +172,14 @@ export default defineConfig({ { text: "Session", link: "/payment-methods/lightning/session" }, ], }, + { + text: "Solana", + collapsed: true, + items: [ + { text: "Overview", link: "/payment-methods/solana" }, + { text: "Charge", link: "/payment-methods/solana/charge" }, + ], + }, { text: "Custom", link: "/payment-methods/custom" }, ], }, @@ -532,6 +540,11 @@ export default defineConfig({ { text: "Server", link: "/sdk/rust/server" }, ], }, + { + text: "Solana", + collapsed: true, + items: [{ text: "Overview", link: "/sdk/solana" }], + }, ], }, { @@ -563,6 +576,7 @@ export default defineConfig({ { text: "mppx (TypeScript)", link: "https://github.com/wevm/mppx" }, { text: "mpp-rs (Rust)", link: "https://github.com/tempoxyz/mpp-rs" }, { text: "pympp (Python)", link: "https://github.com/tempoxyz/pympp" }, + { text: "mppx-solana (Solana)", link: "https://github.com/nitishxyz/mppx-solana" }, ], }, ],