{"id":50395154,"url":"https://github.com/quiknode-labs/mpp","last_synced_at":"2026-05-30T20:30:56.356Z","repository":{"id":355102965,"uuid":"1217384258","full_name":"quiknode-labs/mpp","owner":"quiknode-labs","description":null,"archived":false,"fork":false,"pushed_at":"2026-05-01T20:47:15.000Z","size":178,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-01T22:24:18.075Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/quiknode-labs.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-21T20:42:06.000Z","updated_at":"2026-05-01T20:47:20.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/quiknode-labs/mpp","commit_stats":null,"previous_names":["quiknode-labs/mpp"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/quiknode-labs/mpp","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quiknode-labs%2Fmpp","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quiknode-labs%2Fmpp/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quiknode-labs%2Fmpp/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quiknode-labs%2Fmpp/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/quiknode-labs","download_url":"https://codeload.github.com/quiknode-labs/mpp/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quiknode-labs%2Fmpp/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33709269,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-30T02:00:06.278Z","response_time":92,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-30T20:30:53.742Z","updated_at":"2026-05-30T20:30:56.347Z","avatar_url":"https://github.com/quiknode-labs.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# @quicknode/mpp\n\n\u003e [!WARNING]\n\u003e **Beta.** Public API may change between minor versions until v1. Pin to an exact version in production.\n\nSDK 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](https://github.com/tempoxyz/mpp-specs).\n\nImplements and expands on the [`draft-evm-charge-00`](https://github.com/tempoxyz/mpp-specs/blob/2f6bfcee6f9e448d2ded15dc350dc92967e17513/specs/methods/evm/draft-evm-charge-00.md) spec\nwith all three non-trivial credential types:\n\n| Type                    | Binding                          | Gas         | UX                                    |\n| ----------------------- | -------------------------------- | ----------- | ------------------------------------- |\n| `permit2` (RECOMMENDED) | Strong (EIP-712 witness)         | Server pays | One signature, any ERC-20             |\n| `authorization`         | Strong (on-chain nonce)          | Server pays | One signature, USDC / EIP-3009 tokens |\n| `hash`                  | Weakest (post-hoc receipt match) | Client pays | Client broadcasts + waits             |\n\n\u003e [!CAUTION]\n\u003e 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](#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`.\n\n## Contents\n\n- [Install](#install)\n- [Compatibility](#compatibility)\n- [Server — accept payments](#server--accept-payments)\n- [Client — pay for content](#client--pay-for-content)\n- [Rate limits](#rate-limits)\n- [Configuration](#configuration)\n- [Live testing on Base Sepolia](#live-testing-on-base-sepolia)\n- [Versioning](#versioning)\n- [Changelog](#changelog)\n- [Contributing](#contributing)\n- [Security](#security)\n- [License](#license)\n\n## Install\n\n```bash\nnpm install @quicknode/mpp mppx viem\n```\n\n## Compatibility\n\n- **Node.js** ≥ 22\n- **Cloudflare Workers**, **Bun**, and other WebCrypto-capable runtimes\n- **Browsers** — the client (`@quicknode/mpp/client`) runs in-browser for agent UIs. The server modules import Node-only code and are not browser-bundleable\n- **TypeScript** — types ship in the package; no separate `@types/*` install needed\n\n## Server — accept payments\n\n```ts\nimport { Mppx, evm } from \"@quicknode/mpp/server\";\n\nconst mppx = Mppx.create({\n  methods: [\n    evm.charge({\n      recipient: \"0xMerchantWallet\",\n      chain: \"base\",\n      submitter: { privateKey: process.env.SUBMITTER_PK! },\n    }),\n  ],\n  secretKey: process.env.MPP_SECRET_KEY!,\n});\n\n// mppx.evm.charge({ amount: '0.01', decimals: 6 })(request) → 402 challenge or verified receipt\n```\n\nNo `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](https://www.quicknode.com/?utm_source=mpp-sdk) and pass your dedicated endpoint via `rpcUrl`.\n\nScope accepted types per-server:\n\n```ts\nevm.charge({\n  recipient,\n  chain: \"base\",\n  rpcUrl, // optional override; omit to use public endpoint\n  credentialTypes: [\"permit2\", \"authorization\"], // drop 'hash' if you don't want client-paid flows\n  submitter: { privateKey: SUBMITTER_PK },\n});\n```\n\n## Client — pay for content\n\n```ts\nimport { Mppx, evm } from \"@quicknode/mpp/client\";\nimport { privateKeyToAccount } from \"viem/accounts\";\n\nconst { fetch } = Mppx.create({\n  methods: [\n    evm.charge({\n      account: privateKeyToAccount(process.env.AGENT_PK! as `0x${string}`),\n      // rpcUrl only needed if you want to allow the 'hash' credential path\n    }),\n  ],\n});\n\n// Auto-handles 402 → pay → retry\nconst res = await fetch(\"https://api.merchant.com/premium\");\n```\n\nSet client preference order:\n\n```ts\nevm.charge({\n  account,\n  prefer: [\"authorization\", \"permit2\"], // skip 'hash' entirely\n});\n```\n\n## Rate limits\n\nThe default public RPC is rate-limited per IP. When the limit is exceeded, the SDK throws `QuicknodeRateLimitError`:\n\n```ts\nimport { QuicknodeRateLimitError } from \"@quicknode/mpp/server\";\n\ntry {\n  await mppx.evm.charge(/* ... */);\n} catch (err) {\n  if (err instanceof QuicknodeRateLimitError) {\n    console.error(`Rate limited on ${err.chain}. Upgrade: ${err.upgradeUrl}`);\n  }\n}\n```\n\nTo avoid the limit entirely, pass your own `rpcUrl` from any Quicknode plan.\n\n## Configuration\n\n### `evm.charge` (server)\n\n| Option            | Required                                                  | Default               | Notes                                                                                                                                                        |\n| ----------------- | --------------------------------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `recipient`       | ✓                                                         | —                     | Merchant wallet (receives USDC)                                                                                                                              |\n| `chain`           | ✓                                                         | —                     | `'base' \\| 'ethereum' \\| 'arbitrum' \\| 'polygon' \\| 'optimism' \\| 'avalanche' \\| 'linea' \\| 'unichain' \\| 'base-sepolia'`                                    |\n| `rpcUrl`          | —                                                         | —                     | Defaults to Quicknode public endpoint for the chain. Rate-limited per IP.                                                                                    |\n| `submitter`       | when `credentialTypes` contains `permit2`/`authorization` | —                     | `{ privateKey }` or `{ account }`                                                                                                                            |\n| `credentialTypes` |                                                           | per-token allowed set | Draft-ordered preference list                                                                                                                                |\n| `token`           |                                                           | `'USDC'`              | Curated symbol: `USDC \\| EURC \\| WETH \\| USDT`. Mutually exclusive with `customToken`.                                                                       |\n| `customToken`     |                                                           | —                     | Caller-supplied `{ address, decimals, symbol?, name?, version?, credentialTypes? }`. Use for any ERC-20 by address, or for native (zero-address). See below. |\n| `confirmations`   |                                                           | per-chain default     | Block-depth check for `hash` credential                                                                                                                      |\n| `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). |\n| `store`           |                                                           | `Store.memory()`      | Any mppx `AtomicStore` (Cloudflare KV, Redis, Upstash)                                                                                                       |\n\n### `evm.charge` (client)\n\n| Option                   | Required                 | Notes                                           |\n| ------------------------ | ------------------------ | ----------------------------------------------- |\n| `account` / `privateKey` | one of                   | Viem `Account` or raw `0x...` hex               |\n| `rpcUrl`                 | only if `hash` is chosen | Used to broadcast the ERC-20 transfer           |\n| `prefer`                 |                          | `['permit2','authorization','hash']` by default |\n\n### Permit2 one-time approval\n\nBefore the agent can use `permit2`, it must approve Permit2 on each token:\n\n```ts\n// One-time, from the agent's wallet:\nawait walletClient.writeContract({\n  address: USDC_ADDRESS,\n  abi: parseAbi([\"function approve(address,uint256)\"]),\n  functionName: \"approve\",\n  args: [\"0x000000000022D473030F116dDEE9F6B43aC78BA3\", 2n ** 256n - 1n],\n});\n```\n\n### Custom tokens \u0026 native settlement\n\nPass `customToken` instead of `token` to settle in any ERC-20 by address, or\nin the chain's native coin (ETH / MATIC / AVAX / …):\n\n```ts\n// Any ERC-20 by address — e.g. DAI on mainnet\nimport { evm } from \"@quicknode/mpp/server\";\n\nevm.charge({\n  chain: \"ethereum\",\n  recipient,\n  submitter: { privateKey: SUBMITTER_PK },\n  customToken: {\n    address: \"0x6B175474E89094C44Da98b954EedeAC495271d0F\", // DAI\n    decimals: 18,\n    symbol: \"DAI\",\n  },\n});\n```\n\n```ts\n// Native chain coin — set address to NATIVE_TOKEN_ADDRESS (zero address)\nimport { evm, NATIVE_TOKEN_ADDRESS } from \"@quicknode/mpp/server\";\n\nevm.charge({\n  chain: \"base\",\n  recipient,\n  customToken: {\n    address: NATIVE_TOKEN_ADDRESS,\n    decimals: 18,\n    symbol: \"ETH\",\n  },\n  // No `submitter` needed — native settlement only supports the `hash`\n  // credential, which the client broadcasts itself.\n});\n```\n\n`customToken` fields:\n\n| Field             | Required | Notes                                                                                                                                                         |\n| ----------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `address`         | ✓        | ERC-20 contract address. Use `NATIVE_TOKEN_ADDRESS` for the chain's native coin.                                                                              |\n| `decimals`        | ✓        | 18 for native ETH / MATIC / AVAX.                                                                                                                             |\n| `symbol`          |          | Display only.                                                                                                                                                 |\n| `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. |\n| `credentialTypes` |          | Defaults: `['permit2','hash']` for ERC-20, `['hash']` for native.                                                                                             |\n\nDefaults intentionally exclude `authorization` for custom ERC-20s: only Circle\nFiatTokens (USDC, EURC) implement EIP-3009 reliably. Opt in by passing\n`credentialTypes: ['authorization', ...]` if your token implements it.\n\nNative settlement is restricted to the `hash` credential and to direct EOA\nsends — `tx.input === '0x'`, `tx.to === recipient`, `tx.value === amount`.\nContract-mediated native transfers aren't accepted by the verifier.\n\n\u003e **Spec note**: native settlement (zero-address `currency`) is a\n\u003e non-normative extension to\n\u003e [`draft-evm-charge-00`](https://github.com/tempoxyz/mpp-specs/blob/2f6bfcee6f9e448d2ded15dc350dc92967e17513/specs/methods/evm/draft-evm-charge-00.md),\n\u003e which scopes itself to ERC-20 transfers. Custom ERC-20 addresses are\n\u003e spec-compliant — the spec defines `currency` as a 20-byte hex string.\n\n## Live testing on Base Sepolia\n\n```bash\nexport RPC_URL=https://base-sepolia.quiknode.pro/\u003ckey\u003e\nexport PAYER_PK=0x...             # funded with Base Sepolia USDC\nexport SUBMITTER_PK=0x...         # funded with Base Sepolia ETH (for permit2/authorization)\nexport RECIPIENT=0x...\n\nnpx tsx scripts/live-sepolia.ts --type hash\nnpx tsx scripts/live-sepolia.ts --type authorization\nnpx tsx scripts/live-sepolia.ts --type permit2\n\n# Or use the zero-config path (no RPC_URL env needed):\nnpx tsx scripts/live-sepolia.ts --type hash --use-default-rpc\n```\n\n### Integration tests (opt-in)\n\nA read-only sanity check against the default public endpoint is gated behind an env var:\n\n```bash\nMPP_INTEGRATION=1 npm test -- --test-name-pattern \"public rpc\"\n```\n\nRuns one `getChainId` call per supported chain. Requires real `PUBLIC_RPC_PREFIX`/`PUBLIC_RPC_TOKEN` values in `src/constants.ts`.\n\n## Versioning\n\nThis package follows [SemVer](https://semver.org/) with a beta caveat:\n\n- **Until 1.0**, minor versions may include breaking changes when [`draft-evm-charge-00`](https://github.com/tempoxyz/mpp-specs/blob/2f6bfcee6f9e448d2ded15dc350dc92967e17513/specs/methods/evm/draft-evm-charge-00.md) evolves or the public API needs to change. Pin an exact version in production.\n- **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.\n\n## Changelog\n\nSee [GitHub Releases](https://github.com/quiknode-labs/mpp/releases) for per-version changes.\n\n## Contributing\n\nIssues and PRs welcome at [quiknode-labs/mpp](https://github.com/quiknode-labs/mpp). Before opening a PR:\n\n```bash\nnpm run verify   # lint + typecheck + tests + build\n```\n\n## Security\n\nFound a vulnerability? Please email security@quicknode.com instead of opening a public issue.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fquiknode-labs%2Fmpp","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fquiknode-labs%2Fmpp","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fquiknode-labs%2Fmpp/lists"}