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

@three-ws/x402-payment-modal

v1.2.0

Published

Drop-in x402 payment modal for any paid HTTP endpoint. One ES module, zero runtime dependencies — handles wallet connect (Phantom/Solana + EVM via EIP-3009), the 402 → sign → settle flow, SIWX re-entry, client-side spending caps, and a receipt. Vanilla JS

Readme

@three-ws/x402-payment-modal

The complete drop-in checkout for any x402 paid endpoint — wallet connect, sign, settle, receipt.

npm npm downloads bundle size License: Proprietary

A drop-in payment modal for any x402 paid endpoint. The browser client is one ES module with zero runtime dependencies; it turns an HTTP 402 Payment Required challenge into a polished checkout: wallet connect (Phantom / Solflare / Backpack on Solana, MetaMask / any EVM wallet on Base via EIP-3009), the 402 → sign → settle flow, optional SIWX re-entry, client-side spending caps, and a receipt. It ships a server checkout adapter for the Solana rail (./server, ./server/express, ./server/vercel) and a React wrapper (./react) — pick only the pieces you need. Vanilla JS, no bundler required.

<script type="module" src="https://unpkg.com/@three-ws/x402-payment-modal"></script>

<button
  data-x402-endpoint="https://api.example.com/paid/summarize"
  data-x402-method="POST"
  data-x402-body='{"text":"hello"}'
  data-x402-merchant="Acme"
  data-x402-action="Summarize">
  Pay &amp; Run
</button>

That's the whole integration. Clicking the button opens the modal, runs the payment, calls your endpoint with the signed X-PAYMENT header, and fires an x402:result event with the response.


Why

x402 lets any HTTP endpoint charge a micropayment per call: the server answers an unpaid request with 402 and a list of accepted payments; the client pays and retries with an X-PAYMENT header. The protocol is simple — the wallet UX is not. This package is the missing front end: a single, framework-agnostic modal that handles wallet detection, chain switching, signing, settlement, errors, throttle retries, and the receipt, so you ship a paid endpoint in minutes.

  • Zero dependencies, zero build. Plain ES module. Drop in a <script> tag or import it. The Solana/EVM crypto helpers are loaded lazily from a CDN only when a payment is actually attempted.
  • Solana + EVM. Phantom (Solana) and any injected EVM wallet (Base USDC via EIP-3009 transferWithAuthorization). The modal picks the right path from the 402 challenge.
  • USDC by default, multi-token if you want it. USDC is the always-on default on Solana. Optionally offer a second SPL token (the modal ships a built-in THREE opt-in) and the buyer gets a token picker — each token is recognized on sight (symbol, decimals). See Accepting multiple Solana tokens.
  • SIWX re-entry. If a wallet already paid for a resource, it can sign back in instead of paying again. See docs/siwx.md.
  • Spending caps. Optional per-call / hourly / daily caps enforced in the browser. See docs/spending-caps.md.
  • Themeable. Light + automatic dark mode, a full --x402-* design-token surface, runtime brand-matching, and a header logo. See docs/theming.md.
  • Accessible. Focus trap + restore, aria-modal, aria-live step announcements, :focus-visible rings, prefers-reduced-motion, keyboard Esc to close.
  • Server + React adapters in the box. Framework-agnostic Solana checkout (./server) with Express and Vercel adapters, and a first-class React wrapper (./react) — all behind separate export subpaths, all optional.

Which x402 modal do I want?

This package has a lighter sibling, @three-ws/x402-modal. Both render a drop-in modal; choose by how much you need.

| | @three-ws/x402-payment-modal (this) | @three-ws/x402-modal | | --- | --- | --- | | Goal | Full checkout SDK: client + server + React | Minimal client-only modal | | Chains | Solana and EVM (Base/Arbitrum/Optimism) | Client modal, EVM-leaning | | Solana server adapter | ✅ ./server + Express + Vercel | ❌ bring your own | | React wrapper | ✅ ./react (X402Button, useX402) | ❌ | | SIWX re-entry | ✅ | — | | Client spending caps | ✅ per-call / hour / day | — | | Multi-token picker (USDC + opt-in SPL) | ✅ | — | | Builder-code (ERC-8021) echo | ✅ | — | | Footprint | Larger (more surface) | Smaller, fewer knobs |

Rule of thumb: taking real Solana payments, want a React component, or need caps/SIWX → use this package. Just need a tiny EVM paywall button and nothing else → the lighter @three-ws/x402-modal is enough.

Package subpaths

Import only what you use — each subpath is independent and its peer deps are optional.

