@three-ws/x402-modal 0.2.0 → 0.2.1

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.
@@ -6,8 +6,13 @@ Three ways to configure, in increasing specificity (later wins):
6
6
  2. **`configure({ … })`** — global defaults, set once at startup.
7
7
  3. **`pay({ … })` options** — per-call overrides.
8
8
 
9
- All defaults reproduce the hosted three.ws modal, so an un-configured drop-in
10
- behaves exactly like `https://three.ws/x402.js`.
9
+ Defaults are vendor-neutral: an un-configured drop-in shows no footer
10
+ attribution and echoes no builder code. Set `brand` and `builderCode` to opt in.
11
+
12
+ > The public API (`pay`, `configure`, `getConfig`, `init`, `bindElement`,
13
+ > `readOptsFrom`, `version`, `CheckoutModal`) is documented in the
14
+ > [README API reference](../README.md#api-reference). The `/global` CDN build
15
+ > additionally exposes `window.X402 = { pay, init, configure, version }`.
11
16
 
12
17
  ---
13
18
 
@@ -22,14 +27,14 @@ getConfig(); // → fully-resolved snapshot
22
27
  | field | type | default | purpose |
23
28
  |---|---|---|---|
24
29
  | `apiOrigin` | `string \| null` | `null` → script origin | Origin of the Solana `prepare`/`encode` helper. `''` = same-origin. Ignored by the EVM path. |
25
- | `brand` | `{ label?, href? }` | `Powered by three.ws` `https://three.ws` | Footer attribution. Merge-updated (set only `label` and `href` survives). |
26
- | `builderCode` | `{ wallet?, service? } \| null` | `{ wallet: '3d_agent', service: '3d_agent_modal' }` | ERC-8021 self-attribution echoed when the `402` declares a builder code. `null` disables the echo. Codes must match `^[a-z0-9_]{1,32}$`. |
30
+ | `brand` | `{ label?, href? } \| null` | `null` (hidden) | Footer attribution. `null` hides the footer link; set `{ label, href? }` to show it. Merge-updated (set only `label` and `href` survives). |
31
+ | `builderCode` | `{ wallet?, service? } \| null` | `null` (no echo) | ERC-8021 self-attribution echoed when the `402` declares a builder code. `null` disables the echo. Codes must match `^[a-z0-9_]{1,32}$`. |
27
32
  | `solanaWeb3Url` | `string` | `esm.sh/@solana/web3.js@1.95.3` | CDN module dynamic-imported on the Solana path. Repoint to self-host under a strict CSP. |
28
33
  | `nobleHashesUrl` | `string` | `esm.sh/@noble/hashes@1.4.0/sha3` | CDN keccak module, used only for EVM SIWX sign-in. |
29
34
 
30
35
  `configure()` merges: `configure({ brand: { label: 'X' } })` keeps the existing
31
36
  `href`. Pass `apiOrigin: null` to reset to script-origin resolution; pass
32
- `builderCode: null` to switch the echo off.
37
+ `brand: null` to hide the footer, or `builderCode: null` to switch the echo off.
33
38
 
34
39
  ---
35
40
 
@@ -103,4 +108,13 @@ with full `prefers-color-scheme` light/dark support and a `prefers-reduced-motio
103
108
  ```
104
109
 
105
110
  The footer text/link is set by `brand`; the two header lines by
106
- `merchant` / `action`.
111
+ `merchant` / `action`. The full list of `.x402-*` hooks and a dark-mode note are
112
+ in the [README theming section](../README.md#theming--styling-hooks).
113
+
114
+ ---
115
+
116
+ ## See also
117
+
118
+ - [`EXAMPLES.md`](./EXAMPLES.md) — runnable recipes per framework.
119
+ - [`BACKEND.md`](./BACKEND.md) — the Solana `prepare`/`encode` helper contract.
120
+ - [`PROTOCOL.md`](./PROTOCOL.md) — the four-step `402 → sign → settle` flow.
@@ -0,0 +1,279 @@
1
+ # Examples
2
+
3
+ Runnable recipes for `@three-ws/x402-modal`. Every snippet below is complete and
4
+ copy-pasteable. All you need to supply is your own x402-protected endpoint (one
5
+ that answers with `402 Payment Required` + an `accepts[]` array — see
6
+ [`PROTOCOL.md`](./PROTOCOL.md)).
7
+
8
+ For two fully-wired files you can serve and click, see
9
+ [`../examples/index.html`](../examples/index.html) (the demo page) and
10
+ [`../examples/server.mjs`](../examples/server.mjs) (the Solana backend helper).
11
+
12
+ ---
13
+
14
+ ## 1 — Declarative button (zero JS, CDN only)
15
+
16
+ The smallest integration. The `/global` script binds the button automatically.
17
+
18
+ ```html
19
+ <!doctype html>
20
+ <meta charset="utf-8" />
21
+
22
+ <button
23
+ data-x402-endpoint="https://api.example.com/paid/summarize"
24
+ data-x402-method="POST"
25
+ data-x402-body='{"text":"hello world"}'
26
+ data-x402-merchant="Acme"
27
+ data-x402-action="Summarize">
28
+ Pay &amp; summarize
29
+ </button>
30
+ <pre id="out"></pre>
31
+
32
+ <script type="module" src="https://unpkg.com/@three-ws/x402-modal/global"></script>
33
+ <script type="module">
34
+ const btn = document.querySelector('button');
35
+ btn.addEventListener('x402:result', (e) => {
36
+ document.getElementById('out').textContent =
37
+ JSON.stringify(e.detail.result, null, 2);
38
+ });
39
+ btn.addEventListener('x402:error', (e) => {
40
+ document.getElementById('out').textContent = 'Error: ' + e.detail.error;
41
+ });
42
+ </script>
43
+ ```
44
+
45
+ ---
46
+
47
+ ## 2 — Programmatic checkout (`pay()`)
48
+
49
+ Drive the flow from your own handler and await the result.
50
+
51
+ ```js
52
+ import { pay } from '@three-ws/x402-modal';
53
+
54
+ async function buy() {
55
+ try {
56
+ const out = await pay({
57
+ endpoint: '/api/paid/summarize',
58
+ method: 'POST',
59
+ body: { text: 'hello world' },
60
+ merchant: 'Acme',
61
+ action: 'Summarize',
62
+ });
63
+ console.log('result:', out.result);
64
+ console.log('payment:', out.payment); // { network, payer, transaction }
65
+ } catch (err) {
66
+ if (err.code === 'cancelled') return; // user closed the modal
67
+ console.error('payment failed:', err.message);
68
+ }
69
+ }
70
+ ```
71
+
72
+ ---
73
+
74
+ ## 3 — Content paywall
75
+
76
+ Unlock content after a single micropayment. CDN-only, no install.
77
+
78
+ ```html
79
+ <button id="buy">Unlock article — $0.05</button>
80
+ <article id="content" hidden></article>
81
+
82
+ <script type="module" src="https://unpkg.com/@three-ws/x402-modal/global"></script>
83
+ <script>
84
+ document.getElementById('buy').addEventListener('click', async () => {
85
+ try {
86
+ const out = await window.X402.pay({
87
+ endpoint: '/api/article/42',
88
+ merchant: 'The Daily',
89
+ action: 'Unlock article',
90
+ });
91
+ const el = document.getElementById('content');
92
+ el.textContent = out.result.body;
93
+ el.hidden = false;
94
+ document.getElementById('buy').remove();
95
+ } catch (err) {
96
+ if (err.code !== 'cancelled') alert(err.message);
97
+ }
98
+ });
99
+ </script>
100
+ ```
101
+
102
+ The endpoint should return `402` for the first request and the article body once
103
+ the `X-PAYMENT` settles. SIWX re-entry (see below) lets a returning buyer skip
104
+ re-paying.
105
+
106
+ ---
107
+
108
+ ## 4 — React
109
+
110
+ `pay()` is a plain promise — no provider, no context.
111
+
112
+ ```jsx
113
+ import { useState, useCallback } from 'react';
114
+ import { pay } from '@three-ws/x402-modal';
115
+
116
+ export function PayButton({ endpoint }) {
117
+ const [out, setOut] = useState(null);
118
+ const [error, setError] = useState(null);
119
+
120
+ const onPay = useCallback(async () => {
121
+ setError(null);
122
+ try {
123
+ const res = await pay({ endpoint, merchant: 'Acme', action: 'Run' });
124
+ setOut(res.result);
125
+ } catch (err) {
126
+ if (err.code === 'cancelled') return;
127
+ setError(err.message);
128
+ }
129
+ }, [endpoint]);
130
+
131
+ return (
132
+ <>
133
+ <button onClick={onPay}>Pay &amp; run</button>
134
+ {error && <p role="alert">{error}</p>}
135
+ {out && <pre>{JSON.stringify(out, null, 2)}</pre>}
136
+ </>
137
+ );
138
+ }
139
+ ```
140
+
141
+ Set global config once in your app entry:
142
+
143
+ ```js
144
+ import { configure } from '@three-ws/x402-modal';
145
+ configure({ brand: { label: 'Powered by Acme', href: 'https://acme.com' } });
146
+ ```
147
+
148
+ ---
149
+
150
+ ## 5 — Vue 3
151
+
152
+ ```vue
153
+ <script setup>
154
+ import { ref } from 'vue';
155
+ import { pay } from '@three-ws/x402-modal';
156
+
157
+ const out = ref(null);
158
+ const error = ref(null);
159
+
160
+ async function onPay() {
161
+ error.value = null;
162
+ try {
163
+ const res = await pay({ endpoint: '/api/paid/run', merchant: 'Acme' });
164
+ out.value = res.result;
165
+ } catch (err) {
166
+ if (err.code !== 'cancelled') error.value = err.message;
167
+ }
168
+ }
169
+ </script>
170
+
171
+ <template>
172
+ <button @click="onPay">Pay &amp; run</button>
173
+ <p v-if="error" role="alert">{{ error }}</p>
174
+ <pre v-if="out">{{ out }}</pre>
175
+ </template>
176
+ ```
177
+
178
+ ---
179
+
180
+ ## 6 — Self-hosted & fully branded
181
+
182
+ Repoint the Solana backend and add a footer, all from the script tag — no JS:
183
+
184
+ ```html
185
+ <script
186
+ type="module"
187
+ src="https://your.cdn/x402.global.js"
188
+ data-x402-api-origin="https://pay.your-company.com"
189
+ data-x402-brand-label="Powered by Acme"
190
+ data-x402-brand-href="https://acme.com"
191
+ data-x402-builder-wallet="acme"
192
+ data-x402-builder-service="acme_checkout"></script>
193
+ ```
194
+
195
+ Equivalent from JS, before the first `pay()`:
196
+
197
+ ```js
198
+ import { configure } from '@three-ws/x402-modal';
199
+
200
+ configure({
201
+ apiOrigin: 'https://pay.your-company.com',
202
+ brand: { label: 'Powered by Acme', href: 'https://acme.com' },
203
+ builderCode: { wallet: 'acme', service: 'acme_checkout' },
204
+ });
205
+ ```
206
+
207
+ ---
208
+
209
+ ## 7 — Spending caps for an autonomous agent
210
+
211
+ Cap how much a single browser session can spend, so a misbehaving agent can't
212
+ drain a wallet. Amounts are micro-USD (`1_000_000` = `$1`).
213
+
214
+ ```js
215
+ import { pay } from '@three-ws/x402-modal';
216
+
217
+ const out = await pay({
218
+ endpoint: '/api/paid/inference',
219
+ body: { prompt: 'summarize this' },
220
+ autoConnect: true, // skip the picker if exactly one wallet exists
221
+ caps: {
222
+ maxPerCall: 250_000, // $0.25 per call
223
+ maxPerHour: 5_000_000, // $5/hour
224
+ maxPerDay: 20_000_000, // $20/day
225
+ },
226
+ });
227
+ ```
228
+
229
+ A breach is rejected **before** the wallet prompt; a downstream failure rolls the
230
+ reservation back. Caps are advisory client-side guardrails — enforce
231
+ authoritative limits server-side too.
232
+
233
+ ---
234
+
235
+ ## 8 — Declarative buttons with the ESM build
236
+
237
+ The ESM build is side-effect-free, so it does **not** auto-bind. Call `init()`
238
+ yourself (and again after injecting buttons):
239
+
240
+ ```js
241
+ import { init } from '@three-ws/x402-modal';
242
+
243
+ init(); // binds every [data-x402-endpoint] on the page
244
+
245
+ // after dynamically inserting more buttons:
246
+ someContainer.append(newButton);
247
+ init(); // idempotent — re-scans, skips already-bound elements
248
+ ```
249
+
250
+ ---
251
+
252
+ ## 9 — Handling SIWX re-entry
253
+
254
+ When a server supports Sign-In-With-X, a wallet that has already paid can
255
+ re-enter by signing a challenge instead of paying again. `PayResult.siwx` is
256
+ present in that case, and the declarative path also fires `x402:siwx-signed`.
257
+
258
+ ```js
259
+ const out = await pay({ endpoint: '/api/paid/feed' });
260
+
261
+ if (out.siwx) {
262
+ console.log('re-entered via sign-in:', out.siwx.address, out.siwx.network);
263
+ } else {
264
+ console.log('fresh payment:', out.payment.transaction);
265
+ }
266
+ console.log('content:', out.result);
267
+ ```
268
+
269
+ ```js
270
+ // declarative equivalent
271
+ btn.addEventListener('x402:siwx-signed', (e) =>
272
+ console.log('signed in as', e.detail.address));
273
+ btn.addEventListener('x402:result', (e) => render(e.detail.result));
274
+ ```
275
+
276
+ ---
277
+
278
+ See [`../TUTORIAL.md`](../TUTORIAL.md) for longer end-to-end walkthroughs and
279
+ [`CONFIGURATION.md`](./CONFIGURATION.md) for the full option reference.
@@ -0,0 +1,119 @@
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>@three-ws/x402-modal — demo</title>
7
+ <style>
8
+ :root { color-scheme: light dark; }
9
+ * { box-sizing: border-box; }
10
+ body {
11
+ margin: 0; font: 16px/1.6 -apple-system, system-ui, sans-serif;
12
+ background: #0b0c10; color: #e9eaf0;
13
+ min-height: 100vh; padding: 64px 24px 120px;
14
+ }
15
+ main { max-width: 680px; margin: 0 auto; }
16
+ h1 { font-size: 36px; line-height: 1.1; margin: 0 0 8px; letter-spacing: -0.02em; }
17
+ .lede { font-size: 18px; color: #aab; margin: 0 0 36px; }
18
+ h2 { font-size: 20px; margin: 40px 0 10px; }
19
+ p { color: #c7c9d6; }
20
+ code { background: #15161e; padding: 2px 6px; border-radius: 6px; font-size: 14px; }
21
+ .card {
22
+ background: #12131b; border: 1px solid #23252f; border-radius: 16px;
23
+ padding: 22px; margin: 16px 0;
24
+ }
25
+ button.cta {
26
+ cursor: pointer; border: none; border-radius: 12px;
27
+ background: linear-gradient(135deg, #0a84ff, #0066d6); color: #fff;
28
+ padding: 13px 20px; font: inherit; font-weight: 700; letter-spacing: -0.01em;
29
+ }
30
+ button.cta:hover { filter: brightness(1.08); }
31
+ pre {
32
+ background: #0e0f16; border: 1px solid #23252f; border-radius: 12px;
33
+ padding: 14px; overflow: auto; font-size: 13px; color: #cfd2e3;
34
+ margin-top: 14px; white-space: pre-wrap; word-break: break-word;
35
+ }
36
+ .muted { color: #7b7f93; font-size: 13px; }
37
+ .err { color: #f87171; }
38
+ </style>
39
+ </head>
40
+ <body>
41
+ <main>
42
+ <h1>@three-ws/x402-modal</h1>
43
+ <p class="lede">A drop-in payment modal for any x402 paid endpoint. Two ways to wire it, both below.</p>
44
+
45
+ <h2>1 — Declarative (<code>data-x402-*</code>)</h2>
46
+ <p>The button below is bound automatically by the global script. Point
47
+ <code>data-x402-endpoint</code> at your own x402 route to try it live.</p>
48
+ <div class="card">
49
+ <button
50
+ class="cta"
51
+ id="declarative"
52
+ data-x402-endpoint="https://api.acme.example/api/paid/summarize"
53
+ data-x402-method="POST"
54
+ data-x402-body='{"text":"hello world"}'
55
+ data-x402-merchant="Acme"
56
+ data-x402-action="Summarize">
57
+ Pay &amp; summarize
58
+ </button>
59
+ <pre id="declarative-out" class="muted">result will appear here…</pre>
60
+ </div>
61
+
62
+ <h2>2 — Programmatic (<code>pay()</code>)</h2>
63
+ <p>Call <code>pay()</code> from your own handler and await the result.</p>
64
+ <div class="card">
65
+ <button class="cta" id="programmatic">Pay with pay()</button>
66
+ <pre id="programmatic-out" class="muted">result will appear here…</pre>
67
+ </div>
68
+
69
+ <p class="muted">
70
+ This page loads the local build at <code>../dist/x402.global.js</code>.
71
+ Run <code>npm run build</code> in the package root first, then serve this
72
+ folder (e.g. <code>npx serve</code>) and open it. The demo endpoint is a
73
+ placeholder — point it at your own x402 route to try it live.
74
+ </p>
75
+ </main>
76
+
77
+ <!-- Local build. In production, use the CDN:
78
+ <script type="module" src="https://unpkg.com/@three-ws/x402-modal/global"></script> -->
79
+ <script type="module" src="../dist/x402.global.js"></script>
80
+
81
+ <script type="module">
82
+ import { pay } from '../dist/x402-modal.mjs';
83
+
84
+ // Declarative button → listen for the DOM events the global build fires.
85
+ const dEl = document.getElementById('declarative');
86
+ const dOut = document.getElementById('declarative-out');
87
+ dEl.addEventListener('x402:result', (e) => {
88
+ dOut.classList.remove('muted', 'err');
89
+ dOut.textContent = JSON.stringify(e.detail.result, null, 2);
90
+ });
91
+ dEl.addEventListener('x402:error', (e) => {
92
+ dOut.classList.add('err');
93
+ dOut.textContent = 'Error: ' + e.detail.error;
94
+ });
95
+
96
+ // Programmatic button → await pay().
97
+ const pOut = document.getElementById('programmatic-out');
98
+ document.getElementById('programmatic').addEventListener('click', async () => {
99
+ pOut.className = 'muted';
100
+ pOut.textContent = 'opening modal…';
101
+ try {
102
+ const out = await pay({
103
+ endpoint: 'https://api.acme.example/api/paid/summarize',
104
+ method: 'POST',
105
+ body: { text: 'hello world' },
106
+ merchant: 'Acme',
107
+ action: 'Summarize',
108
+ });
109
+ pOut.className = '';
110
+ pOut.textContent = JSON.stringify({ result: out.result, payment: out.payment }, null, 2);
111
+ } catch (e) {
112
+ if (e.code === 'cancelled') { pOut.className = 'muted'; pOut.textContent = 'cancelled.'; return; }
113
+ pOut.className = 'err';
114
+ pOut.textContent = 'Error: ' + e.message;
115
+ }
116
+ });
117
+ </script>
118
+ </body>
119
+ </html>
@@ -0,0 +1,144 @@
1
+ // examples/server.mjs — reference Solana checkout helper for @three-ws/x402-modal.
2
+ //
3
+ // The EVM/Base payment path is fully client-side and needs NO backend. This
4
+ // helper exists only for the Solana path, which must build a transfer
5
+ // transaction (RPC + facilitator fee-payer) for Phantom to sign.
6
+ //
7
+ // It implements the two actions the modal calls:
8
+ // POST /api/x402-checkout?action=prepare { accept, buyer }
9
+ // → { network, tx_base64 }
10
+ // POST /api/x402-checkout?action=encode { accept, signed_tx_base64, resource_url, builder_code? }
11
+ // → { x_payment }
12
+ //
13
+ // Run it:
14
+ // npm i @solana/web3.js @solana/spl-token
15
+ // SOLANA_RPC="https://api.mainnet-beta.solana.com" node examples/server.mjs
16
+ //
17
+ // Then point the modal at it:
18
+ // configure({ apiOrigin: 'http://localhost:8787' })
19
+ //
20
+ // In production, validate `accept` against your own catalog (never trust a
21
+ // client-supplied payTo/asset/amount), rate-limit `prepare`, and shape
22
+ // `payload` to whatever your x402 facilitator expects. See ../docs/BACKEND.md.
23
+
24
+ import { createServer } from 'node:http';
25
+ import {
26
+ Connection,
27
+ PublicKey,
28
+ TransactionMessage,
29
+ VersionedTransaction,
30
+ } from '@solana/web3.js';
31
+ import {
32
+ getAssociatedTokenAddressSync,
33
+ createTransferCheckedInstruction,
34
+ } from '@solana/spl-token';
35
+
36
+ const PORT = Number(process.env.PORT || 8787);
37
+ const RPC = process.env.SOLANA_RPC || 'https://api.mainnet-beta.solana.com';
38
+ const connection = new Connection(RPC, 'confirmed');
39
+
40
+ function isSolana(net) {
41
+ return typeof net === 'string' && (net === 'solana' || net.startsWith('solana:'));
42
+ }
43
+
44
+ async function readJson(req) {
45
+ const chunks = [];
46
+ for await (const c of req) chunks.push(c);
47
+ return JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
48
+ }
49
+
50
+ function json(res, status, body) {
51
+ const payload = JSON.stringify(body);
52
+ res.writeHead(status, {
53
+ 'content-type': 'application/json',
54
+ // Allow cross-origin use when the script is served from another origin.
55
+ 'access-control-allow-origin': '*',
56
+ 'access-control-allow-headers': 'content-type',
57
+ 'access-control-allow-methods': 'POST, OPTIONS',
58
+ });
59
+ res.end(payload);
60
+ }
61
+
62
+ async function handlePrepare(req, res) {
63
+ const { accept, buyer } = await readJson(req);
64
+ if (!isSolana(accept?.network)) {
65
+ return json(res, 400, {
66
+ error: 'wrong_network',
67
+ error_description: 'prepare only builds Solana transactions; EVM clients sign EIP-3009 locally.',
68
+ });
69
+ }
70
+
71
+ const mint = new PublicKey(accept.asset);
72
+ const buyerKey = new PublicKey(buyer);
73
+ const payTo = new PublicKey(accept.payTo);
74
+ const feePayer = new PublicKey(accept.extra.feePayer);
75
+ const decimals = Number(accept.extra.decimals ?? 6);
76
+ const amount = BigInt(accept.amount);
77
+
78
+ const fromAta = getAssociatedTokenAddressSync(mint, buyerKey);
79
+ const toAta = getAssociatedTokenAddressSync(mint, payTo);
80
+
81
+ const ix = createTransferCheckedInstruction(
82
+ fromAta, mint, toAta, buyerKey, amount, decimals,
83
+ );
84
+ // Append the x402 reference account your facilitator watches for settlement.
85
+ // Many facilitators expect a read-only reference pubkey on the transfer ix:
86
+ if (accept.extra.reference) {
87
+ ix.keys.push({ pubkey: new PublicKey(accept.extra.reference), isSigner: false, isWritable: false });
88
+ }
89
+
90
+ const { blockhash } = await connection.getLatestBlockhash('confirmed');
91
+ const message = new TransactionMessage({
92
+ payerKey: feePayer,
93
+ recentBlockhash: blockhash,
94
+ instructions: [ix],
95
+ }).compileToV0Message();
96
+ const tx = new VersionedTransaction(message);
97
+
98
+ return json(res, 200, {
99
+ network: accept.network,
100
+ tx_base64: Buffer.from(tx.serialize()).toString('base64'),
101
+ });
102
+ }
103
+
104
+ async function handleEncode(req, res) {
105
+ const { accept, signed_tx_base64, resource_url, builder_code } = await readJson(req);
106
+ if (!signed_tx_base64 || !resource_url) {
107
+ return json(res, 400, { error: 'bad_request', error_description: 'signed_tx_base64 and resource_url are required' });
108
+ }
109
+
110
+ // Standard Solana exact-scheme payload. Adapt `payload` to your facilitator.
111
+ const paymentPayload = {
112
+ x402Version: 2,
113
+ scheme: 'exact',
114
+ network: accept.network,
115
+ resource: { url: resource_url, mimeType: 'application/json' },
116
+ accepted: accept,
117
+ payload: { transaction: signed_tx_base64 },
118
+ ...(builder_code ? { extensions: { 'builder-code': builder_code } } : {}),
119
+ };
120
+
121
+ return json(res, 200, {
122
+ x_payment: Buffer.from(JSON.stringify(paymentPayload), 'utf8').toString('base64'),
123
+ });
124
+ }
125
+
126
+ const server = createServer(async (req, res) => {
127
+ try {
128
+ if (req.method === 'OPTIONS') return json(res, 204, {});
129
+ const url = new URL(req.url, `http://${req.headers.host}`);
130
+ if (req.method === 'POST' && url.pathname === '/api/x402-checkout') {
131
+ const action = url.searchParams.get('action');
132
+ if (action === 'prepare') return await handlePrepare(req, res);
133
+ if (action === 'encode') return await handleEncode(req, res);
134
+ return json(res, 404, { error: 'not_found', error_description: `unknown action: ${action ?? '(none)'}` });
135
+ }
136
+ return json(res, 404, { error: 'not_found' });
137
+ } catch (err) {
138
+ return json(res, 500, { error: 'server_error', error_description: String(err?.message || err) });
139
+ }
140
+ });
141
+
142
+ server.listen(PORT, () => {
143
+ console.log(`x402-checkout helper on http://localhost:${PORT} (RPC: ${RPC})`);
144
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@three-ws/x402-modal",
3
- "version": "0.2.0",
4
- "description": "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, and a receipt — vanilla JS, no bundler required. Powered by three.ws.",
3
+ "version": "0.2.1",
4
+ "description": "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, and a receipt — vanilla JS, no bundler required.",
5
5
  "keywords": [
6
6
  "x402",
7
7
  "payments",
@@ -23,16 +23,15 @@
23
23
  "agent-payments",
24
24
  "402"
25
25
  ],
26
- "author": "three.ws <support@three.ws> (https://three.ws)",
27
- "license": "Apache-2.0",
28
- "homepage": "https://three.ws",
26
+ "author": "nirholas",
27
+ "license": "UNLICENSED",
28
+ "homepage": "https://github.com/nirholas/x402-modal#readme",
29
29
  "repository": {
30
30
  "type": "git",
31
- "url": "git+https://github.com/nirholas/three.ws.git",
32
- "directory": "x402-modal-sdk"
31
+ "url": "git+https://github.com/nirholas/x402-modal.git"
33
32
  },
34
33
  "bugs": {
35
- "url": "https://github.com/nirholas/three.ws/issues"
34
+ "url": "https://github.com/nirholas/x402-modal/issues"
36
35
  },
37
36
  "type": "module",
38
37
  "main": "./dist/x402-modal.mjs",
@@ -54,9 +53,12 @@
54
53
  "src",
55
54
  "types",
56
55
  "docs",
56
+ "examples",
57
57
  "README.md",
58
58
  "TUTORIAL.md",
59
- "LICENSE"
59
+ "CONTRIBUTING.md",
60
+ "LICENSE",
61
+ "CHANGELOG.md"
60
62
  ],
61
63
  "scripts": {
62
64
  "build": "node build.mjs",