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

@quicknode/mpp

v0.2.0

Published

Quicknode payment methods for MPP (Machine Payments Protocol)

Readme

@quicknode/mpp

[!WARNING] Beta. Public API may change between minor versions until v1. Pin to an exact version in production.

SDK for extending the MPP protocol with EVM-settled payments, verified via Quicknode RPC. Gate any HTTP endpoint behind a stablecoin (or native-coin) payment — agents pay with one signature, the server verifies on-chain, and the request is forwarded. Built for the Machine Payments Protocol.

Implements and expands on the draft-evm-charge-00 spec with all three non-trivial credential types:

| Type | Binding | Gas | UX | | ----------------------- | -------------------------------- | ----------- | ------------------------------------- | | permit2 (RECOMMENDED) | Strong (EIP-712 witness) | Server pays | One signature, any ERC-20 | | authorization | Strong (on-chain nonce) | Server pays | One signature, USDC / EIP-3009 tokens | | hash | Weakest (post-hoc receipt match) | Client pays | Client broadcasts + waits |

[!CAUTION] The hash credential is post-hoc receipt matching only — it binds nothing to the specific challenge. Any historical Transfer to the recipient that matches the requested token + amount can be claimed as proof of payment, once each. To narrow the replay window, set maxReceiptAgeSeconds on the server (see Configuration). Even then, concurrent third-party payments to the same recipient for the same amount within the window can still leak through. For payments where stronger binding matters, prefer permit2 or authorization.

Contents

Install

npm install @quicknode/mpp mppx viem

Compatibility

  • Node.js ≥ 22
  • Cloudflare Workers, Bun, and other WebCrypto-capable runtimes
  • Browsers — the client (@quicknode/mpp/client) runs in-browser for agent UIs. The server modules import Node-only code and are not browser-bundleable
  • TypeScript — types ship in the package; no separate @types/* install needed

Server — accept payments

import { Mppx, evm } from "@quicknode/mpp/server";

const mppx = Mppx.create({
  methods: [
    evm.charge({
      recipient: "0xMerchantWallet",
      chain: "base",
      submitter: { privateKey: process.env.SUBMITTER_PK! },
    }),
  ],
  secretKey: process.env.MPP_SECRET_KEY!,
});

// mppx.evm.charge({ amount: '0.01', decimals: 6 })(request) → 402 challenge or verified receipt

No rpcUrl? The SDK uses Quicknode's shared public endpoint for the chosen chain. Good for local dev and low-volume workloads. When you start seeing QuicknodeRateLimitError, upgrade at quicknode.com and pass your dedicated endpoint via rpcUrl.

Scope accepted types per-server:

evm.charge({
  recipient,
  chain: "base",
  rpcUrl, // optional override; omit to use public endpoint
  credentialTypes: ["permit2", "authorization"], // drop 'hash' if you don't want client-paid flows
  submitter: { privateKey: SUBMITTER_PK },
});

Client — pay for content

import { Mppx, evm } from "@quicknode/mpp/client";
import { privateKeyToAccount } from "viem/accounts";

const { fetch } = Mppx.create({
  methods: [
    evm.charge({
      account: privateKeyToAccount(process.env.AGENT_PK! as `0x${string}`),
      // rpcUrl only needed if you want to allow the 'hash' credential path
    }),
  ],
});

// Auto-handles 402 → pay → retry
const res = await fetch("https://api.merchant.com/premium");

Set client preference order:

evm.charge({
  account,
  prefer: ["authorization", "permit2"], // skip 'hash' entirely
});

Rate limits

The default public RPC is rate-limited per IP. When the limit is exceeded, the SDK throws QuicknodeRateLimitError:

import { QuicknodeRateLimitError } from "@quicknode/mpp/server";

try {
  await mppx.evm.charge(/* ... */);
} catch (err) {
  if (err instanceof QuicknodeRateLimitError) {
    console.error(`Rate limited on ${err.chain}. Upgrade: ${err.upgradeUrl}`);
  }
}

To avoid the limit entirely, pass your own rpcUrl from any Quicknode plan.

Configuration

evm.charge (server)

| Option | Required | Default | Notes | | ----------------- | --------------------------------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | recipient | ✓ | — | Merchant wallet (receives USDC) | | chain | ✓ | — | 'base' \| 'ethereum' \| 'arbitrum' \| 'polygon' \| 'optimism' \| 'avalanche' \| 'linea' \| 'unichain' \| 'base-sepolia' | | rpcUrl | — | — | Defaults to Quicknode public endpoint for the chain. Rate-limited per IP. | | submitter | when credentialTypes contains permit2/authorization | — | { privateKey } or { account } | | credentialTypes | | per-token allowed set | Draft-ordered preference list | | token | | 'USDC' | Curated symbol: USDC \| EURC \| WETH \| USDT. Mutually exclusive with customToken. | | customToken | | — | Caller-supplied { address, decimals, symbol?, name?, version?, credentialTypes? }. Use for any ERC-20 by address, or for native (zero-address). See below. | | confirmations | | per-chain default | Block-depth check for hash credential | | maxReceiptAgeSeconds | | — | If set, rejects hash credentials whose receipt block is older than N seconds at verification time. Closes the historical-Transfer-replay class. Recommended ≥ slowest expected confirmation window (e.g. 600 for L1, 60 for fast L2). | | store | | Store.memory() | Any mppx AtomicStore (Cloudflare KV, Redis, Upstash) |