| Subpath | Import | Needs | | --- | --- | --- | | . | @three-ws/x402-payment-modal | nothing (browser) | | ./min | @three-ws/x402-payment-modal/min | nothing — pre-minified bundle | | ./server | @three-ws/x402-payment-modal/server | @solana/web3.js, @solana/spl-token | | ./server/express | @three-ws/x402-payment-modal/server/express | + express | | ./server/vercel | @three-ws/x402-payment-modal/server/vercel | @solana/web3.js, @solana/spl-token | | ./react | @three-ws/x402-payment-modal/react | react |


Install

Via CDN (no build step):

<script type="module" src="https://unpkg.com/@three-ws/x402-payment-modal"></script>

Via npm (bundler / framework):

npm install @three-ws/x402-payment-modal
import { pay, configure } from '@three-ws/x402-payment-modal';

Optional peer dependencies

The core client has no runtime dependencies. The other subpaths declare optional peer deps — install them only when you use that subpath:

| You use… | Install | | --- | --- | | ./server or ./server/vercel (Solana checkout) | @solana/web3.js @solana/spl-token | | ./server/express | @solana/web3.js @solana/spl-token express | | ./react | react (you already have it) |

# Solana checkout server
npm install @solana/web3.js @solana/spl-token
# …and express if you use the Express adapter
npm install express

EVM-only sites need nothing server-side: the wallet signs EIP-3009 typed-data entirely in the browser. The Solana/EVM crypto helpers used by the client are loaded lazily from a CDN only when a payment is attempted, so they are not in your bundle.


Quick start

1. Declarative (HTML attributes)

Any element with data-x402-endpoint is auto-bound on load and re-scanned as the DOM changes:

<button
  data-x402-endpoint="/api/paid/translate"
  data-x402-method="POST"
  data-x402-body='{"text":"bonjour","to":"en"}'
  data-x402-merchant="Acme Translate"
  data-x402-action="Translate">
  Translate ($0.01)
</button>

<script type="module" src="https://unpkg.com/@three-ws/x402-payment-modal"></script>

<script>
  document.querySelector('button').addEventListener('x402:result', (e) => {
    console.log('paid + result:', e.detail.result);
  });
</script>

2. Programmatic

import { pay } from '@three-ws/x402-payment-modal';

try {
  const { result, payment } = await pay({
    endpoint: '/api/paid/translate',
    method: 'POST',
    body: { text: 'bonjour', to: 'en' },
    merchant: 'Acme Translate',
    action: 'Translate',
  });
  console.log(result, 'tx:', payment?.transaction);
} catch (err) {
  if (err.code !== 'cancelled') console.error(err);
}

See the runnable examples/ — plain HTML, React, and an Express server.


Architecture

Three independent pieces, each optional except the client:

  • Client (. / ./min) — renders the modal, detects wallets, drives the 402 → sign → settle flow. Zero runtime deps.
  • Server adapter (./server*) — Solana only. Builds the SPL transfer Phantom signs and wraps it into the X-PAYMENT envelope. EVM never touches it.
  • React wrapper (./react) — X402Button + useX402, SSR-safe.
flowchart TD
    A[User clicks pay] --> B[Client: GET/POST endpoint]
    B --> C{HTTP 402 + accepts?}
    C -->|no, 200| X[Error: not an x402 endpoint]
    C -->|yes| D[discover: show price + network]
    D --> E[connect: pick & connect wallet]
    E --> F{network}
    F -->|EVM| G[sign EIP-3009 typed-data\nbrowser only, no server]
    F -->|Solana| H[POST ./server ?action=prepare]
    H --> I[Phantom signs the v0 tx]
    I --> J[POST ./server ?action=encode → X-PAYMENT]
    G --> K[verify: retry with X-PAYMENT]
    J --> K
    K --> L{settled?}
    L -->|429 throttle| K
    L -->|2xx| M[receipt + result → resolve PayResult]
    L -->|4xx/5xx| N[error + roll back any cap reservation]

Full walkthrough in docs/architecture.md.


Client API

| Export | Signature | Purpose | | --- | --- | --- | | pay | pay(opts) => Promise<PayResult> | Open the modal and run a payment. | | configure | configure(opts) => Config | Set checkout origin, branding, builder-code, CDN URLs. | | init | init() => void | Bind [data-x402-endpoint] elements (auto-called). | | version | string | Library version. |

Also exposed as window.X402.{ pay, init, configure, version } for inline scripts.

pay(opts)opts:

| Field | Type | Notes | | --- | --- | --- | | endpoint | string | Required. The paid (x402) URL. | | method | string | Default GET, or POST when body is set. | | body | object \| string | Objects are JSON-stringified. | | headers | object | Extra headers merged into the paid call. | | merchant | string | Shown in the modal header. | | action | string | Shown in the modal header. | | autoConnect | boolean | Skip the picker when exactly one wallet is detected. | | caps | SpendingCaps | { maxPerCall, maxPerHour, maxPerDay } in atomic micro-USD. |

