npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

solana-mpp

v0.1.1

Published

Solana MPP SDK — SPL token payments over HTTP 402

Downloads

216

Readme


Why Solana for Machine Payments?

| | 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 |


How Payment Works

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.


Payment Intents

Charge (One-Time Payment)

A single payment per request. The client pays the exact amount and gets access to the resource. Simple, stateless, and ideal for:

  • Pay-per-call APIs
  • One-time data access
  • File downloads
  • Single inference calls

Session (Prepaid Account)

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:

  • Metered API usage (pay per page, per query, per minute)
  • Streaming data feeds
  • Multi-step agent workflows
  • Any use case where per-request payment overhead matters

Session lifecycle:

  1. Open — Client deposits tokens, server creates session with bearer token
  2. Use — Client sends bearer token on each request, server deducts from balance
  3. Top-up — Client can add more tokens if balance runs low
  4. Close — Client ends session, server refunds remaining balance on-chain

Installation

npm install solana-mpp mppx @solana/web3.js @solana/spl-token

@solana/web3.js and @solana/spl-token are peer dependencies — you likely already have them if you're building on Solana.


Quick Start

Server: Accept Payments

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 })
}

Client: Make Payments

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.


Server API

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

Server Exports

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 |


Client API

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

WalletLike

Any 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
  },
}

Client Exports

import { Mppx } from 'solana-mpp/client'

| Export | Description | |---|---| | Mppx | Client-side payment handler with fetch() that auto-handles 402 flows |


Full Examples

Example 1: Pay-Per-Joke API (Charge)

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)

Example 2: Metered Data API (Session)

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')

Running the Examples

Both examples run against a local Solana validator.

Prerequisites

# Install Solana CLI tools
sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"

# Start a local validator
solana-test-validator

Charge Example

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.

Session Example

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.


Networks

| 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,
})

Architecture

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

How Verification Works

Payment verification is fully on-chain with no external dependencies:

  1. 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).

  2. Transaction Discovery — The server finds the payment transaction by calling getSignaturesForAddress on the reference key, or uses the client-provided signature directly.

  3. Transfer Validation — The server parses the transaction and verifies:

    • The reference key is present in the transaction's account list
    • An SPL token transfer occurred to the recipient's Associated Token Account
    • The transferred amount matches or exceeds the requested amount
    • The correct token mint was used
    • The transaction succeeded (no errors)
  4. Replay Protection — Transaction signatures are stored and checked to prevent double-spending the same payment.


Standards

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).