@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/CHANGELOG.md +57 -0
- package/LICENSE +180 -0
- package/README.md +347 -0
- package/TUTORIAL.md +188 -0
- package/dist/index.d.ts +141 -0
- package/dist/x402.js +1777 -0
- package/dist/x402.min.js +375 -0
- package/docs/api-reference.md +227 -0
- package/docs/architecture.md +171 -0
- package/docs/server-setup.md +239 -0
- package/docs/siwx.md +116 -0
- package/docs/spending-caps.md +92 -0
- package/docs/theming.md +124 -0
- package/examples/README.md +22 -0
- package/examples/plain-html/index.html +229 -0
- package/examples/react/README.md +69 -0
- package/examples/react/X402Button.jsx +84 -0
- package/examples/server-express/package.json +16 -0
- package/examples/server-express/public/index.html +89 -0
- package/examples/server-express/server.js +89 -0
- package/package.json +113 -0
- package/server/README.md +68 -0
- package/server/checkout.js +392 -0
- package/server/express.js +44 -0
- package/server/vercel.js +54 -0
- package/src/index.js +1776 -0
- package/types/index.d.ts +141 -0
- package/types/server.d.ts +109 -0
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 & 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)
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|