@three-ws/x402-payment-modal 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/TUTORIAL.md ADDED
@@ -0,0 +1,188 @@
1
+ # Tutorial: ship a paid endpoint with x402
2
+
3
+ This walks you from an empty folder to a working page where a user pays USDC to
4
+ call your API — using `@three-ws/x402-payment-modal` for the front end. It takes
5
+ about 15 minutes.
6
+
7
+ By the end you'll have:
8
+
9
+ 1. A paid HTTP endpoint that answers `402` until it sees a valid payment.
10
+ 2. A button that opens the payment modal and runs the call.
11
+ 3. The Solana checkout endpoint the modal needs (skip this if you only take EVM).
12
+
13
+ > **Prerequisites:** Node 18+, and a browser with [Phantom](https://phantom.app)
14
+ > (Solana) and/or an EVM wallet like MetaMask. You'll need a little USDC on Base
15
+ > or Solana to test a real payment end-to-end.
16
+
17
+ ---
18
+
19
+ ## Part 1 — The front end (5 minutes)
20
+
21
+ Create `index.html`:
22
+
23
+ ```html
24
+ <!doctype html>
25
+ <html>
26
+ <head>
27
+ <meta charset="utf-8" />
28
+ <title>Paid endpoint demo</title>
29
+ </head>
30
+ <body>
31
+ <h1>Summarize anything — $0.01</h1>
32
+
33
+ <button
34
+ id="summarize"
35
+ data-x402-endpoint="https://api.example.com/paid/summarize"
36
+ data-x402-method="POST"
37
+ data-x402-body='{"text":"The quick brown fox..."}'
38
+ data-x402-merchant="Acme AI"
39
+ data-x402-action="Summarize">
40
+ Pay &amp; Summarize
41
+ </button>
42
+
43
+ <pre id="out"></pre>
44
+
45
+ <script type="module" src="https://unpkg.com/@three-ws/x402-payment-modal"></script>
46
+ <script>
47
+ const out = document.getElementById('out');
48
+ const btn = document.getElementById('summarize');
49
+ btn.addEventListener('x402:result', (e) => {
50
+ out.textContent = JSON.stringify(e.detail.result, null, 2);
51
+ });
52
+ btn.addEventListener('x402:error', (e) => {
53
+ out.textContent = 'Error: ' + e.detail.error;
54
+ });
55
+ </script>
56
+ </body>
57
+ </html>
58
+ ```
59
+
60
+ Open it in a browser and click the button. The modal appears, reads the price
61
+ from the endpoint's `402` response, and walks the user through paying. (Pointing
62
+ at `api.example.com` won't actually settle — we build a real endpoint next.)
63
+
64
+ That's the entire client integration. Everything below is the *server* you're
65
+ charging for.
66
+
67
+ ---
68
+
69
+ ## Part 2 — A paid endpoint (5 minutes)
70
+
71
+ A paid endpoint follows the x402 contract:
72
+
73
+ 1. **No `X-PAYMENT` header →** respond `402` with a challenge describing what you
74
+ accept.
75
+ 2. **Valid `X-PAYMENT` header →** verify + settle it with an x402 facilitator,
76
+ do the work, and return `200` with an `X-PAYMENT-RESPONSE` header.
77
+
78
+ Here's the challenge half (the part the modal reads). Create `server.js`:
79
+
80
+ ```js
81
+ import express from 'express';
82
+
83
+ const app = express();
84
+ app.use(express.json());
85
+
86
+ // Atomic USDC is 6-decimal: 10000 = $0.01.
87
+ const PRICE_ATOMIC = '10000';
88
+
89
+ function challenge(req) {
90
+ const url = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
91
+ return {
92
+ x402Version: 2,
93
+ error: 'X-PAYMENT header is required',
94
+ resource: { url, description: 'Summarize text', mimeType: 'application/json' },
95
+ accepts: [
96
+ {
97
+ scheme: 'exact',
98
+ network: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', // Solana mainnet
99
+ amount: PRICE_ATOMIC,
100
+ asset: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC mint
101
+ payTo: 'REPLACE_WITH_YOUR_SOLANA_ADDRESS',
102
+ maxTimeoutSeconds: 60,
103
+ extra: {
104
+ name: 'USDC',
105
+ decimals: 6,
106
+ // The facilitator sponsor that pays the SOL network fee so the buyer
107
+ // needs only USDC. Provided by your x402 facilitator.
108
+ feePayer: 'REPLACE_WITH_FACILITATOR_FEE_PAYER',
109
+ },
110
+ },
111
+ ],
112
+ };
113
+ }
114
+
115
+ app.post('/paid/summarize', (req, res) => {
116
+ const xPayment = req.get('X-PAYMENT');
117
+ if (!xPayment) {
118
+ return res.status(402).json(challenge(req));
119
+ }
120
+
121
+ // 1. Verify + settle `xPayment` with your x402 facilitator here.
122
+ // (See the x402 spec — facilitator /verify and /settle.)
123
+ // 2. Do the actual work:
124
+ const summary = `Summary of: ${String(req.body?.text || '').slice(0, 40)}…`;
125
+
126
+ // 3. Return the result. Attach the base64 settlement as X-PAYMENT-RESPONSE.
127
+ res.json({ summary });
128
+ });
129
+
130
+ app.listen(3000, () => console.log('http://localhost:3000'));
131
+ ```
132
+
133
+ > Verifying and settling the `X-PAYMENT` payload against a facilitator is the
134
+ > server's responsibility and is beyond this modal's scope — see the
135
+ > [x402 spec](https://x402.org). The modal's job is everything on the client.
136
+
137
+ Point your button's `data-x402-endpoint` at `http://localhost:3000/paid/summarize`
138
+ and the modal will read your real price.
139
+
140
+ ---
141
+
142
+ ## Part 3 — The Solana checkout endpoint (5 minutes)
143
+
144
+ EVM payments are signed entirely in the browser, so if you only accept Base/EVM
145
+ USDC you can **stop here**. For Solana, Phantom can sign but not *build*
146
+ transactions, so the modal calls a small endpoint to build the transfer and wrap
147
+ the signed tx. The package ships it — mount it in the same server:
148
+
149
+ ```bash
150
+ npm install @solana/web3.js @solana/spl-token
151
+ ```
152
+
153
+ ```js
154
+ import { x402CheckoutRouter } from '@three-ws/x402-payment-modal/server/express';
155
+
156
+ app.use(
157
+ '/api/x402-checkout',
158
+ x402CheckoutRouter({ rpcUrl: process.env.SOLANA_RPC_URL }),
159
+ );
160
+ ```
161
+
162
+ The modal automatically POSTs to `/api/x402-checkout?action=prepare` and
163
+ `?action=encode` on the same origin as the script. If your checkout lives on a
164
+ different origin, tell the modal:
165
+
166
+ ```js
167
+ import { configure } from '@three-ws/x402-payment-modal';
168
+ configure({ checkoutOrigin: 'https://pay.acme.com' });
169
+ ```
170
+
171
+ Set a reliable RPC (a public endpoint is rate-limited):
172
+
173
+ ```bash
174
+ SOLANA_RPC_URL="https://your-rpc-provider/..." node server.js
175
+ ```
176
+
177
+ That's it — a full paid endpoint with a polished checkout.
178
+
179
+ ---
180
+
181
+ ## Where to go next
182
+
183
+ - **Brand the modal** → [docs/theming.md](docs/theming.md)
184
+ - **Let repeat buyers skip paying** → [docs/siwx.md](docs/siwx.md)
185
+ - **Cap how much a user can spend** → [docs/spending-caps.md](docs/spending-caps.md)
186
+ - **React / framework usage** → [examples/react/](examples/react/)
187
+ - **A runnable server** → [examples/server-express/](examples/server-express/)
188
+ - **The full lifecycle** → [docs/architecture.md](docs/architecture.md)
@@ -0,0 +1,141 @@
1
+ // Type definitions for @three-ws/x402-payment-modal
2
+
3
+ /** Client-side spending caps, enforced in localStorage before each payment. */
4
+ export interface SpendingCaps {
5
+ /** Max atomic micro-USD per single call. */
6
+ maxPerCall?: string | number;
7
+ /** Max atomic micro-USD per rolling UTC hour. */
8
+ maxPerHour?: string | number;
9
+ /** Max atomic micro-USD per rolling UTC day. */
10
+ maxPerDay?: string | number;
11
+ }
12
+
13
+ /** Options for {@link pay}. */
14
+ export interface PayOptions {
15
+ /** URL of the paid (x402) endpoint. Required. */
16
+ endpoint: string;
17
+ /** HTTP method to call the endpoint with. Defaults to GET, or POST if `body` is set. */
18
+ method?: string;
19
+ /** Request body. Objects are JSON-stringified; strings are sent as-is. */
20
+ body?: unknown;
21
+ /** Extra request headers merged into the paid call. */
22
+ headers?: Record<string, string>;
23
+ /** Merchant name shown in the modal header. */
24
+ merchant?: string;
25
+ /** Action label shown in the modal header. */
26
+ action?: string;
27
+ /** Skip the wallet picker and open the wallet directly when exactly one supported wallet is detected. */
28
+ autoConnect?: boolean;
29
+ /** Client-side spending caps (stablecoin assets only in the browser). */
30
+ caps?: SpendingCaps;
31
+ }
32
+
33
+ /** Settlement details returned by the facilitator on a paid call. */
34
+ export interface PaymentReceipt {
35
+ network?: string;
36
+ transaction?: string;
37
+ payer?: string;
38
+ }
39
+
40
+ /** SIWX re-entry details when the wallet signed in instead of paying. */
41
+ export interface SiwxReceipt {
42
+ address: string;
43
+ network: string | number;
44
+ }
45
+
46
+ /** Resolved value of {@link pay}. */
47
+ export interface PayResult {
48
+ ok: true;
49
+ /** Parsed JSON (or text) the paid endpoint returned. */
50
+ result: unknown;
51
+ /** Present on a paid call. */
52
+ payment?: PaymentReceipt;
53
+ /** Present when re-entry happened via SIWX instead of a payment. */
54
+ siwx?: SiwxReceipt;
55
+ response: {
56
+ status: number;
57
+ headers: Record<string, string>;
58
+ };
59
+ }
60
+
61
+ /** Branding shown in the modal footer. */
62
+ export interface BrandConfig {
63
+ name?: string;
64
+ url?: string;
65
+ }
66
+
67
+ /** ERC-8021 builder-code self-attribution echoed when the 402 challenge declares one. */
68
+ export interface BuilderCodeConfig {
69
+ wallet?: string;
70
+ service?: string;
71
+ }
72
+
73
+ /** CDN URLs for the crypto helpers loaded on demand. */
74
+ export interface EsmConfig {
75
+ solanaWeb3?: string;
76
+ nobleHashesSha3?: string;
77
+ }
78
+
79
+ /** Host configuration. See {@link configure}. */
80
+ export interface X402Config {
81
+ checkoutOrigin?: string | null;
82
+ checkoutPath?: string;
83
+ brand?: BrandConfig;
84
+ footerNote?: string;
85
+ builderCode?: BuilderCodeConfig;
86
+ esm?: EsmConfig;
87
+ }
88
+
89
+ /**
90
+ * Override host configuration (Solana checkout origin, branding, builder-code,
91
+ * esm.sh CDN URLs). Shallow-merges nested objects. Call before the first `pay()`.
92
+ * Returns the resolved config.
93
+ */
94
+ export function configure(opts?: X402Config): Required<X402Config>;
95
+
96
+ /**
97
+ * Open the payment modal for an x402 endpoint and resolve when the user completes
98
+ * (or rejects with `{ code: 'cancelled' }` if dismissed).
99
+ */
100
+ export function pay(opts: PayOptions): Promise<PayResult>;
101
+
102
+ /** Bind all `[data-x402-endpoint]` elements on the page (called automatically on load). */
103
+ export function init(): void;
104
+
105
+ /** Library version. */
106
+ export const version: string;
107
+
108
+ /** Solana USDC mint (mainnet). */
109
+ export const USDC_MINT_SOLANA: string;
110
+ /** $THREE — the three.ws utility token mint. Recognized by the modal so a 402
111
+ * `accept` using it renders as THREE without merchant-supplied metadata. */
112
+ export const THREE_MINT: string;
113
+
114
+ export interface KnownSolanaToken {
115
+ symbol: string;
116
+ name: string;
117
+ decimals: number;
118
+ stable?: boolean;
119
+ accent?: string;
120
+ glyph?: string;
121
+ }
122
+
123
+ /** Well-known Solana settlement assets, keyed by mint address. */
124
+ export const KNOWN_SOLANA_TOKENS: Readonly<Record<string, KnownSolanaToken>>;
125
+
126
+ /** Global exposed for non-module / inline-script usage. */
127
+ declare global {
128
+ interface Window {
129
+ X402?: {
130
+ pay: typeof pay;
131
+ init: typeof init;
132
+ configure: typeof configure;
133
+ version: string;
134
+ tokens: {
135
+ USDC_MINT_SOLANA: string;
136
+ THREE_MINT: string;
137
+ KNOWN_SOLANA_TOKENS: Readonly<Record<string, KnownSolanaToken>>;
138
+ };
139
+ };
140
+ }
141
+ }