
Security News
AI Has Taken Over Open Source
Vibe coding at scale is reshaping how packages are created, contributed, and selected across the software supply chain
@quicknode/mpp
Advanced tools
[!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
hashcredential 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, setmaxReceiptAgeSecondson 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, preferpermit2orauthorization.
npm install @quicknode/mpp mppx viem
@quicknode/mpp/client) runs in-browser for agent UIs. The server modules import Node-only code and are not browser-bundleable@types/* install neededimport { 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 },
});
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
});
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.
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 |
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],
});
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 todraft-evm-charge-00, which scopes itself to ERC-20 transfers. Custom ERC-20 addresses are spec-compliant — the spec definescurrencyas a 20-byte hex string.
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
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.
This package follows SemVer with a beta caveat:
draft-evm-charge-00 evolves or the public API needs to change. Pin an exact version in production.@quicknode/mpp, @quicknode/mpp/server, @quicknode/mpp/client, their submodules, and @quicknode/mpp/constants. Anything under internal/ is private and may change without notice.See GitHub Releases for per-version changes.
Issues and PRs welcome at quiknode-labs/mpp. Before opening a PR:
npm run verify # lint + typecheck + tests + build
Found a vulnerability? Please email security@quicknode.com instead of opening a public issue.
MIT
FAQs
Quicknode payment methods for MPP (Machine Payments Protocol)
We found that @quicknode/mpp demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 28 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Vibe coding at scale is reshaping how packages are created, contributed, and selected across the software supply chain

Security News
npm invalidated all granular access tokens that bypass 2FA after a fresh Mini Shai-Hulud wave compromised 323 npm packages. Staged publishing also entered public preview.

Research
/Security News
Compromised npm package art-template delivered a Coruna-like iOS Safari exploit framework through a watering-hole attack.