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-modal

v0.3.0

Published

A drop-in, dependency-free payment modal for any x402 paid endpoint. One script tag turns a 402 challenge into a polished checkout: wallet connect (Phantom on Solana, MetaMask/EVM via EIP-3009), the 402 → sign → settle flow, SIWX re-entry, spending caps,

Readme

@three-ws/x402-modal

A drop-in payment modal for any x402 paid endpoint.

One script tag turns an HTTP 402 Payment Required into a polished checkout: wallet connect (Phantom on Solana, MetaMask/EVM via EIP-3009), the 402 → sign → settle flow, SIWX re-entry, spending caps, and a receipt — all in vanilla JS, with no bundler and no framework.

npm downloads license node

Quick start · How it works · API · Configuration · Backend · Tutorials · FAQ


Why

x402 revives HTTP 402 Payment Required as a real payment rail: a server answers a request with a 402 whose body lists what it accepts (asset, amount, network, pay-to), the client pays, and re-sends the request with an X-PAYMENT header. It's perfect for pay-per-call APIs, agent economies, and content paywalls — but every merchant ends up rebuilding the same fiddly client: parse the challenge, connect a wallet, sign the right thing for the right chain, retry, settle, show a receipt.

This package is that client, done once and done well. Point it at a 402 endpoint and it renders the entire flow. The EVM/Base path is 100% client-side. The Solana path needs one small backend helper (see The backend).

Quick start

1 — One script tag (zero JS)

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

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

Clicking the button opens the modal, runs the payment, calls the endpoint, and fires an x402:result event on the button with { ok, result, payment, response }:

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

2 — Programmatic (full control)

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

const out = await pay({
  endpoint: '/api/paid/summarize',
  method: 'POST',
  body: { text: 'hello world' },
  merchant: 'Acme',
  action: 'Summarize',
});

console.log(out.result);   // the endpoint's response, after settlement
console.log(out.payment);  // { network, payer, transaction }

pay() resolves once the paid call returns 200, or rejects with an Error whose .code === 'cancelled' if the user closes the modal.

3 — Self-hosted, fully branded

<script
  type="module"
  src="https://your.cdn/x402.global.js"
  data-x402-api-origin="https://pay.your-company.com"
  data-x402-brand-label="Powered by Acme"
  data-x402-brand-href="https://acme.com"></script>

or from JS, before the first pay():

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

configure({
  apiOrigin: 'https://pay.your-company.com',     // Solana checkout backend
  brand: { label: 'Powered by Acme', href: 'https://acme.com' },
});

No dependencies to install. The ESM build leaves the two optional wallet libraries (@solana/web3.js for Solana, a keccak for EVM sign-in) as runtime CDN imports, fetched only when that wallet path actually runs. The /global build is a single self-contained file.

How it works

pay({ endpoint })
   │
   ├─ 1. discover   GET/POST endpoint → 402 (or 401 + payment-required header)
   │                parse `accepts[]` (asset · amount · network · payTo)
   │
   ├─ 2. connect    pick a wallet that can satisfy an accept:
   │                  Solana → Phantom          EVM → MetaMask / window.ethereum
   │
   ├─ 3. authorize  Solana: backend builds the tx → Phantom signs it
   │                  EVM:   wallet signs an EIP-3009 transferWithAuthorization
   │                  (no on-chain tx, no gas for the payer)
   │
   └─ 4. verify     re-send the request with `X-PAYMENT` → endpoint runs the work,
                    settles on-chain, returns 200 + `x-payment-response` receipt

Each step renders as a live row in the modal (spinner → check → error), with a Try again affordance on failure, automatic retry on a 429 upstream throttle (the payment isn't settled until the work succeeds, so re-sending can't double-charge), and a receipt with an explorer link on success.

Networks

| Network | Wallet | Scheme | Needs a backend? | |---|---|---|---| | Base (eip155:8453) | MetaMask / any window.ethereum | EIP-3009 transferWithAuthorization | No — fully client-side | | Base Sepolia, Arbitrum, Optimism | same | EIP-3009 | No | | Solana (solana:*) | Phantom | exact (facilitator-settled) | Yes — prepare/encode helper |

When a 402 advertises more than one network the modal shows a wallet picker; when it advertises exactly one, it goes straight there.

API

pay(options): Promise<PayResult>

| option | type | default | notes | |---------------|-------------------------------|--------------------|-------| | endpoint | string | — (required) | the x402-protected URL to pay for and call | | method | string | GET / POST* | *POST when a body is set | | body | object \| string | — | forwarded to the endpoint (object → JSON) | | headers | Record<string,string> | — | merged into discovery + paid calls | | merchant | string | Payment | shown in the modal header | | action | string | Pay-per-call | shown in the modal header | | caps | { maxPerCall, maxPerHour, maxPerDay } | — | µUSD spending caps (see Configuration) | | autoConnect | boolean | false | skip the picker when exactly one wallet is detected | | apiOrigin | string | global config | per-call override of the Solana checkout backend | | brand | { label, href } | global config | per-call footer override |

Returns { ok: true, result, payment?, siwx?, response }. payment is present on a fresh payment ({ network, payer, transaction }); siwx is present when the user re-entered via sign-in instead of paying.

discover(options): Promise<PaymentChallenge>

Step 1 of pay() on its own: probe an endpoint and return its parsed 402 challenge without opening any UI. Takes endpoint (required), method, body and headers; touches no DOM and no wallet, so it also runs on a server, in a CLI, or inside an agent that has no modal at all.

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

const challenge = await discover({ endpoint: '/api/paid/summarize' });
for (const a of challenge.accepts) {
  console.log(a.network, a.amount, a.extra?.name);  // eip155:8453 1000 USDC
}