Resolves to PayResult: { ok: true, result, payment?: { network, transaction, payer }, siwx?: { address, network }, response: { status, headers } }. Rejects with an Error whose .code === 'cancelled' when the user closes the modal.

DOM events (bubbling CustomEvents on the clicked element): x402:result (detail = PayResult), x402:error (detail = { error }), x402:siwx-signed (detail = { address, network }).

Full reference: docs/api-reference.md.


Configuration

Defaults work out of the box. Override host-specific bits with configure():

import { configure } from '@three-ws/x402-payment-modal';

configure({
  checkoutOrigin: 'https://pay.acme.com', // where your Solana checkout endpoint lives
  brand: { name: 'Acme', url: 'https://acme.com' },
  footerNote: 'Acme Pay',
});

…or with data-* attributes on the script tag (no inline module needed):

<script type="module"
  src="https://unpkg.com/@three-ws/x402-payment-modal"
  data-x402-checkout-origin="https://pay.acme.com"
  data-x402-brand-name="Acme"
  data-x402-brand-url="https://acme.com"></script>

| Option | data-* attribute | Default | | --- | --- | --- | | checkoutOrigin | data-x402-checkout-origin | the script's own origin | | checkoutPath | data-x402-checkout-path | /api/x402-checkout | | footerNote | data-x402-footer-note | x402 · onchain settled | | brand.name / brand.url | data-x402-brand-name / -brand-url | null (footer link hidden) | | brand.logo | — | null (no header logo) | | theme | — | 'auto' (follow the OS) | | cssVars | — | null (use built-in tokens) | | builderCode.wallet / .service | data-x402-builder-wallet / -builder-service | '' (no self-attribution) | | esm.solanaWeb3 / esm.nobleHashesSha3 | — | pinned esm.sh URLs |

theme is 'auto' | 'light' | 'dark'; cssVars is a flat map of --x402-* design tokens (e.g. { '--x402-accent': '#ff5c00', '--x402-radius': '8px' }) for runtime brand-matching — see docs/theming.md. Repoint esm.* at a self-hosted mirror if a strict Content-Security-Policy blocks esm.sh. (The EVM/Base path needs no third-party code at all.)


Server (Solana only)

Phantom signs serialized transactions but doesn't build instructions, so the Solana path needs a tiny endpoint to build the SPL transfer and wrap the signed tx into the X-PAYMENT envelope. The package ships it.

Express:

import express from 'express';
import { x402CheckoutRouter } from '@three-ws/x402-payment-modal/server/express';

const app = express();
app.use(express.json());
app.use('/api/x402-checkout', x402CheckoutRouter({ rpcUrl: process.env.SOLANA_RPC_URL }));
app.listen(3000);

Vercel / Next.jsapi/x402-checkout.js:

export { default } from '@three-ws/x402-payment-modal/server/vercel';

Pass rpcUrls: [...] (an ordered list, tried with failover) instead of a single rpcUrl for production — the public RPC is rate-limited and warns once at startup. Lower-level helpers (prepareSolanaCheckout, encodeX402Payment, handleCheckout, CheckoutError, solanaAccept) are exported from @three-ws/x402-payment-modal/server. Full guide: docs/server-setup.md.


React

The ./react subpath ships a hook and a button. The browser-only core is dynamically imported on first use, so both are SSR-safe — nothing runs during render or on the server.

import { X402Button, useX402 } from '@three-ws/x402-payment-modal/react';

// Drop-in button:
<X402Button
  endpoint="/api/paid/summarize"
  method="POST"
  body={{ url: 'https://en.wikipedia.org/wiki/x402' }}
  merchant="Acme"
  action="Summarize"
  label="Summarize for $0.01"
  onResult={(r) => console.log('paid', r.payment)}
  onError={(e) => console.error(e)}
/>;

// …or drive it yourself with the hook:
function Buy() {
  const { pay, isPaying, result, error } = useX402({ merchant: 'Acme' });
  return (
    <button disabled={isPaying} onClick={() => pay({ endpoint: '/api/paid/summarize' })}>
      {isPaying ? 'Processing…' : 'Pay'}
    </button>
  );
}

useX402(defaults) returns { pay, status, result, error, reset, isPaying } where status is idle | paying | done | error. Full reference: docs/react.md.


Accepting multiple Solana tokens

USDC is the default settlement asset, but an x402 challenge can list more than one accepted payment. List a USDC accept and a second SPL-token accept on Solana, and the modal renders a token picker: the buyer chooses which to pay in, the headline price and the transaction follow the choice. No client wiring — the checkout endpoint already builds an SPL transfer for whatever mint the chosen accept names. The package ships a built-in THREE opt-in token as a ready example, but any mint works.

