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/pay-kit

v0.5.0

Published

Building blocks for Agentic payments (x402, MPP, AP2)

Readme

Charge stablecoins (USDC, USDT, PYUSD, …) for any HTTP endpoint, in TypeScript. One surface (@solana/pay-kit) over the Machine Payments Protocol, built on the same protocol adapter seam as the Ruby, Python, and PHP SDKs — with x402 to follow behind it. Everything runs on web-standard Request/Response, so Express, Hono, and plain node:http all sit on the same dispatcher.

You do not need to know anything about Solana to use this library: pick a currency, give it your wallet address, and gate a route.

TypeScript Node


Quick start

Three progressively-realistic snippets. Express is the framework here; the same surface works anywhere you can hand the dispatcher a web-standard Request.

1. Smallest possible app

Gate one route with an inline price. Save as server.ts and boot with npx tsx server.ts. Zero-config beyond the RPC: the package uses a published demo keypair as the recipient and the hosted Surfpool sandbox at https://402.surfnet.dev:8899.

// server.ts
import express from 'express'
import { createPayKit, usd } from '@solana/pay-kit'

const pay = await createPayKit({ rpcUrl: 'https://402.surfnet.dev:8899' })

const app = express()
app.get('/report', pay.express(usd('0.10')), (_req, res) => {
  res.send('premium content')
})
app.listen(4567)

The middleware halts the request with a 402 challenge if no valid payment was sent; when one was, it verifies and settles it, sets the settlement headers on the response, and hands control to your handler.

Hit /report with pay curl and the customer walks through Touch ID and a USDC payment.

2. Multiple gates via a catalogue

When more than one route is paid, lift the prices into the pricing catalogue. Routes reference gates by name, and the names are typed: pay.express('report') autocompletes and a typo is a compile error.

import { createPayKit, usd } from '@solana/pay-kit'

const pay = await createPayKit({
  rpcUrl: 'https://402.surfnet.dev:8899',
  pricing: {
    report: { amount: usd('0.10'), description: 'Premium report' },
    apiCall: { amount: usd('0.001') },
  },
})

app.get('/report', pay.express('report'), (req, res) => {
  res.json({ content: 'premium', tx: pay.payment(req)?.transaction })
})
app.get('/api/data', pay.express('apiCall'), (_req, res) => {
  res.json({ data: [] })
})

Gates are validated at construction — wrong currency, fee math that doesn't add up, a fee routed back to the recipient — so configuration errors surface at boot, before any traffic.

3. Production-shape config

Snippet 2's demo recipient and public sandbox are fine for poking around. Production wants explicit keys, a dedicated RPC, a stable challenge secret, and a persistent replay store. The route handlers are unchanged — only the config grows.

import { createPayKit, Signer, Store, usd } from '@solana/pay-kit'

const PLATFORM = 'CXhrFZJLKqjzmP3sjYLcF4dTeXWKCy9e2SXXZ2Yo6MPY'

const pay = await createPayKit({
  network: 'solana_mainnet',
  stablecoins: ['USDC', 'PYUSD'],
  operator: { signer: await Signer.file('config/operator.json') },
  rpcUrl: 'https://mainnet.helius-rpc.com/?api-key=YOUR_HELIUS_KEY',
  mpp: { challengeBindingSecret: process.env.PAY_KIT_MPP_SECRET! },
  replayStore: Store.redis(redisClient), // any ioredis / node-redis / Valkey client
  pricing: {
    report: { amount: usd('0.10'), description: 'Premium report' },

    // Platform-fee pattern:
    // Customer pays $10.00,
    // Operator nets $9.70, PLATFORM nets $0.30.
    marketplaceSale: { amount: usd('10.00'), feeWithin: { [PLATFORM]: usd('0.30') } },
  },
})

Two safety rails fire at boot:

  • solana_mainnet plus the published demo signer throws (DemoSignerOnMainnetError) — no real funds get routed to a publicly known address by accident.
  • Outside localnet, a missing mpp.challengeBindingSecret throws. Provide it in createPayKit or via PAY_KIT_MPP_SECRET so the HMAC stays stable across restarts. On localnet an ephemeral secret is generated with a warning.

Run the example

The playground at playground/ wires the server-side SDK into an Express app exposing every primitive the kit ships (/api/v1/stocks/quote/:symbol, /api/v1/weather/:city, x402, subscriptions, sessions, …).

Boot the server:

cd playground
pnpm install
pnpm dev          # Vite on :5173, Express on :3000

Consume with pay curl:

# Install the pay CLI:
brew install pay
# or npm install -g @solana/pay

# Fail with 402 - payment required
curl -i http://127.0.0.1:3000/api/v1/weather/paris

# Succeed with 200 - payment provided
pay curl -i http://127.0.0.1:3000/api/v1/weather/paris

