@three-ws/x402-modal 0.2.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,281 @@
1
+ # Tutorials — @three-ws/x402-modal
2
+
3
+ Hands-on, copy-paste walkthroughs. Each one is self-contained and ends with
4
+ something running. Pick the path that matches your stack:
5
+
6
+ 1. [Tutorial 1 — Sell an API call in 5 minutes (Base, no backend)](#tutorial-1--sell-an-api-call-in-5-minutes-base-no-backend)
7
+ 2. [Tutorial 2 — Programmatic checkout in a SPA](#tutorial-2--programmatic-checkout-in-a-spa)
8
+ 3. [Tutorial 3 — A content paywall](#tutorial-3--a-content-paywall)
9
+ 4. [Tutorial 4 — Self-hosting & full branding](#tutorial-4--self-hosting--full-branding)
10
+ 5. [Tutorial 5 — Adding the Solana path (the backend helper)](#tutorial-5--adding-the-solana-path-the-backend-helper)
11
+ 6. [Tutorial 6 — Spending caps for autonomous agents](#tutorial-6--spending-caps-for-autonomous-agents)
12
+ 7. [Troubleshooting](#troubleshooting)
13
+
14
+ Every tutorial assumes you already have — or are about to build — an x402 server
15
+ that answers a protected route with `402 Payment Required`. If you don't, skim
16
+ [the backend doc](./docs/BACKEND.md) first; it shows the minimal server side.
17
+
18
+ ---
19
+
20
+ ## Tutorial 1 — Sell an API call in 5 minutes (Base, no backend)
21
+
22
+ **Goal:** charge USDC on Base for one API call, with zero backend beyond your
23
+ existing x402 server. The Base path is fully client-side.
24
+
25
+ ### 1. Have an endpoint that returns 402
26
+
27
+ Your server should respond to an unpaid request with `402` and an `accepts`
28
+ array describing a Base USDC payment. A minimal challenge body looks like:
29
+
30
+ ```json
31
+ {
32
+ "x402Version": 2,
33
+ "accepts": [{
34
+ "scheme": "exact",
35
+ "network": "eip155:8453",
36
+ "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
37
+ "payTo": "0xYourMerchantWallet",
38
+ "amount": "10000",
39
+ "maxTimeoutSeconds": 600,
40
+ "extra": { "name": "USDC", "version": "2", "decimals": 6 }
41
+ }]
42
+ }
43
+ ```
44
+
45
+ (`amount` is atomic — `10000` = `$0.01` at 6 decimals.)
46
+
47
+ ### 2. Add the modal to your page
48
+
49
+ ```html
50
+ <script type="module" src="https://unpkg.com/@three-ws/x402-modal/global"></script>
51
+
52
+ <button
53
+ data-x402-endpoint="https://api.acme.com/paid/summarize"
54
+ data-x402-method="POST"
55
+ data-x402-body='{"text":"Long article text here..."}'
56
+ data-x402-merchant="Acme AI"
57
+ data-x402-action="Summarize article">
58
+ Summarize for $0.01
59
+ </button>
60
+ ```
61
+
62
+ ### 3. Handle the result
63
+
64
+ ```html
65
+ <script>
66
+ document.querySelector('button').addEventListener('x402:result', (e) => {
67
+ const { result, payment } = e.detail;
68
+ document.querySelector('#out').textContent = JSON.stringify(result, null, 2);
69
+ console.log('settled tx:', payment.transaction);
70
+ });
71
+ document.querySelector('button').addEventListener('x402:error', (e) => {
72
+ alert('Payment failed: ' + e.detail.error);
73
+ });
74
+ </script>
75
+ <pre id="out"></pre>
76
+ ```
77
+
78
+ That's the whole integration. Click → MetaMask signs an EIP-3009 authorization
79
+ (no gas, no on-chain tx for the user) → your endpoint runs and settles → the
80
+ result renders.
81
+
82
+ ---
83
+
84
+ ## Tutorial 2 — Programmatic checkout in a SPA
85
+
86
+ **Goal:** trigger checkout from your own button in React/Vue/Svelte/vanilla and
87
+ get the result back as a promise.
88
+
89
+ ```sh
90
+ npm i @three-ws/x402-modal
91
+ ```
92
+
93
+ ```jsx
94
+ // React example
95
+ import { pay } from '@three-ws/x402-modal';
96
+
97
+ function BuyButton({ text }) {
98
+ const [out, setOut] = useState(null);
99
+ const [err, setErr] = useState(null);
100
+
101
+ async function onClick() {
102
+ setErr(null);
103
+ try {
104
+ const res = await pay({
105
+ endpoint: '/api/paid/summarize',
106
+ method: 'POST',
107
+ body: { text },
108
+ merchant: 'Acme AI',
109
+ action: 'Summarize',
110
+ });
111
+ setOut(res.result);
112
+ } catch (e) {
113
+ if (e.code === 'cancelled') return; // user closed the modal — not an error
114
+ setErr(e.message);
115
+ }
116
+ }
117
+
118
+ return (
119
+ <>
120
+ <button onClick={onClick}>Summarize for $0.01</button>
121
+ {err && <p className="error">{err}</p>}
122
+ {out && <pre>{JSON.stringify(out, null, 2)}</pre>}
123
+ </>
124
+ );
125
+ }
126
+ ```
127
+
128
+ The ESM import has **no side effects** — it won't scan the DOM or touch
129
+ `window`. You control exactly when the modal opens.
130
+
131
+ ---
132
+
133
+ ## Tutorial 3 — A content paywall
134
+
135
+ **Goal:** blur premium content until the visitor pays, then reveal it.
136
+
137
+ ```html
138
+ <script type="module" src="https://unpkg.com/@three-ws/x402-modal/global"></script>
139
+
140
+ <article id="premium" class="locked">
141
+ <div class="blurred">…premium article body…</div>
142
+ <button
143
+ data-x402-endpoint="https://api.acme.com/paid/article/42"
144
+ data-x402-merchant="Acme Times"
145
+ data-x402-action="Unlock this article">
146
+ Unlock — $0.25
147
+ </button>
148
+ </article>
149
+
150
+ <style>
151
+ .locked .blurred { filter: blur(8px); pointer-events: none; user-select: none; }
152
+ .unlocked .blurred { filter: none; }
153
+ </style>
154
+
155
+ <script>
156
+ const article = document.getElementById('premium');
157
+ article.querySelector('button').addEventListener('x402:result', (e) => {
158
+ // Put the unlocked content from the endpoint into the page, then reveal.
159
+ article.querySelector('.blurred').innerHTML = e.detail.result.html;
160
+ article.classList.replace('locked', 'unlocked');
161
+ // Remember it so a refresh doesn't re-lock (optional).
162
+ localStorage.setItem('unlocked:article:42', '1');
163
+ });
164
+ </script>
165
+ ```
166
+
167
+ To skip the paywall for returning buyers, check your flag on load — and if your
168
+ server supports **SIWX** (sign-in-with-x), the modal will automatically offer
169
+ "Already paid? Sign in" so they re-enter by signing instead of paying again.
170
+
171
+ ---
172
+
173
+ ## Tutorial 4 — Self-hosting & full branding
174
+
175
+ **Goal:** serve the script yourself, point the Solana backend at your own
176
+ domain, and replace the footer attribution — without writing JS.
177
+
178
+ ```html
179
+ <script
180
+ type="module"
181
+ src="https://cdn.acme.com/x402.global.js"
182
+ data-x402-api-origin="https://pay.acme.com"
183
+ data-x402-brand-label="Powered by Acme Pay"
184
+ data-x402-brand-href="https://acme.com/pay"
185
+ data-x402-builder-wallet="acme"
186
+ data-x402-builder-service="acme_checkout"></script>
187
+ ```
188
+
189
+ Grab `x402.global.js` from the package's `dist/` (or build it with
190
+ `npm run build`) and host it anywhere static.
191
+
192
+ Prefer JS? Configure before the first payment:
193
+
194
+ ```js
195
+ import { configure } from '@three-ws/x402-modal';
196
+
197
+ configure({
198
+ apiOrigin: 'https://pay.acme.com',
199
+ brand: { label: 'Powered by Acme Pay', href: 'https://acme.com/pay' },
200
+ builderCode: { wallet: 'acme', service: 'acme_checkout' },
201
+ });
202
+ ```
203
+
204
+ To drop the footer link entirely, set `data-x402-builder-disable` and a
205
+ `brand` with no `href`.
206
+
207
+ ---
208
+
209
+ ## Tutorial 5 — Adding the Solana path (the backend helper)
210
+
211
+ **Goal:** accept USDC on Solana too. This is the only path that needs a backend,
212
+ because building a Solana transfer needs RPC + the facilitator fee-payer.
213
+
214
+ The modal calls two actions on `{apiOrigin}/api/x402-checkout`:
215
+
216
+ ```
217
+ POST /api/x402-checkout?action=prepare
218
+ body: { accept, buyer }
219
+ → { tx_base64 } # a VersionedTransaction the buyer will sign
220
+
221
+ POST /api/x402-checkout?action=encode
222
+ body: { accept, signed_tx_base64, resource_url, builder_code? }
223
+ → { x_payment } # the base64 X-PAYMENT value to send to the merchant
224
+ ```
225
+
226
+ A reference Express handler that delegates to an x402 facilitator SDK lives in
227
+ [`examples/server.mjs`](./examples/server.mjs), and the exact field contract is
228
+ in [`docs/BACKEND.md`](./docs/BACKEND.md).
229
+
230
+ Once the helper is live, no client change is needed beyond pointing `apiOrigin`
231
+ at it (Tutorial 4). When your `402` advertises **both** a Solana and a Base
232
+ `accept`, the modal shows a wallet picker; when it advertises one, it goes
233
+ straight there.
234
+
235
+ ---
236
+
237
+ ## Tutorial 6 — Spending caps for autonomous agents
238
+
239
+ **Goal:** let an agent (or a kiosk, or a shared browser) pay automatically, but
240
+ cap how much it can spend per call / hour / day.
241
+
242
+ ```js
243
+ import { pay } from '@three-ws/x402-modal';
244
+
245
+ await pay({
246
+ endpoint: '/api/paid/inference',
247
+ body: { prompt },
248
+ autoConnect: true, // skip the picker when one wallet is present
249
+ caps: {
250
+ maxPerCall: 500_000, // $0.50 max for any single call
251
+ maxPerHour: 5_000_000, // $5.00/hour
252
+ maxPerDay: 20_000_000, // $20.00/day
253
+ },
254
+ });
255
+ ```
256
+
257
+ Amounts are **micro-USD** (`1_000_000` = `$1`). Caps are tracked per wallet
258
+ address in `localStorage`, bucketed by rolling UTC hour and day, and survive
259
+ page reloads. A call that would breach a cap is rejected *before* the wallet
260
+ prompt with a clear reason; a payment that fails downstream rolls its
261
+ reservation back so it doesn't count against the budget.
262
+
263
+ > Caps are a client-side guardrail, not a trust boundary. For hard limits an
264
+ > agent can't bypass, also enforce server-side.
265
+
266
+ ---
267
+
268
+ ## Troubleshooting
269
+
270
+ | Symptom | Cause / fix |
271
+ |---|---|
272
+ | **"Endpoint did not return 402 (got 200)"** | The modal was pointed at a free/unprotected URL, or your server didn't send a `402`. Confirm the route is actually gated. |
273
+ | **"no `accepts` array could be found"** | Your `402` body (or `payment-required` header) is missing the `accepts` array. See the challenge shape in Tutorial 1. |
274
+ | **"NaN USDC" in the price** | Your `accept` used a field other than `amount`/`maxAmountRequired`. The modal normalizes both; anything else needs mapping. |
275
+ | **Solana payment errors with "component … was blocked"** | A strict Content-Security-Policy blocked the dynamic `@solana/web3.js` import. Allow the CDN, set `solanaWeb3Url` to a self-hosted copy, or use the Base path (no third-party code). |
276
+ | **EVM signature rejected by the facilitator** | Check the USDC `extra.version` in your `accept` matches the deployed contract (Base USDC is `"2"`), and that `payTo`/`asset` are correct. |
277
+ | **Modal opens then immediately shows the picker disabled** | No supported wallet detected. Install Phantom (Solana) or MetaMask (EVM), or advertise an `accept` for a chain the visitor's wallet supports. |
278
+ | **`pay()` rejected but nothing went wrong** | The user closed the modal: the rejection's `.code === 'cancelled'`. Treat it as a no-op, not an error. |
279
+
280
+ Still stuck? Open an issue at
281
+ <https://github.com/nirholas/three.ws/issues>.