Build the accepts with the solanaAccept helper (no hardcoded mints):

import { solanaAccept } from '@three-ws/x402-payment-modal/server';

// feePayer is your facilitator's sponsor account (pays the SOL network fee).
const accepts = [
  solanaAccept({ token: 'usdc',  uiAmount: 0.25, payTo, feePayer }), // $0.25 in USDC
  solanaAccept({ token: 'three', uiAmount: 1000, payTo, feePayer }), // or 1,000 THREE
];

// Return them in your 402 body / PAYMENT-REQUIRED envelope:
//   { x402Version: 2, accepts }

solanaAccept takes token: 'usdc' | 'three' (or an explicit mint for any SPL token), the price as uiAmount (human units) or amount (atomic string), and emits a spec-shaped accept. The constants THREE_MINT, USDC_MINT_SOLANA, and WELL_KNOWN_SOLANA_TOKENS are exported too. On the client, the same mints are exposed at window.X402.tokens for inline merchants.

Non-stable tokens float against the dollar. The modal can't dollar-denominate a floating-price token for browser-side spending caps — so enforce caps for any non-stable token (like THREE) server-side. USDC and other stablecoin caps work in the browser as usual.


Documentation


Browser support

Modern evergreen browsers (ES2020, BigInt, dynamic import(), fetch, crypto.getRandomValues). The Solana wallet path requires Phantom; the EVM path requires an injected wallet (window.ethereum).

Security

  • Payments are signed in the user's wallet — this package never sees a private key. The Solana checkout server only builds an unsigned transaction and base64- wraps the user-signed one; it cannot move funds.
  • SIWX re-entry is a gasless CAIP-122 signature, never a transfer. The client detects the offer, signs, and submits the proof; the server must verify the nonce/domain/expiry/signature and check the entitlement — see docs/siwx.md.
  • Spending caps are a client-side guardrail, not a security boundary — a user can clear localStorage. Enforce real limits server-side.
  • Pin the script to a version or self-host it for production (the lazily-loaded crypto helpers can be repointed at a mirror via configure({ esm }) to satisfy a strict CSP).

FAQ & troubleshooting

The modal opens but says "Couldn't confirm the price." Discovery hit a response that wasn't an x402 challenge. The endpoint must answer the unpaid request with HTTP 402 (or an MCP-style 401) carrying an accepts array — in the JSON body or a base64 payment-required header. A 200 means the endpoint isn't paywalled; the modal surfaces that instead of silently succeeding.

Do I need a server? Only for Solana. EVM (Base/Arbitrum/Optimism USDC) signs EIP-3009 typed-data entirely in the browser. For Solana, mount ./server so the modal can build the SPL transfer Phantom signs — see docs/server-setup.md.

Solana payments fail with an RPC error under load. You're on the public RPC (it warns once at startup). Pass a dedicated endpoint — x402CheckoutRouter({ rpcUrls: ['https://your-helius-url', ...] }) — and the adapter rotates across the list on transient failures.

A Token-2022 mint (e.g. THREE) wouldn't settle in an older version. Fixed in 1.2.0 — the server now detects each mint's owning program (legacy SPL Token vs Token-2022) and derives ATAs and transferChecked against the right one.

esm.sh is blocked by my Content-Security-Policy. The client lazy-loads @solana/web3.js and @noble/hashes from a CDN only when a payment is attempted. Repoint them at a self-hosted mirror: configure({ esm: { solanaWeb3: '/vendor/solana-web3.js', nobleHashesSha3: '/vendor/noble-sha3.js' } }). EVM-only sites load no third-party code at all.

Why did pay() reject? A user dismissing the modal rejects with err.code === 'cancelled' — treat it as a no-op, not a failure. Any other rejection is a real error. The declarative auto-binder swallows cancellation and only fires x402:error for genuine failures.

My spending caps don't apply to a non-stablecoin. Browser caps are denominated in micro-USD and the dependency-free client doesn't fetch live prices, so caps only bind stablecoins (USDC/USDT/DAI). Enforce caps for floating-price tokens server-side — see docs/spending-caps.md.

Can the user pick which token to pay in? Yes — if the 402 challenge lists more than one Solana accept, the modal renders a token picker. See Accepting multiple Solana tokens.


Related packages

License

Proprietary — Copyright (c) 2026 nirholas. All Rights Reserved. Unauthorized use, copying, modification, or distribution is prohibited. See LICENSE.

Standalone package. Browser client is dependency-free; server/react peer deps are optional. Issues and PRs welcome — see CONTRIBUTING.md and https://github.com/nirholas/x402-payment-modal/issues.