evm.charge (client)

| Option | Required | Notes | | ------------------------ | ------------------------ | ----------------------------------------------- | | account / privateKey | one of | Viem Account or raw 0x... hex | | rpcUrl | only if hash is chosen | Used to broadcast the ERC-20 transfer | | prefer | | ['permit2','authorization','hash'] by default |

Permit2 one-time approval

Before the agent can use permit2, it must approve Permit2 on each token:

// One-time, from the agent's wallet:
await walletClient.writeContract({
  address: USDC_ADDRESS,
  abi: parseAbi(["function approve(address,uint256)"]),
  functionName: "approve",
  args: ["0x000000000022D473030F116dDEE9F6B43aC78BA3", 2n ** 256n - 1n],
});

Custom tokens & native settlement

Pass customToken instead of token to settle in any ERC-20 by address, or in the chain's native coin (ETH / MATIC / AVAX / …):

// Any ERC-20 by address — e.g. DAI on mainnet
import { evm } from "@quicknode/mpp/server";

evm.charge({
  chain: "ethereum",
  recipient,
  submitter: { privateKey: SUBMITTER_PK },
  customToken: {
    address: "0x6B175474E89094C44Da98b954EedeAC495271d0F", // DAI
    decimals: 18,
    symbol: "DAI",
  },
});
// Native chain coin — set address to NATIVE_TOKEN_ADDRESS (zero address)
import { evm, NATIVE_TOKEN_ADDRESS } from "@quicknode/mpp/server";

evm.charge({
  chain: "base",
  recipient,
  customToken: {
    address: NATIVE_TOKEN_ADDRESS,
    decimals: 18,
    symbol: "ETH",
  },
  // No `submitter` needed — native settlement only supports the `hash`
  // credential, which the client broadcasts itself.
});

customToken fields:

| Field | Required | Notes | | ----------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | address | ✓ | ERC-20 contract address. Use NATIVE_TOKEN_ADDRESS for the chain's native coin. | | decimals | ✓ | 18 for native ETH / MATIC / AVAX. | | symbol | | Display only. | | name, version | | EIP-712 domain values. Pass these for authorization (EIP-3009) when the token's on-chain name() / version() reverts or differs from its EIP-712 domain. | | credentialTypes | | Defaults: ['permit2','hash'] for ERC-20, ['hash'] for native. |

Defaults intentionally exclude authorization for custom ERC-20s: only Circle FiatTokens (USDC, EURC) implement EIP-3009 reliably. Opt in by passing credentialTypes: ['authorization', ...] if your token implements it.

Native settlement is restricted to the hash credential and to direct EOA sends — tx.input === '0x', tx.to === recipient, tx.value === amount. Contract-mediated native transfers aren't accepted by the verifier.

Spec note: native settlement (zero-address currency) is a non-normative extension to draft-evm-charge-00, which scopes itself to ERC-20 transfers. Custom ERC-20 addresses are spec-compliant — the spec defines currency as a 20-byte hex string.

Live testing on Base Sepolia

export RPC_URL=https://base-sepolia.quiknode.pro/<key>
export PAYER_PK=0x...             # funded with Base Sepolia USDC
export SUBMITTER_PK=0x...         # funded with Base Sepolia ETH (for permit2/authorization)
export RECIPIENT=0x...

npx tsx scripts/live-sepolia.ts --type hash
npx tsx scripts/live-sepolia.ts --type authorization
npx tsx scripts/live-sepolia.ts --type permit2

# Or use the zero-config path (no RPC_URL env needed):
npx tsx scripts/live-sepolia.ts --type hash --use-default-rpc

Integration tests (opt-in)

A read-only sanity check against the default public endpoint is gated behind an env var:

MPP_INTEGRATION=1 npm test -- --test-name-pattern "public rpc"

Runs one getChainId call per supported chain. Requires real PUBLIC_RPC_PREFIX/PUBLIC_RPC_TOKEN values in src/constants.ts.

Versioning

This package follows SemVer with a beta caveat:

  • Until 1.0, minor versions may include breaking changes when draft-evm-charge-00 evolves or the public API needs to change. Pin an exact version in production.
  • Public API = everything exported from @quicknode/mpp, @quicknode/mpp/server, @quicknode/mpp/client, their submodules, and @quicknode/mpp/constants. Anything under internal/ is private and may change without notice.

Changelog

See GitHub Releases for per-version changes.

Contributing

Issues and PRs welcome at quiknode-labs/mpp. Before opening a PR:

npm run verify   # lint + typecheck + tests + build

Security

Found a vulnerability? Please email [email protected] instead of opening a public issue.

License

MIT