accepts[] comes back normalized (spec-canonical maxAmountRequired coerced to amount), read from the response body or the base64 payment-required header, whichever carries it. It rejects when the endpoint answers with no readable challenge, including a free 200: pointing it at an unpaid route is an error, never a silent success.

configure(config): config · getConfig(): config

Set global defaults once at startup. See Configuration.

init(): void

Scan the document and bind every [data-x402-endpoint] element. The /global build calls this automatically (and re-scans on DOM mutation); call it yourself only when using the ESM build with declarative buttons.

DOM events (declarative usage)

Bound elements dispatch bubbling CustomEvents:

  • x402:result — detail is the full PayResult.
  • x402:error — detail is { error: string }. (Cancellation does not fire this.)
  • x402:siwx-signed — detail is { address, network }, when re-entry was via SIWX.

data-* attributes (declarative usage)

data-x402-endpoint (required), data-x402-method, data-x402-body (JSON), data-x402-headers (JSON), data-x402-caps (JSON), data-x402-api-origin, data-x402-merchant, data-x402-action.

Configuration

All fields are optional; the defaults reproduce the hosted three.ws modal.

configure({
  // Origin serving the Solana prepare/encode checkout helpers. Only the Solana
  // path uses it; the EVM path needs no backend. null → resolve from the
  // script's own origin; '' → same-origin.
  apiOrigin: 'https://pay.example.com',

  // Footer attribution.
  brand: { label: 'Powered by Acme', href: 'https://acme.com' },

  // ERC-8021 builder-code self-attribution, echoed back only when the 402
  // challenge declares a builder code. null disables the echo.
  builderCode: { wallet: 'acme', service: 'acme_checkout' },

  // Override the on-demand CDN modules (e.g. to self-host under a strict CSP).
  solanaWeb3Url: 'https://esm.sh/@solana/[email protected]?bundle',
  nobleHashesUrl: 'https://esm.sh/@noble/[email protected]/sha3?bundle',
});

Spending caps

Caps are enforced in localStorage, bucketed by rolling UTC hour and day, and survive reloads. Amounts are micro-USD (1_000_000 = $1). A failed payment rolls its reservation back.

await pay({
  endpoint: '/api/paid/x',
  caps: {
    maxPerCall: 1_000_000,    // $1.00 per call
    maxPerHour: 10_000_000,   // $10/hour
    maxPerDay:  50_000_000,   // $50/day
  },
});

Stablecoins (USDC, USDT, DAI) are converted to µUSD exactly. Non-stable assets pass through atomic in the browser (no price feed is fetched to keep the script dependency-free) — enforce those server-side.

The backend

EVM / Base needs no backend. The payer signs an EIP-3009 transferWithAuthorization in their wallet and the modal sends the signed authorization straight to your merchant endpoint as X-PAYMENT. Your x402 server (and its facilitator) verify and settle it.

Solana needs one tiny helper, because building a Solana transfer transaction requires RPC access and the facilitator's fee-payer. The modal expects two actions at {apiOrigin}/api/x402-checkout:

| action | request | response | |---|---|---| | ?action=prepare | { accept, buyer } | { tx_base64 } — an unsigned/partially-signed VersionedTransaction | | ?action=encode | { accept, signed_tx_base64, resource_url, builder_code? } | { x_payment } — the base64 X-PAYMENT value to send to the merchant |

apiOrigin defaults to the origin that served the script, so when you self-host both the script and this helper there is nothing to configure. See docs/BACKEND.md for the full contract and a reference implementation, and examples/ for runnable code.

Install

npm i @three-ws/x402-modal
import { pay, configure } from '@three-ws/x402-modal';   // ESM, no side effects

or skip the install entirely and use the CDN /global build (auto-binds [data-x402-endpoint], exposes window.X402).

Development

npm install          # esbuild is the only devDependency
npm run build        # → dist/x402-modal.mjs (ESM) + dist/x402.global.js (IIFE)
npm test             # node --test, zero extra deps

npm test covers the protocol layer (challenge discovery against a real local HTTP server, amount/network/caps helpers, the SIWX message format) and the config surface. What only a browser can prove (the global build binding data-x402-endpoint, the modal mounting, live discovery, cancellation, script-tag config) runs from the monorepo root against the live demo endpoint:

npm --prefix x402-modal-sdk run build
node scripts/x402-modal-e2e.mjs

It reads a real 402 challenge and never signs or spends anything. examples/index.html is the same demo page, for driving by hand: build, serve this folder, and open it.

Security notes

  • The modal never holds keys. Signing happens in the user's wallet; the signed payload goes to your endpoint.
  • A 429 from the merchant is retried with the same signed payment — safe, because x402 settles only after the work succeeds.
  • Upstream throttle/billing text is never relayed to the buyer verbatim.
  • All endpoint-supplied strings are HTML-escaped before rendering.
  • For the Solana path, the dynamic CDN import can be blocked by a strict Content-Security-Policy; either allow it, repoint it via solanaWeb3Url, or steer users to the dependency-free Base path.

FAQ

Do I need a wallet adapter / WalletConnect? No. Solana uses the injected Phantom provider; EVM uses the injected window.ethereum.

Does the payer pay gas? On EVM, no — EIP-3009 is a gasless signed authorization your facilitator submits. On Solana the facilitator is the fee-payer.

Can I theme it? It ships a self-contained stylesheet with light/dark (prefers-color-scheme) support. Override the .x402-* classes, or set brand for the footer. The header reflects merchant / action.

Framework support? It's framework-agnostic. Import pay() and call it from a React/Vue/Svelte handler, or drop the /global script and use data-* buttons.

Where does this run in production? This is the same modal that powers payments on three.ws; the package is its standalone, configurable home.

License

All rights reserved, see LICENSE. Part of the three.ws platform for building, animating, rigging, and monetizing 3D AI agents.