Set RECIPIENT, NETWORK, RPC_URL, MPP_SECRET_KEY, or FEE_PAYER_KEY to point at a different localnet fixture or wallet.


MPP

The Machine Payments Protocol is the broader HTTP Payment Authentication scheme — a 402 Payment Required handshake whose challenge carries a rich intent shape supporting multi-recipient splits, server-side fee accounting, and a separate fee-payer signer.

Use MPP when:

  • Your gate has a platform or gateway fee (Stripe-Connect "application fee" pattern).
  • You want the server to subsidize the customer's network fee.
  • You want one challenge per gate instead of per-mint-quoted offers.

Supported in TypeScript:

| Intent | Client | Server | |----------------|:------:|:------:| | charge/pull | ✅ | ✅ | | charge/push | ✅ | ✅ | | session | ✅ | ✅ | | subscription | ✅ | ✅ |

For charge/pull the server owns the full lifecycle: it issues signed challenges with a fresh recentBlockhash, validates the Authorization: Payment credential, decodes the client-signed transaction and checks recipient, amount, mint, splits, ATA, memos, and compute budget, optionally co-signs as fee payer, broadcasts, polls to confirmed/finalized, and emits payment-receipt with the settlement signature. For charge/push the server fetches the transaction by signature, runs the same structural verifier, and consumes the signature through the replay store.

x402

x402 revives HTTP 402 Payment Required as a client-server payment handshake. The TypeScript surface ships all three SVM schemes via @x402/svm (client + server + facilitator each):

| Scheme | Client | Server | |--------------------|:------:|:------:| | exact | ✅ | ✅ | | upto | ✅ | ✅ | | batch-settlement | ✅ | ✅ |

pay-kit wires exact and upto straight into gates — configure({ accept: ['x402'] }) mounts the exact adapter for fixed-price gates and the in-process upto facilitator for usage gates. For delegated upto channels, set x402.facilitatorFee to the facilitator share in basis points; the default is 0. batch-settlement ships in @x402/svm but is not yet exposed as a pay-kit gate adapter. All three are exercised client- and server-side by the cross-language conformance suite in the harness.

Client

Unlike the Ruby, Python, and PHP SDKs (server-only), TypeScript also ships the paying side, via @solana/pay-kit/client:

import { createPayKitClient } from '@solana/pay-kit/client'

const client = await createPayKitClient({ signer, rpcUrl })
const res = await client.fetch('https://api.example/paid')

client.fetch is a fetch-shaped helper: on a 402 it pays with the matching protocol (MPP or x402) and retries the request with the Authorization: Payment credential. Pass a third argument ('x402' / 'mpp') to force a rail when an endpoint offers both.


Vocabulary

| Term | Meaning | |--------------|---------| | gate | A protected unit. Has an amount, optional fees, accepted protocols. | | amount | The base amount a gate charges, before any feeOnTop. | | total | What the customer pays: amount + sum(feeOnTop). Derived. | | price | Value object returned by usd(…): number + currency + settlement. | | feeWithin | Fee taken out of the amount. payTo nets less. | | feeOnTop | Fee added to the amount. Customer pays more; payTo nets full. | | payment | Proof submitted by the client to pass a gate. | | protocol | 'x402' or 'mpp' (top-level dispatch). | | scheme | x402 sub-form: exact. MPP sub-form: charge. | | accept | Ordered preference list (protocols and stablecoins both). | | currency | Fiat unit a price is quoted in (USD, EUR, GBP). | | settlement | The stablecoin that actually transfers (USDC, USDT, …). |

Three primitives

The same trio as the Ruby, Python, and PHP SDKs. TypeScript returns a result object instead of halting, matching the surrounding ecosystem:

| Method | Purpose | |--------|---------| | pay.requirePayment(request, gate) | Verify-or-deny; returns { status: 402, response } or { status: 200, payment, withSettlement } | | pay.paid(request, gateName?) | Predicate, never settles | | pay.payment(request) | The verified Payment, undefined until paid |

gate accepts a catalogue name, a Gate, a bare Price (inline gate), or a per-request resolver function.

Inline pricing

For one-off endpoints that don't warrant a catalogue entry, pass a price directly:

const result = await pay.requirePayment(request, usd('0.25'))

Gate catalogue

Each gate is a frozen value object with an amount, an ordered list of accepted protocols, and zero or more named fees.

const SELLER = 'Ay…'
const PLATFORM = 'CX…'

