@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.
@@ -0,0 +1,124 @@
1
+ # Theming
2
+
3
+ The modal ships its own styles so it looks finished out of the box, then gives
4
+ you clean hooks to skin it to your brand. There is no build step and no CSS file
5
+ to import — styles are injected at runtime.
6
+
7
+ For the markup these classes wrap, see the step model in
8
+ [architecture](./architecture.md).
9
+
10
+ ## How styles are injected
11
+
12
+ On first use, the modal injects a single `<style id="x402-styles">` block into
13
+ the document head. It is injected **once**; subsequent opens reuse it.
14
+
15
+ To override it, do one of:
16
+
17
+ 1. Define your own rules with **higher specificity** in a stylesheet loaded
18
+ **after** the module runs (so your rules win on equal specificity, or beat
19
+ the injected ones).
20
+ 2. Use `!important` on the specific declarations you want to force.
21
+ 3. Override the exposed **CSS custom property** (`--x402-z`) where one exists.
22
+
23
+ The modal is **light by default** and includes a built-in dark theme via
24
+ `@media (prefers-color-scheme: dark)` — it follows the OS setting automatically.
25
+
26
+ ## CSS classes
27
+
28
+ | Class | Element / role |
29
+ |------------------------|---------------------------------------------------------------------|
30
+ | `.x402-overlay` | Root overlay. Holds `--x402-z` (z-index, default `2147483600`). |
31
+ | `.x402-modal` | The modal card container. |
32
+ | `.x402-head` | Header region (merchant/action/close). |
33
+ | `.x402-merchant` | Merchant block in the header. |
34
+ | `.x402-name` | Merchant name text. |
35
+ | `.x402-action` | Action label text (from `action`). |
36
+ | `.x402-close` | Close button. |
37
+ | `.x402-price-row` | Row holding price + network. |
38
+ | `.x402-price` | Numeric price. |
39
+ | `.x402-currency` | Currency label (e.g. USDC). |
40
+ | `.x402-network` | Network badge (e.g. Base / Solana). |
41
+ | `.x402-body` | Main content area. |
42
+ | `.x402-step` | A lifecycle step row. Modifiers below. |
43
+ | `.x402-step.x402-active` | The step currently in progress. |
44
+ | `.x402-step.x402-done` | A completed step. |
45
+ | `.x402-step.x402-error`| A step that failed. |
46
+ | `.x402-wallet-btn` | A wallet choice button (Phantom / EVM). |
47
+ | `.x402-pay-btn` | Primary pay / confirm button. |
48
+ | `.x402-pay-secondary` | Secondary action button (e.g. demoted pay under SIWX). |
49
+ | `.x402-error-box` | Error message container. |
50
+ | `.x402-receipt` | Settled-payment receipt block. |
51
+ | `.x402-result` | The paid endpoint's returned result. |
52
+ | `.x402-foot` | Footer (brand attribution + footer note). |
53
+
54
+ The four `.x402-step` rows correspond to the discover / connect / authorize /
55
+ verify lifecycle described in [architecture](./architecture.md).
56
+
57
+ ## Overriding the z-index
58
+
59
+ The overlay z-index is a custom property so you can lower it under your own
60
+ top-layer UI (or raise it) without touching the rest of the stylesheet:
61
+
62
+ ```css
63
+ .x402-overlay {
64
+ --x402-z: 9999;
65
+ }
66
+ ```
67
+
68
+ Default is `2147483600` (just under the 32-bit max) so the modal sits above most
69
+ app chrome.
70
+
71
+ ## Dark mode
72
+
73
+ Dark styling is automatic via `prefers-color-scheme: dark`. To force a single
74
+ theme regardless of OS, override the relevant classes. For example, to force the
75
+ light look everywhere:
76
+
77
+ ```css
78
+ @media (prefers-color-scheme: dark) {
79
+ .x402-modal { background: #ffffff; color: #111111; }
80
+ }
81
+ ```
82
+
83
+ ## Worked example: brand the pay button and modal radius
84
+
85
+ Load this **after** the module so it wins:
86
+
87
+ ```html
88
+ <script type="module" src="https://unpkg.com/@three-ws/x402-payment-modal"></script>
89
+ <style>
90
+ /* Rounder card */
91
+ .x402-modal {
92
+ border-radius: 18px;
93
+ }
94
+
95
+ /* Brand-colored primary button with a hover lift */
96
+ .x402-pay-btn {
97
+ background: #4f46e5; /* indigo */
98
+ color: #fff;
99
+ border: none;
100
+ transition: transform 120ms ease, background 120ms ease;
101
+ }
102
+ .x402-pay-btn:hover {
103
+ background: #4338ca;
104
+ transform: translateY(-1px);
105
+ }
106
+ .x402-pay-btn:active {
107
+ transform: translateY(0);
108
+ }
109
+ .x402-pay-btn:focus-visible {
110
+ outline: 2px solid #818cf8;
111
+ outline-offset: 2px;
112
+ }
113
+
114
+ /* Tone down the secondary action */
115
+ .x402-pay-secondary {
116
+ background: transparent;
117
+ color: #4f46e5;
118
+ }
119
+ </style>
120
+ ```
121
+
122
+ If a rule still loses to the injected stylesheet, raise specificity (e.g.
123
+ `.x402-overlay .x402-pay-btn`) or add `!important` to the individual
124
+ declarations.
@@ -0,0 +1,22 @@
1
+ # Examples
2
+
3
+ Runnable examples for [`@three-ws/x402-payment-modal`](https://www.npmjs.com/package/@three-ws/x402-payment-modal)
4
+ — a zero-dependency, vanilla-JS payment modal for any x402 paid HTTP endpoint.
5
+
6
+ | Example | What it shows | Fastest way to try it |
7
+ | --- | --- | --- |
8
+ | [`plain-html/`](./plain-html) | No build step. Declarative `data-x402-*` buttons, a programmatic `pay()` call, and `x402:result` / `x402:error` event handling — all from a single CDN `<script>`. | Open `plain-html/index.html` in a browser (or serve the folder, e.g. `npx serve plain-html`). |
9
+ | [`react/`](./react) | A reusable `<X402Button>` component that wraps `pay()` with a dynamic import, so it is SSR-safe. | Copy `react/X402Button.jsx` into your app — see [`react/README.md`](./react/README.md). |
10
+ | [`server-express/`](./server-express) | A complete Express server that mounts the Solana checkout router and serves a demo paid endpoint returning a real x402 v2 challenge. | `cd server-express && npm install && npm start`, then open http://localhost:3000 |
11
+
12
+ ## Which do I need?
13
+
14
+ - **EVM payments** sign in the browser (EIP-3009) — the **plain-html** or
15
+ **react** client is all you need.
16
+ - **Solana payments** also require the **server-express** checkout endpoint to
17
+ build and settle the transfer. See
18
+ [`../docs/server-setup.md`](../docs/server-setup.md).
19
+
20
+ > The client examples point at placeholder endpoints
21
+ > (`https://api.example.com/...`). Swap in a real x402-enabled endpoint to take
22
+ > an actual payment.
@@ -0,0 +1,229 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>x402 Payment Modal — Plain HTML demo</title>
7
+
8
+ <!--
9
+ ─────────────────────────────────────────────────────────────────────────
10
+ DEMO ONLY
11
+
12
+ This page shows three ways to use @three-ws/x402-payment-modal with no
13
+ build step at all — just an ES module loaded from a CDN.
14
+
15
+ (a) Declarative: a button annotated with data-x402-* attributes. The
16
+ library auto-binds these on load — zero JavaScript required.
17
+ (b) Programmatic: a button wired in an inline module that imports pay().
18
+ (c) Events: x402:result / x402:error bubble from the clicked element;
19
+ we listen and render the outcome into #outcome below.
20
+
21
+ The endpoint below (https://api.example.com/...) is a PLACEHOLDER. To
22
+ actually take a payment, point data-x402-endpoint / pay({ endpoint }) at a
23
+ REAL x402-enabled HTTP endpoint that responds with HTTP 402 + an x402
24
+ challenge. The modal handles the 402 → connect wallet → sign → settle flow.
25
+
26
+ Self-hosting: instead of the unpkg URL you can serve the module yourself,
27
+ e.g. <script type="module" src="/vendor/x402.min.js" ...>. The data-x402-*
28
+ config attributes work the same way on a self-hosted <script> tag.
29
+ ─────────────────────────────────────────────────────────────────────────
30
+ -->
31
+
32
+ <style>
33
+ :root {
34
+ --bg: #0b0c10;
35
+ --card: #15171e;
36
+ --border: #262a35;
37
+ --text: #e7e9ee;
38
+ --muted: #9aa1b1;
39
+ --accent: #6c8cff;
40
+ --accent-press: #5476f5;
41
+ --ok: #3ddc97;
42
+ --err: #ff6b6b;
43
+ }
44
+ * { box-sizing: border-box; }
45
+ html, body { height: 100%; }
46
+ body {
47
+ margin: 0;
48
+ background: radial-gradient(1200px 600px at 50% -10%, #1a1d27 0%, var(--bg) 60%);
49
+ color: var(--text);
50
+ font: 15px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
51
+ display: grid;
52
+ place-items: center;
53
+ padding: 32px 16px;
54
+ }
55
+ .card {
56
+ width: 100%;
57
+ max-width: 520px;
58
+ background: var(--card);
59
+ border: 1px solid var(--border);
60
+ border-radius: 16px;
61
+ padding: 28px;
62
+ box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45);
63
+ }
64
+ h1 { font-size: 20px; margin: 0 0 4px; letter-spacing: -0.01em; }
65
+ p.lede { margin: 0 0 22px; color: var(--muted); }
66
+ h2 {
67
+ font-size: 12px;
68
+ text-transform: uppercase;
69
+ letter-spacing: 0.08em;
70
+ color: var(--muted);
71
+ margin: 22px 0 8px;
72
+ }
73
+ .row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
74
+ button {
75
+ appearance: none;
76
+ border: 0;
77
+ border-radius: 10px;
78
+ padding: 11px 18px;
79
+ font: inherit;
80
+ font-weight: 600;
81
+ color: #fff;
82
+ background: var(--accent);
83
+ cursor: pointer;
84
+ transition: transform 0.08s ease, background 0.15s ease, box-shadow 0.15s ease;
85
+ }
86
+ button:hover { background: var(--accent-press); }
87
+ button:active { transform: translateY(1px); }
88
+ button:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
89
+ button[disabled] { opacity: 0.55; cursor: progress; }
90
+ .hint { color: var(--muted); font-size: 13px; }
91
+ code {
92
+ background: #0c0e14;
93
+ border: 1px solid var(--border);
94
+ border-radius: 6px;
95
+ padding: 1px 6px;
96
+ font-size: 12px;
97
+ }
98
+ #outcome {
99
+ margin-top: 22px;
100
+ min-height: 44px;
101
+ border: 1px dashed var(--border);
102
+ border-radius: 12px;
103
+ padding: 14px 16px;
104
+ color: var(--muted);
105
+ font-size: 13px;
106
+ white-space: pre-wrap;
107
+ word-break: break-word;
108
+ transition: border-color 0.2s ease;
109
+ }
110
+ #outcome.ok { border-color: var(--ok); color: var(--ok); }
111
+ #outcome.err { border-color: var(--err); color: var(--err); }
112
+ .footer { margin-top: 20px; color: var(--muted); font-size: 12px; }
113
+ a { color: var(--accent); }
114
+ </style>
115
+
116
+ <!--
117
+ (a) Declarative usage.
118
+
119
+ Loading the module runs init() automatically: any element carrying a
120
+ data-x402-endpoint attribute becomes a pay trigger. Config that applies to
121
+ the whole page (brand, where the Solana checkout endpoint lives) rides on
122
+ THIS script tag as data-x402-* attributes.
123
+ -->
124
+ <script
125
+ type="module"
126
+ src="https://unpkg.com/@three-ws/x402-payment-modal"
127
+ data-x402-brand-name="Acme Summaries"
128
+ data-x402-brand-url="https://example.com"
129
+ data-x402-checkout-origin="https://example.com"
130
+ data-x402-footer-note="Payments settle in USDC. Powered by x402."
131
+ ></script>
132
+ </head>
133
+
134
+ <body>
135
+ <main class="card">
136
+ <h1>x402 Payment Modal</h1>
137
+ <p class="lede">Pay any x402 endpoint in one click — no wallet integration code.</p>
138
+
139
+ <!-- (a) Zero-JS declarative button. The attributes describe the request. -->
140
+ <h2>Declarative (no JavaScript)</h2>
141
+ <div class="row">
142
+ <button
143
+ id="declarative-pay"
144
+ data-x402-endpoint="https://api.example.com/paid/summarize"
145
+ data-x402-method="POST"
146
+ data-x402-body='{"url":"https://en.wikipedia.org/wiki/x402"}'
147
+ data-x402-merchant="Acme Summaries"
148
+ data-x402-action="Summarize article"
149
+ >
150
+ Summarize for 0.01 USDC
151
+ </button>
152
+ <span class="hint">Driven entirely by <code>data-x402-*</code> attributes.</span>
153
+ </div>
154
+
155
+ <!-- (b) Programmatic button — wired by the inline module below. -->
156
+ <h2>Programmatic (inline module)</h2>
157
+ <div class="row">
158
+ <button id="programmatic-pay">Translate for 0.01 USDC</button>
159
+ <span class="hint">Calls <code>pay()</code> from a <code>&lt;script type="module"&gt;</code>.</span>
160
+ </div>
161
+
162
+ <!-- (c) Outcome surface — populated by the x402:result / x402:error listeners. -->
163
+ <h2>Outcome</h2>
164
+ <div id="outcome" role="status" aria-live="polite">No payment attempted yet.</div>
165
+
166
+ <p class="footer">
167
+ Endpoints above are placeholders — swap in a real x402 endpoint to take a payment.
168
+ You can also self-host the module instead of unpkg.
169
+ </p>
170
+ </main>
171
+
172
+ <script type="module">
173
+ // (b) Programmatic usage: import pay() straight from the CDN module.
174
+ // In a self-hosted setup, import from your own URL instead, e.g.
175
+ // import { pay } from '/vendor/x402.min.js';
176
+ import { pay } from 'https://unpkg.com/@three-ws/x402-payment-modal';
177
+
178
+ const button = document.getElementById('programmatic-pay');
179
+
180
+ button.addEventListener('click', async () => {
181
+ button.disabled = true;
182
+ const original = button.textContent;
183
+ button.textContent = 'Processing…';
184
+ try {
185
+ const result = await pay({
186
+ endpoint: 'https://api.example.com/paid/translate',
187
+ method: 'POST',
188
+ body: { text: 'Hello, world', to: 'es' },
189
+ merchant: 'Acme Translate',
190
+ action: 'Translate text',
191
+ });
192
+ // result = { ok, result, payment?, siwx?, response }
193
+ console.log('Payment result:', result);
194
+ } catch (err) {
195
+ // The modal rejects with err.code === 'cancelled' when dismissed.
196
+ if (err && err.code === 'cancelled') {
197
+ console.log('User cancelled the payment.');
198
+ } else {
199
+ console.error('Payment failed:', err);
200
+ }
201
+ } finally {
202
+ button.disabled = false;
203
+ button.textContent = original;
204
+ }
205
+ });
206
+
207
+ // (c) Event listeners. x402:result and x402:error bubble from the element
208
+ // that triggered the payment, so a single document-level listener covers
209
+ // both the declarative and programmatic buttons.
210
+ const outcome = document.getElementById('outcome');
211
+
212
+ document.addEventListener('x402:result', (event) => {
213
+ const detail = event.detail; // PayResult: { ok, result, payment?, siwx?, response }
214
+ outcome.className = 'ok';
215
+ const payer = detail.payment?.payer ? `\nPayer: ${detail.payment.payer}` : '';
216
+ const tx = detail.payment?.transaction ? `\nTx: ${detail.payment.transaction}` : '';
217
+ outcome.textContent = `Paid ✓ (${detail.payment?.network ?? 'settled'})${payer}${tx}`;
218
+ });
219
+
220
+ // detail.error is a string. Note: dismissing the modal does NOT fire this
221
+ // event — the auto-binder swallows the cancellation — so anything here is a
222
+ // genuine failure.
223
+ document.addEventListener('x402:error', (event) => {
224
+ outcome.className = 'err';
225
+ outcome.textContent = `Payment failed: ${event.detail?.error ?? 'unknown error'}`;
226
+ });
227
+ </script>
228
+ </body>
229
+ </html>
@@ -0,0 +1,69 @@
1
+ # React example — `<X402Button>`
2
+
3
+ A drop-in React component that wraps `pay()` from
4
+ [`@three-ws/x402-payment-modal`](https://www.npmjs.com/package/@three-ws/x402-payment-modal).
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ npm i @three-ws/x402-payment-modal
10
+ ```
11
+
12
+ ## Use it
13
+
14
+ Copy [`X402Button.jsx`](./X402Button.jsx) into your project, then:
15
+
16
+ ```jsx
17
+ import X402Button from './X402Button';
18
+
19
+ export default function Demo() {
20
+ return (
21
+ <X402Button
22
+ endpoint="https://api.example.com/paid/summarize"
23
+ method="POST"
24
+ body={{ url: 'https://en.wikipedia.org/wiki/x402' }}
25
+ merchant="Acme Summaries"
26
+ action="Summarize article"
27
+ label="Summarize for 0.01 USDC"
28
+ onResult={(r) => console.log('paid', r.payment)}
29
+ onError={(e) => console.error('payment failed', e)}
30
+ />
31
+ );
32
+ }
33
+ ```
34
+
35
+ ### Props
36
+
37
+ | Prop | Type | Notes |
38
+ | ---------- | ---------- | ---------------------------------------------------------- |
39
+ | `endpoint` | `string` | Required. The x402-enabled HTTP endpoint to call. |
40
+ | `method` | `string` | HTTP method (default `GET`). |
41
+ | `body` | `object` | Request body (sent as JSON). |
42
+ | `merchant` | `string` | Display name shown in the modal. |
43
+ | `action` | `string` | Short description of what the user is paying for. |
44
+ | `label` | `string` | Button text (default `Pay`). `children` overrides it. |
45
+ | `onResult` | `function` | Called with the `PayResult` on success. |
46
+ | `onError` | `function` | Called on failure. **Not** called when the user cancels. |
47
+
48
+ `onResult` receives `{ ok, result, payment?, siwx?, response }`.
49
+
50
+ ## SSR safety
51
+
52
+ The package is browser-only (it renders a modal and connects to a wallet), so
53
+ `X402Button` imports it lazily **inside the click handler**:
54
+
55
+ ```js
56
+ const { pay } = await import('@three-ws/x402-payment-modal');
57
+ ```
58
+
59
+ Nothing from the package runs during render or on the server, so the component
60
+ is safe in Next.js, Remix, and other SSR frameworks without `dynamic`/`ssr:false`
61
+ wrappers.
62
+
63
+ ## Solana payments need a server
64
+
65
+ EVM payments sign in the browser (EIP-3009) and need no backend. **Solana
66
+ payments require a checkout endpoint** that builds and settles the transfer.
67
+ Stand one up before going live — see
68
+ [`../../docs/server-setup.md`](../../docs/server-setup.md) and the runnable
69
+ [`examples/server-express`](../server-express) server.
@@ -0,0 +1,84 @@
1
+ /**
2
+ * <X402Button> — a thin React wrapper around @three-ws/x402-payment-modal.
3
+ *
4
+ * The package is a browser-only ES module (it renders a modal and talks to a
5
+ * wallet), so we dynamically import it INSIDE the click handler. That keeps the
6
+ * component SSR-safe: nothing from the package is touched during render or on
7
+ * the server.
8
+ *
9
+ * Usage:
10
+ *
11
+ * import X402Button from './X402Button';
12
+ *
13
+ * <X402Button
14
+ * endpoint="https://api.example.com/paid/summarize"
15
+ * method="POST"
16
+ * body={{ url: 'https://en.wikipedia.org/wiki/x402' }}
17
+ * merchant="Acme Summaries"
18
+ * action="Summarize article"
19
+ * label="Summarize for 0.01 USDC"
20
+ * onResult={(r) => console.log('paid', r)}
21
+ * onError={(e) => console.error(e)}
22
+ * />
23
+ *
24
+ * For Solana payments your app must also run the server checkout endpoint
25
+ * (see ../../docs/server-setup.md and examples/server-express). EVM payments
26
+ * sign in-browser and need no server.
27
+ */
28
+
29
+ import { useCallback, useState } from 'react';
30
+
31
+ export default function X402Button({
32
+ endpoint,
33
+ method = 'GET',
34
+ body,
35
+ merchant,
36
+ action,
37
+ label = 'Pay',
38
+ onResult,
39
+ onError,
40
+ children,
41
+ ...rest
42
+ }) {
43
+ const [processing, setProcessing] = useState(false);
44
+
45
+ const handleClick = useCallback(async () => {
46
+ if (processing) return;
47
+ setProcessing(true);
48
+
49
+ try {
50
+ // Dynamic import keeps the browser-only module out of the SSR bundle.
51
+ const { pay } = await import('@three-ws/x402-payment-modal');
52
+
53
+ const result = await pay({
54
+ endpoint,
55
+ method,
56
+ body,
57
+ merchant,
58
+ action,
59
+ });
60
+
61
+ // result = { ok, result, payment?, siwx?, response }
62
+ onResult?.(result);
63
+ } catch (err) {
64
+ // The modal rejects with err.code === 'cancelled' when the user dismisses
65
+ // it. That is not an error — stay silent so we don't flash a failure.
66
+ if (err && err.code === 'cancelled') return;
67
+ onError?.(err);
68
+ } finally {
69
+ setProcessing(false);
70
+ }
71
+ }, [processing, endpoint, method, body, merchant, action, onResult, onError]);
72
+
73
+ return (
74
+ <button
75
+ type="button"
76
+ onClick={handleClick}
77
+ disabled={processing}
78
+ aria-busy={processing}
79
+ {...rest}
80
+ >
81
+ {processing ? 'Processing…' : children ?? label}
82
+ </button>
83
+ );
84
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "x402-checkout-example",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "description": "Runnable Express server that mounts the @three-ws/x402-payment-modal Solana checkout router and serves a demo paid endpoint.",
6
+ "type": "module",
7
+ "scripts": {
8
+ "start": "node server.js"
9
+ },
10
+ "dependencies": {
11
+ "@solana/spl-token": "*",
12
+ "@solana/web3.js": "*",
13
+ "@three-ws/x402-payment-modal": "*",
14
+ "express": "*"
15
+ }
16
+ }
@@ -0,0 +1,89 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>x402 Express checkout demo</title>
7
+ <style>
8
+ body {
9
+ margin: 0;
10
+ min-height: 100vh;
11
+ display: grid;
12
+ place-items: center;
13
+ background: #0b0c10;
14
+ color: #e7e9ee;
15
+ font: 15px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
16
+ }
17
+ .card {
18
+ background: #15171e;
19
+ border: 1px solid #262a35;
20
+ border-radius: 16px;
21
+ padding: 28px;
22
+ max-width: 460px;
23
+ text-align: center;
24
+ box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45);
25
+ }
26
+ h1 { font-size: 20px; margin: 0 0 6px; }
27
+ p { color: #9aa1b1; margin: 0 0 20px; }
28
+ button {
29
+ border: 0;
30
+ border-radius: 10px;
31
+ padding: 11px 18px;
32
+ font: inherit;
33
+ font-weight: 600;
34
+ color: #fff;
35
+ background: #6c8cff;
36
+ cursor: pointer;
37
+ }
38
+ button:hover { background: #5476f5; }
39
+ button[disabled] { opacity: 0.55; cursor: progress; }
40
+ #out {
41
+ margin-top: 18px;
42
+ min-height: 22px;
43
+ font-size: 13px;
44
+ color: #9aa1b1;
45
+ white-space: pre-wrap;
46
+ word-break: break-word;
47
+ }
48
+ </style>
49
+ </head>
50
+ <body>
51
+ <main class="card">
52
+ <h1>x402 Express checkout demo</h1>
53
+ <p>Click to pay the local <code>/api/paid/hello</code> endpoint in USDC.</p>
54
+ <button id="pay">Say hello for 0.01 USDC</button>
55
+ <div id="out" role="status" aria-live="polite"></div>
56
+ </main>
57
+
58
+ <script type="module">
59
+ // Loaded from the CDN for the demo; bundle it locally for production.
60
+ import { pay, configure } from 'https://unpkg.com/@three-ws/x402-payment-modal';
61
+
62
+ // Tell the modal where this server's Solana checkout endpoint lives.
63
+ configure({
64
+ checkoutOrigin: window.location.origin,
65
+ checkoutPath: '/api/x402-checkout',
66
+ brand: { name: 'x402 Express demo', url: window.location.origin },
67
+ });
68
+
69
+ const out = document.getElementById('out');
70
+ document.getElementById('pay').addEventListener('click', async (e) => {
71
+ e.target.disabled = true;
72
+ try {
73
+ const result = await pay({
74
+ endpoint: '/api/paid/hello',
75
+ merchant: 'x402 Express demo',
76
+ action: 'Say hello',
77
+ });
78
+ out.textContent = `Paid ✓ ${JSON.stringify(result.result ?? result.ok)}`;
79
+ } catch (err) {
80
+ out.textContent = err?.code === 'cancelled'
81
+ ? 'Cancelled.'
82
+ : `Failed: ${err?.message ?? err}`;
83
+ } finally {
84
+ e.target.disabled = false;
85
+ }
86
+ });
87
+ </script>
88
+ </body>
89
+ </html>