const pricing = createPricing(config, {
  // Simple. Customer pays $0.10, payTo nets $0.10.
  report: { amount: usd('0.10'), description: 'Premium report' },

  // Stripe-Connect "application fee". Customer pays $10.00,
  // SELLER nets $9.70, PLATFORM nets $0.30.
  marketplaceSale: {
    amount: usd('10.00'),
    payTo: SELLER,
    feeWithin: { [PLATFORM]: usd('0.30') },
  },

  // Surcharge. Customer pays $10.50, SELLER nets $10.00, PLATFORM $0.50.
  ticket: {
    amount: usd('10.00'),
    payTo: SELLER,
    feeOnTop: { [PLATFORM]: usd('0.50') },
  },

  // Dynamic per-request pricing.
  tiered: request =>
    usd(new URL(request.url).searchParams.get('tier') === 'premium' ? '5.00' : '0.10'),
})

Boot-time validations (all throw ConfigurationError or a subtype):

  • payTo resolves from the gate or operator.recipient.
  • A fee recipient must differ from payTo. Fold the fee into the amount instead.
  • All fee prices share one currency with the amount.
  • sum(feeWithin) must be less than the amount.
  • accept: ['x402'] on a fee-bearing gate throws (ProtocolIncompatibleError).

Signers

Key handling is built on Solana Keychain. Local constructors ride on @solana/keychain-memory; remote backends plug in through Signer.from:

import { Signer } from '@solana/pay-kit'

// Local key material:
const fromFile = await Signer.file('config/operator.json')   // Solana CLI JSON
const fromEnv = await Signer.env('OPERATOR_KEY')             // JSON / hex / base58, auto-detected
const ephemeral = await Signer.generate()                    // tests

// Remote signing (AWS KMS, GCP KMS, Vault, Privy, Turnkey, …):
import { createKeychainSigner } from '@solana/keychain'
const remote = Signer.from(
  await createKeychainSigner({ backend: 'vault', /* … */ }),
  { feePayer: false },
)

The demo signer (Signer.demo()) is byte-for-byte identical across the Ruby, Python, PHP, and Lua SDKs so processes running different SDKs can exchange traffic during local development. It is public by design and refused on mainnet at boot.

Middleware

The framework shims sit on the dispatcher, the same way Ruby's Rack middleware, Python's FastAPI/Flask/Django shims, and PHP's PSR-15 middleware sit on theirs.

Express / Connect (pay.express) — typed against node:http, so it adds no framework dependency and also fits Polka and plain node:http servers:

app.get('/report', pay.express('report'), (req, res) => {
  res.json({ ok: true, tx: pay.payment(req)?.transaction })
})

Hono (pay.hono) — works with any framework whose context exposes the web request as c.req.raw:

app.use('/report', pay.hono('report'))
app.get('/report', c => c.json({ ok: true, tx: pay.payment(c.req.raw)?.transaction }))

Fetch handlers (Cloudflare Workers, Bun, Deno, Next.js) — wrap the handler instead of chaining middleware:

import { usd } from '@solana/pay-kit'

export default {
  fetch: pay.fetch(usd('0.10'), (request, payment) =>
    Response.json({ ok: true, tx: payment.transaction }),
  ),
}

Web-standard core

The dispatcher is framework-free: it takes a web-standard Request and produces a web-standard Response. Protocol work happens behind one adapter contract (detect / acceptsEntry / challengeHeaders / verifyAndSettle); the MPP adapter wraps @solana/mpp's charge method and caches one handler per (recipient, splits) shape. Verified payments are tracked per Request, so paid() and payment() answer for the same request object the route handler holds — which is why the Hono shim needs no context plumbing at all.


Install

pnpm add @solana/pay-kit

Test

cd typescript
pnpm install
pnpm typecheck
pnpm test
pnpm test:integration

Harness

The cross-language harness lives in harness/. The TypeScript SDK ships both the reference client (harness/ts-client) and the in-process reference server used by every other SDK's adapter.

cd harness
pnpm install
pnpm test

Spec

This SDK implements the Solana Charge Intent for the HTTP Payment Authentication Scheme. The cross-language surface is specified in docs/paykit-interface.md.


Repo layout

typescript/
├── packages/pay-kit/    # @solana/pay-kit: gates, pricing, signers, dispatcher
│   └── src/
│       ├── config.ts, gate.ts, price.ts, pricing.ts, signer.ts, errors.ts, …
│       ├── adapter.ts            # The protocol adapter contract
│       ├── adapters/mpp.ts       # MPP charge adapter over @solana/mpp
│       ├── adapters/x402-upto.ts # x402 upto (usage) facilitator + Charge meter
│       └── paykit.ts             # createPayKit + dispatcher (requirePayment /
│                                 #   paid / payment) + express / hono / fetch
├── packages/mpp/        # @solana/mpp: protocol layer (client + server + sessions)
├── vitest.config*.ts    # test configurations
└── package.json         # workspace scripts

Coding convention

This SDK follows eslint + prettier and the per-language style notes at skills/pay-sdk-implementation/references/coding-conventions.md.

The repo-level pay-sdk-implementation skill remains the protocol source of truth: Rust / spec wire format first, TypeScript idioms second.

License

MIT