@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.
- package/CHANGELOG.md +24 -0
- package/CONTRIBUTING.md +92 -0
- package/LICENSE +38 -180
- package/README.md +541 -175
- package/TUTORIAL.md +1 -1
- package/dist/x402-modal.mjs +12 -9
- package/dist/x402-modal.mjs.map +2 -2
- package/dist/x402.global.js +10 -9
- package/dist/x402.global.js.map +2 -2
- package/docs/BACKEND.md +2 -1
- package/docs/CONFIGURATION.md +20 -6
- package/docs/EXAMPLES.md +279 -0
- package/examples/index.html +119 -0
- package/examples/server.mjs +144 -0
- package/package.json +11 -9
- package/src/x402-modal.js +20 -15
- package/types/index.d.ts +7 -3
package/README.md
CHANGED
|
@@ -4,64 +4,134 @@
|
|
|
4
4
|
|
|
5
5
|
**A drop-in payment modal for any [x402](https://x402.org) paid endpoint.**
|
|
6
6
|
|
|
7
|
-
One script tag turns an HTTP `402 Payment Required` into a polished checkout:
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
One `<script>` tag turns an HTTP `402 Payment Required` into a polished checkout:
|
|
8
|
+
discover the challenge, connect a wallet (Phantom on Solana, MetaMask / any EVM
|
|
9
|
+
wallet via EIP-3009), sign, settle, and show a receipt — in **vanilla JS, with no
|
|
10
|
+
bundler and no framework**.
|
|
11
11
|
|
|
12
12
|
[](https://www.npmjs.com/package/@three-ws/x402-modal)
|
|
13
|
-
[](https://www.jsdelivr.com/package/npm/@three-ws/x402-modal)
|
|
14
|
+
[](./LICENSE)
|
|
15
15
|

|
|
16
|
+

|
|
16
17
|
|
|
17
|
-
[
|
|
18
|
+
[Install](#install) · [Quickstart](#quickstart) · [How it works](#how-it-works) · [API](#api-reference) · [Configuration](#configuration-reference) · [Frameworks](#framework-guides) · [Wallets](#wallets--assets) · [FAQ](#faq--troubleshooting)
|
|
18
19
|
|
|
19
20
|
</div>
|
|
20
21
|
|
|
21
22
|
---
|
|
22
23
|
|
|
23
|
-
##
|
|
24
|
+
## What & why
|
|
24
25
|
|
|
25
26
|
[x402](https://x402.org) revives HTTP `402 Payment Required` as a real payment
|
|
26
27
|
rail: a server answers a request with a `402` whose body lists what it
|
|
27
|
-
`accepts` (asset
|
|
28
|
-
request with an `X-PAYMENT` header. It
|
|
28
|
+
`accepts` (asset · amount · network · pay-to), the client pays, and re-sends the
|
|
29
|
+
request with an `X-PAYMENT` header. It is built for pay-per-call APIs, agent
|
|
29
30
|
economies, and content paywalls — but every merchant ends up rebuilding the same
|
|
30
31
|
fiddly client: parse the challenge, connect a wallet, sign the right thing for
|
|
31
|
-
the right chain, retry, settle, show a receipt.
|
|
32
|
+
the right chain, retry, settle, and show a receipt.
|
|
32
33
|
|
|
33
34
|
**This package is that client, done once and done well.** Point it at a `402`
|
|
34
|
-
endpoint and it renders the entire flow.
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
endpoint and it renders the entire flow.
|
|
36
|
+
|
|
37
|
+
### Features
|
|
38
|
+
|
|
39
|
+
- **Zero-config drop-in** — one `<script>` tag binds every `data-x402-endpoint`
|
|
40
|
+
element on the page. No build step, no framework, no wallet adapter.
|
|
41
|
+
- **Works against any origin** — the `402 → sign → settle` flow is
|
|
42
|
+
merchant-agnostic. Point it at your endpoint or someone else's.
|
|
43
|
+
- **Solana + EVM** — Phantom on Solana, MetaMask / any injected `window.ethereum`
|
|
44
|
+
on Base and other EVM chains. USDC by default; arbitrary SPL tokens supported.
|
|
45
|
+
- **No bundler required** — ships a single self-contained IIFE for `<script>`
|
|
46
|
+
use, plus a side-effect-free ESM build for bundlers.
|
|
47
|
+
- **Gasless for the payer** — EVM uses EIP-3009 signed authorizations; Solana
|
|
48
|
+
uses a facilitator fee-payer. The buyer never needs native gas.
|
|
49
|
+
- **Spending caps** — per-wallet `localStorage` caps by call / hour / day, so an
|
|
50
|
+
autonomous agent can't overspend.
|
|
51
|
+
- **Zero runtime dependencies** — the two optional wallet libraries are loaded
|
|
52
|
+
on demand from a CDN, only when that wallet path actually runs.
|
|
53
|
+
- **Every state designed** — discover, connect, sign, settle, retry, error, and
|
|
54
|
+
receipt all render as live rows with light/dark theming.
|
|
55
|
+
|
|
56
|
+
The EVM/Base path is **100% client-side**. The Solana path needs one small
|
|
57
|
+
backend helper (see [The backend](#the-solana-backend)).
|
|
37
58
|
|
|
38
|
-
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Install
|
|
39
62
|
|
|
40
|
-
###
|
|
63
|
+
### npm (bundlers / frameworks)
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
npm i @three-ws/x402-modal
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
import { pay, configure } from '@three-ws/x402-modal'; // ESM, no side effects
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### CDN `<script>` (no install, no bundler)
|
|
74
|
+
|
|
75
|
+
The `/global` build auto-binds `[data-x402-endpoint]` elements and exposes
|
|
76
|
+
`window.X402`. Pick a CDN:
|
|
41
77
|
|
|
42
78
|
```html
|
|
79
|
+
<!-- unpkg -->
|
|
43
80
|
<script type="module" src="https://unpkg.com/@three-ws/x402-modal/global"></script>
|
|
44
81
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
data-x402-method="POST"
|
|
48
|
-
data-x402-body='{"text":"hello world"}'
|
|
49
|
-
data-x402-merchant="Acme"
|
|
50
|
-
data-x402-action="Summarize">
|
|
51
|
-
Pay & summarize
|
|
52
|
-
</button>
|
|
82
|
+
<!-- jsDelivr -->
|
|
83
|
+
<script type="module" src="https://cdn.jsdelivr.net/npm/@three-ws/x402-modal/global"></script>
|
|
53
84
|
```
|
|
54
85
|
|
|
55
|
-
|
|
56
|
-
|
|
86
|
+
> Pin a version for production, e.g.
|
|
87
|
+
> `https://unpkg.com/@three-ws/x402-modal@0.2.0/global`.
|
|
57
88
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
89
|
+
The CDN bundle is a single self-contained file (~12 kB gzipped). No `npm`, no
|
|
90
|
+
build step.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Quickstart
|
|
95
|
+
|
|
96
|
+
The smallest possible page that pops the modal and settles a `402`. Save it as
|
|
97
|
+
`index.html`, replace the endpoint with your own x402 route, and open it:
|
|
98
|
+
|
|
99
|
+
```html
|
|
100
|
+
<!doctype html>
|
|
101
|
+
<html lang="en">
|
|
102
|
+
<head><meta charset="utf-8" /><title>x402 checkout</title></head>
|
|
103
|
+
<body>
|
|
104
|
+
<button
|
|
105
|
+
data-x402-endpoint="https://api.example.com/paid/summarize"
|
|
106
|
+
data-x402-method="POST"
|
|
107
|
+
data-x402-body='{"text":"hello world"}'
|
|
108
|
+
data-x402-merchant="Acme"
|
|
109
|
+
data-x402-action="Summarize">
|
|
110
|
+
Pay & summarize
|
|
111
|
+
</button>
|
|
112
|
+
|
|
113
|
+
<pre id="out"></pre>
|
|
114
|
+
|
|
115
|
+
<script type="module" src="https://unpkg.com/@three-ws/x402-modal/global"></script>
|
|
116
|
+
<script type="module">
|
|
117
|
+
const btn = document.querySelector('button');
|
|
118
|
+
btn.addEventListener('x402:result', (e) => {
|
|
119
|
+
document.getElementById('out').textContent =
|
|
120
|
+
JSON.stringify(e.detail.result, null, 2);
|
|
121
|
+
});
|
|
122
|
+
btn.addEventListener('x402:error', (e) => {
|
|
123
|
+
document.getElementById('out').textContent = 'Error: ' + e.detail.error;
|
|
124
|
+
});
|
|
125
|
+
</script>
|
|
126
|
+
</body>
|
|
127
|
+
</html>
|
|
62
128
|
```
|
|
63
129
|
|
|
64
|
-
|
|
130
|
+
Clicking the button opens the modal, runs the payment, calls the endpoint, and
|
|
131
|
+
fires `x402:result` on the button with the full
|
|
132
|
+
`{ ok, result, payment, response }` payload.
|
|
133
|
+
|
|
134
|
+
Prefer to drive it yourself? Call `pay()` and await the result:
|
|
65
135
|
|
|
66
136
|
```js
|
|
67
137
|
import { pay } from '@three-ws/x402-modal';
|
|
@@ -74,149 +144,253 @@ const out = await pay({
|
|
|
74
144
|
action: 'Summarize',
|
|
75
145
|
});
|
|
76
146
|
|
|
77
|
-
console.log(out.result);
|
|
78
|
-
console.log(out.payment);
|
|
147
|
+
console.log(out.result); // the endpoint's response, after settlement
|
|
148
|
+
console.log(out.payment); // { network, payer, transaction }
|
|
79
149
|
```
|
|
80
150
|
|
|
81
151
|
`pay()` resolves once the paid call returns `200`, or rejects with an `Error`
|
|
82
152
|
whose `.code === 'cancelled'` if the user closes the modal.
|
|
83
153
|
|
|
84
|
-
|
|
154
|
+
---
|
|
85
155
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
156
|
+
## How it works
|
|
157
|
+
|
|
158
|
+
The modal drives the four-step x402 flow. Each step renders as a live row in the
|
|
159
|
+
modal (spinner → check → error), with a **Try again** affordance on failure,
|
|
160
|
+
automatic retry on a `429` upstream throttle (the payment isn't settled until the
|
|
161
|
+
work succeeds, so re-sending can't double-charge), and a receipt with an explorer
|
|
162
|
+
link on success.
|
|
163
|
+
|
|
164
|
+
```mermaid
|
|
165
|
+
sequenceDiagram
|
|
166
|
+
participant U as User
|
|
167
|
+
participant M as x402-modal
|
|
168
|
+
participant W as Wallet (Phantom / EVM)
|
|
169
|
+
participant E as Merchant endpoint
|
|
170
|
+
|
|
171
|
+
U->>M: click / pay({ endpoint })
|
|
172
|
+
M->>E: 1. DISCOVER — request with no payment
|
|
173
|
+
E-->>M: 402 + accepts[] (asset · amount · network · payTo)
|
|
174
|
+
M->>U: render price + network, pick a wallet
|
|
175
|
+
U->>W: 2. CONNECT
|
|
176
|
+
W-->>M: account address
|
|
177
|
+
M->>W: 3. SIGN — EVM: EIP-3009 typed data · Solana: built tx
|
|
178
|
+
W-->>M: signed authorization / transaction
|
|
179
|
+
M->>E: 4. SETTLE — re-send request with X-PAYMENT
|
|
180
|
+
E-->>M: 200 + result + x-payment-response receipt
|
|
181
|
+
M-->>U: receipt + explorer link, resolve pay()
|
|
93
182
|
```
|
|
94
183
|
|
|
95
|
-
|
|
184
|
+
1. **Discover** — send the request you described with no payment. Expect a `402`
|
|
185
|
+
with a JSON body containing `accepts[]`, or a `401` with a base64-JSON
|
|
186
|
+
`payment-required` header (MCP 2025-06-18). Anything else is an error.
|
|
187
|
+
2. **Connect** — pick a wallet that can satisfy an accept: Solana → Phantom,
|
|
188
|
+
EVM → MetaMask / any injected `window.ethereum`. When the `402` advertises
|
|
189
|
+
more than one network the modal shows a wallet picker; with exactly one it
|
|
190
|
+
goes straight there.
|
|
191
|
+
3. **Sign** — EVM signs an EIP-3009 `transferWithAuthorization` (no on-chain tx,
|
|
192
|
+
no gas for the payer). Solana has a backend build the transaction, then
|
|
193
|
+
Phantom signs it.
|
|
194
|
+
4. **Settle** — re-send the request with the `X-PAYMENT` header. The endpoint
|
|
195
|
+
runs the work, settles on-chain, and returns `200` plus an
|
|
196
|
+
`x-payment-response` receipt.
|
|
197
|
+
|
|
198
|
+
> The full step-by-step protocol, including the `401`/`payment-required` header
|
|
199
|
+
> variant and SIWX re-entry, lives in [`docs/PROTOCOL.md`](./docs/PROTOCOL.md).
|
|
96
200
|
|
|
97
|
-
|
|
98
|
-
import { configure } from '@three-ws/x402-modal';
|
|
201
|
+
---
|
|
99
202
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
203
|
+
## API reference
|
|
204
|
+
|
|
205
|
+
The ESM entry (`@three-ws/x402-modal`) is **side-effect-free** — importing it
|
|
206
|
+
never touches `window` or binds anything. The `/global` entry adds the auto-bind
|
|
207
|
+
behavior and `window.X402`.
|
|
208
|
+
|
|
209
|
+
### `pay(options): Promise<PayResult>`
|
|
210
|
+
|
|
211
|
+
Open the modal for one endpoint and resolve after settlement.
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
function pay(options: PayOptions): Promise<PayResult>;
|
|
104
215
|
```
|
|
105
216
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
217
|
+
| option | type | default | notes |
|
|
218
|
+
|---------------|-------------------------------|------------------|-------|
|
|
219
|
+
| `endpoint` | `string` | — (**required**) | the x402-protected URL to pay for and call |
|
|
220
|
+
| `method` | `string` | `GET` / `POST`\* | \*defaults to `POST` when a `body` is set |
|
|
221
|
+
| `body` | `object \| string` | — | forwarded to the endpoint (object → JSON) |
|
|
222
|
+
| `headers` | `Record<string,string>` | — | merged into discovery + paid calls |
|
|
223
|
+
| `merchant` | `string` | header default | shown in the modal header |
|
|
224
|
+
| `action` | `string` | header default | shown in the modal header (e.g. "Summarize") |
|
|
225
|
+
| `caps` | `SpendingCaps` | — | µUSD spending caps (see [caps](#spending-caps)) |
|
|
226
|
+
| `autoConnect` | `boolean` | `false` | skip the picker when exactly one wallet is detected |
|
|
227
|
+
| `apiOrigin` | `string` | global config | per-call override of the Solana checkout backend |
|
|
228
|
+
| `brand` | `{ label, href }` | global config | per-call footer override |
|
|
229
|
+
|
|
230
|
+
**Returns** `PayResult`:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
interface PayResult {
|
|
234
|
+
ok: true;
|
|
235
|
+
result: unknown; // the endpoint's response body (JSON or text)
|
|
236
|
+
payment?: { network, payer, transaction, ... }; // present on a fresh payment
|
|
237
|
+
siwx?: { address, network }; // present on SIWX re-entry instead of paying
|
|
238
|
+
response: { status: number; headers: Record<string, string> };
|
|
239
|
+
}
|
|
240
|
+
```
|
|
110
241
|
|
|
111
|
-
|
|
242
|
+
**Rejects** with an `Error`. A user-cancelled modal rejects with
|
|
243
|
+
`err.code === 'cancelled'` — check for it to distinguish a deliberate close from
|
|
244
|
+
a real failure.
|
|
112
245
|
|
|
246
|
+
### `configure(config): config`
|
|
247
|
+
|
|
248
|
+
Merge config into the global defaults and return the resolved snapshot. Call it
|
|
249
|
+
once at startup, before the first `pay()`. See [Configuration](#configuration-reference).
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
function configure(config?: X402Config): Required<X402Config>;
|
|
113
253
|
```
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
│
|
|
122
|
-
├─ 3. authorize Solana: backend builds the tx → Phantom signs it
|
|
123
|
-
│ EVM: wallet signs an EIP-3009 transferWithAuthorization
|
|
124
|
-
│ (no on-chain tx, no gas for the payer)
|
|
125
|
-
│
|
|
126
|
-
└─ 4. verify re-send the request with `X-PAYMENT` → endpoint runs the work,
|
|
127
|
-
settles on-chain, returns 200 + `x-payment-response` receipt
|
|
254
|
+
|
|
255
|
+
### `getConfig(): config`
|
|
256
|
+
|
|
257
|
+
Read the current resolved global config.
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
function getConfig(): Required<X402Config>;
|
|
128
261
|
```
|
|
129
262
|
|
|
130
|
-
|
|
131
|
-
**Try again** affordance on failure, automatic retry on a `429` upstream
|
|
132
|
-
throttle (the payment isn't settled until the work succeeds, so re-sending can't
|
|
133
|
-
double-charge), and a receipt with an explorer link on success.
|
|
263
|
+
### `init(): void`
|
|
134
264
|
|
|
135
|
-
|
|
265
|
+
Scan the document and bind every `[data-x402-endpoint]` element. Idempotent.
|
|
136
266
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
| Base Sepolia, Arbitrum, Optimism | same | EIP-3009 | No |
|
|
141
|
-
| **Solana** (`solana:*`) | Phantom | `exact` (facilitator-settled) | Yes — `prepare`/`encode` helper |
|
|
267
|
+
The `/global` build calls this automatically (and re-scans on DOM mutation via a
|
|
268
|
+
`MutationObserver`). Call it yourself only when using the **ESM** build with
|
|
269
|
+
declarative `data-*` buttons.
|
|
142
270
|
|
|
143
|
-
|
|
144
|
-
when it advertises exactly one, it goes straight there.
|
|
271
|
+
### `bindElement(el: Element): void`
|
|
145
272
|
|
|
146
|
-
|
|
273
|
+
Bind one element's click to open the modal. On success it dispatches
|
|
274
|
+
`x402:result` (and `x402:siwx-signed` when re-entry was via SIWX); on failure
|
|
275
|
+
`x402:error`. A user cancel dispatches nothing. Idempotent per element.
|
|
147
276
|
|
|
148
|
-
### `
|
|
277
|
+
### `readOptsFrom(el: Element): PayOptions`
|
|
149
278
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
| `endpoint` | `string` | — (**required**) | the x402-protected URL to pay for and call |
|
|
153
|
-
| `method` | `string` | `GET` / `POST`* | *POST when a `body` is set |
|
|
154
|
-
| `body` | `object \| string` | — | forwarded to the endpoint (object → JSON) |
|
|
155
|
-
| `headers` | `Record<string,string>` | — | merged into discovery + paid calls |
|
|
156
|
-
| `merchant` | `string` | `Payment` | shown in the modal header |
|
|
157
|
-
| `action` | `string` | `Pay-per-call` | shown in the modal header |
|
|
158
|
-
| `caps` | `{ maxPerCall, maxPerHour, maxPerDay }` | — | µUSD spending caps (see [Configuration](#configuration)) |
|
|
159
|
-
| `autoConnect` | `boolean` | `false` | skip the picker when exactly one wallet is detected |
|
|
160
|
-
| `apiOrigin` | `string` | global config | per-call override of the Solana checkout backend |
|
|
161
|
-
| `brand` | `{ label, href }` | global config | per-call footer override |
|
|
279
|
+
Read `PayOptions` from an element's `data-x402-*` attributes. Useful if you want
|
|
280
|
+
to bind elements yourself with custom event handling.
|
|
162
281
|
|
|
163
|
-
|
|
164
|
-
on a fresh payment (`{ network, payer, transaction }`); `siwx` is present when
|
|
165
|
-
the user re-entered via sign-in instead of paying.
|
|
282
|
+
### `version: string`
|
|
166
283
|
|
|
167
|
-
|
|
284
|
+
The package version string (e.g. `"0.2.0"`).
|
|
168
285
|
|
|
169
|
-
|
|
286
|
+
### `class CheckoutModal`
|
|
170
287
|
|
|
171
|
-
|
|
288
|
+
The low-level modal controller. Most callers should use `pay()` instead.
|
|
172
289
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
290
|
+
```ts
|
|
291
|
+
class CheckoutModal {
|
|
292
|
+
constructor(opts: PayOptions);
|
|
293
|
+
mount(): Promise<PayResult>; // mount the DOM, return the result promise
|
|
294
|
+
start(): Promise<void>; // begin discovery
|
|
295
|
+
close(reason?: string): void; // close; reason 'cancelled' rejects mount()
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
`pay(opts)` is exactly `new CheckoutModal(opts)`, then `mount()` + `start()`.
|
|
176
300
|
|
|
177
|
-
### DOM events
|
|
301
|
+
### DOM events
|
|
178
302
|
|
|
179
303
|
Bound elements dispatch bubbling `CustomEvent`s:
|
|
180
304
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
305
|
+
| event | `detail` | when |
|
|
306
|
+
|---------------------|---------------------------|------|
|
|
307
|
+
| `x402:result` | the full `PayResult` | the paid call succeeded |
|
|
308
|
+
| `x402:siwx-signed` | `{ address, network }` | the user re-entered via SIWX instead of paying (fires just before `x402:result`) |
|
|
309
|
+
| `x402:error` | `{ error: string }` | the flow failed (a **cancel does not fire this**) |
|
|
310
|
+
|
|
311
|
+
### Exports map
|
|
312
|
+
|
|
313
|
+
| import specifier | build | shape |
|
|
314
|
+
|--------------------------------------|-------|-------|
|
|
315
|
+
| `@three-ws/x402-modal` | `dist/x402-modal.mjs` | ESM, side-effect-free public API |
|
|
316
|
+
| `@three-ws/x402-modal/global` | `dist/x402.global.js` | minified IIFE, auto-binds + `window.X402` |
|
|
317
|
+
| `@three-ws/x402-modal/src` | `src/x402-modal.js` | unbundled source (advanced / debugging) |
|
|
184
318
|
|
|
185
|
-
|
|
319
|
+
The `unpkg` and `jsdelivr` package fields point at `/global`, so bare
|
|
320
|
+
`unpkg.com/@three-ws/x402-modal` also resolves to the drop-in build.
|
|
186
321
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## Configuration reference
|
|
325
|
+
|
|
326
|
+
Three ways to configure, in increasing specificity (later wins):
|
|
327
|
+
|
|
328
|
+
1. **`data-x402-*` on the `/global` `<script>` tag** — declarative, no JS.
|
|
329
|
+
2. **`configure({ … })`** — global defaults, set once at startup.
|
|
330
|
+
3. **`pay({ … })` options** — per-call overrides.
|
|
190
331
|
|
|
191
|
-
|
|
332
|
+
**Defaults are vendor-neutral.** An un-configured drop-in shows **no footer
|
|
333
|
+
attribution** (`brand: null`) and **echoes no builder code** (`builderCode:
|
|
334
|
+
null`). You opt in by setting them.
|
|
192
335
|
|
|
193
|
-
|
|
336
|
+
### `configure()` / `pay()` options
|
|
337
|
+
|
|
338
|
+
| option | type | default | description |
|
|
339
|
+
|------------------|-------------------------------|--------------------------------------|-------------|
|
|
340
|
+
| `apiOrigin` | `string \| null` | `null` → resolves to the script's origin | Origin serving the Solana `prepare`/`encode` helpers. `''` = same-origin. Ignored by the EVM path. |
|
|
341
|
+
| `brand` | `{ label?, href? } \| null` | `null` (footer hidden) | Footer attribution. `null` hides it; `{ label, href? }` renders the label (as an anchor when `href` is set). Merge-updated. |
|
|
342
|
+
| `builderCode` | `{ wallet?, service? } \| null` | `null` (no echo) | ERC-8021 self-attribution, echoed back only when the `402` declares a builder code. Codes must match `^[a-z0-9_]{1,32}$`. |
|
|
343
|
+
| `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. |
|
|
344
|
+
| `nobleHashesUrl` | `string` | `esm.sh/@noble/hashes@1.4.0/sha3` | CDN keccak module, used only for EVM SIWX sign-in. |
|
|
345
|
+
|
|
346
|
+
`configure()` merges: `configure({ brand: { label: 'X' } })` keeps the existing
|
|
347
|
+
`href`. Pass `apiOrigin: null` to reset to script-origin resolution; `brand:
|
|
348
|
+
null` to hide the footer; `builderCode: null` to switch the echo off.
|
|
194
349
|
|
|
195
350
|
```js
|
|
196
|
-
configure
|
|
197
|
-
// Origin serving the Solana prepare/encode checkout helpers. Only the Solana
|
|
198
|
-
// path uses it; the EVM path needs no backend. null → resolve from the
|
|
199
|
-
// script's own origin; '' → same-origin.
|
|
200
|
-
apiOrigin: 'https://pay.example.com',
|
|
351
|
+
import { configure } from '@three-ws/x402-modal';
|
|
201
352
|
|
|
202
|
-
|
|
353
|
+
configure({
|
|
354
|
+
apiOrigin: 'https://pay.example.com', // Solana backend
|
|
203
355
|
brand: { label: 'Powered by Acme', href: 'https://acme.com' },
|
|
204
|
-
|
|
205
|
-
// ERC-8021 builder-code self-attribution, echoed back only when the 402
|
|
206
|
-
// challenge declares a builder code. null disables the echo.
|
|
207
356
|
builderCode: { wallet: 'acme', service: 'acme_checkout' },
|
|
208
|
-
|
|
209
|
-
// Override the on-demand CDN modules (e.g. to self-host under a strict CSP).
|
|
210
|
-
solanaWeb3Url: 'https://esm.sh/@solana/web3.js@1.95.3?bundle',
|
|
211
|
-
nobleHashesUrl: 'https://esm.sh/@noble/hashes@1.4.0/sha3?bundle',
|
|
212
357
|
});
|
|
213
358
|
```
|
|
214
359
|
|
|
360
|
+
### `data-x402-*` attributes
|
|
361
|
+
|
|
362
|
+
**On the `/global` `<script>` tag** (global config):
|
|
363
|
+
|
|
364
|
+
| attribute | maps to |
|
|
365
|
+
|------------------------------|-------------------------------------------|
|
|
366
|
+
| `data-x402-api-origin` | `apiOrigin` |
|
|
367
|
+
| `data-x402-brand-label` | `brand.label` |
|
|
368
|
+
| `data-x402-brand-href` | `brand.href` |
|
|
369
|
+
| `data-x402-builder-wallet` | `builderCode.wallet` |
|
|
370
|
+
| `data-x402-builder-service` | `builderCode.service` |
|
|
371
|
+
| `data-x402-builder-disable` | `builderCode = null` (presence or `"true"`) |
|
|
372
|
+
| `data-x402-solana-web3-url` | `solanaWeb3Url` |
|
|
373
|
+
| `data-x402-noble-hashes-url` | `nobleHashesUrl` |
|
|
374
|
+
|
|
375
|
+
**On a clickable element** (per-button `pay()` options):
|
|
376
|
+
|
|
377
|
+
| attribute | type | default | description |
|
|
378
|
+
|------------------------|--------|---------------------------|-------------|
|
|
379
|
+
| `data-x402-endpoint` | string | — (**required** to bind) | the x402-protected URL |
|
|
380
|
+
| `data-x402-method` | string | `GET`, or `POST` if a body is set | HTTP method |
|
|
381
|
+
| `data-x402-body` | JSON | — | request body (parsed as JSON; falls back to raw string) |
|
|
382
|
+
| `data-x402-headers` | JSON | — | extra request headers |
|
|
383
|
+
| `data-x402-caps` | JSON | — | spending caps (see below) |
|
|
384
|
+
| `data-x402-api-origin` | string | global config | per-button Solana backend override |
|
|
385
|
+
| `data-x402-merchant` | string | header default | modal header line 1 |
|
|
386
|
+
| `data-x402-action` | string | the element's text | modal header line 2 |
|
|
387
|
+
|
|
215
388
|
### Spending caps
|
|
216
389
|
|
|
217
|
-
Caps are enforced in `localStorage`,
|
|
218
|
-
survive reloads. Amounts are **micro-USD**
|
|
219
|
-
|
|
390
|
+
Caps are enforced in `localStorage`, tracked per wallet address, bucketed by
|
|
391
|
+
rolling UTC hour and day, and survive reloads. Amounts are **micro-USD**
|
|
392
|
+
(`1_000_000` = `$1`). A breach is rejected **before** the wallet prompt; a
|
|
393
|
+
downstream failure rolls the reservation back.
|
|
220
394
|
|
|
221
395
|
```js
|
|
222
396
|
await pay({
|
|
@@ -229,78 +403,270 @@ await pay({
|
|
|
229
403
|
});
|
|
230
404
|
```
|
|
231
405
|
|
|
406
|
+
```ts
|
|
407
|
+
interface SpendingCaps {
|
|
408
|
+
maxPerCall?: number | string;
|
|
409
|
+
maxPerHour?: number | string;
|
|
410
|
+
maxPerDay?: number | string;
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
232
414
|
Stablecoins (USDC, USDT, DAI) are converted to µUSD exactly. Non-stable assets
|
|
233
|
-
pass through atomic in the browser
|
|
234
|
-
dependency-free
|
|
415
|
+
pass through atomic in the browser — no price feed is fetched, to keep the script
|
|
416
|
+
dependency-free — so **enforce those server-side**.
|
|
235
417
|
|
|
236
|
-
|
|
418
|
+
---
|
|
237
419
|
|
|
238
|
-
|
|
239
|
-
`transferWithAuthorization` in their wallet and the modal sends the signed
|
|
240
|
-
authorization straight to your merchant endpoint as `X-PAYMENT`. Your x402
|
|
241
|
-
server (and its facilitator) verify and settle it.
|
|
420
|
+
## Framework guides
|
|
242
421
|
|
|
243
|
-
|
|
244
|
-
requires RPC access and the facilitator's fee-payer. The modal expects two
|
|
245
|
-
actions at `{apiOrigin}/api/x402-checkout`:
|
|
422
|
+
### Vanilla HTML (declarative)
|
|
246
423
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
| `?action=prepare` | `{ accept, buyer }` | `{ tx_base64 }` — an unsigned/partially-signed `VersionedTransaction` |
|
|
250
|
-
| `?action=encode` | `{ accept, signed_tx_base64, resource_url, builder_code? }` | `{ x_payment }` — the base64 `X-PAYMENT` value to send to the merchant |
|
|
424
|
+
```html
|
|
425
|
+
<script type="module" src="https://unpkg.com/@three-ws/x402-modal/global"></script>
|
|
251
426
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
427
|
+
<button
|
|
428
|
+
data-x402-endpoint="/api/paid/summarize"
|
|
429
|
+
data-x402-method="POST"
|
|
430
|
+
data-x402-body='{"text":"hello"}'
|
|
431
|
+
data-x402-merchant="Acme"
|
|
432
|
+
data-x402-action="Summarize">
|
|
433
|
+
Pay & summarize
|
|
434
|
+
</button>
|
|
256
435
|
|
|
257
|
-
|
|
436
|
+
<script type="module">
|
|
437
|
+
document.querySelector('button')
|
|
438
|
+
.addEventListener('x402:result', (e) => console.log(e.detail.result));
|
|
439
|
+
</script>
|
|
440
|
+
```
|
|
258
441
|
|
|
259
|
-
|
|
260
|
-
|
|
442
|
+
### React
|
|
443
|
+
|
|
444
|
+
`pay()` is just a promise — call it from any handler. No provider, no context.
|
|
445
|
+
|
|
446
|
+
```jsx
|
|
447
|
+
import { useState, useCallback } from 'react';
|
|
448
|
+
import { pay } from '@three-ws/x402-modal';
|
|
449
|
+
|
|
450
|
+
export function PayButton() {
|
|
451
|
+
const [out, setOut] = useState(null);
|
|
452
|
+
const [error, setError] = useState(null);
|
|
453
|
+
|
|
454
|
+
const onPay = useCallback(async () => {
|
|
455
|
+
setError(null);
|
|
456
|
+
try {
|
|
457
|
+
const res = await pay({
|
|
458
|
+
endpoint: '/api/paid/summarize',
|
|
459
|
+
method: 'POST',
|
|
460
|
+
body: { text: 'hello world' },
|
|
461
|
+
merchant: 'Acme',
|
|
462
|
+
action: 'Summarize',
|
|
463
|
+
});
|
|
464
|
+
setOut(res.result);
|
|
465
|
+
} catch (err) {
|
|
466
|
+
if (err.code === 'cancelled') return; // user closed the modal
|
|
467
|
+
setError(err.message);
|
|
468
|
+
}
|
|
469
|
+
}, []);
|
|
470
|
+
|
|
471
|
+
return (
|
|
472
|
+
<>
|
|
473
|
+
<button onClick={onPay}>Pay & summarize</button>
|
|
474
|
+
{error && <p role="alert">{error}</p>}
|
|
475
|
+
{out && <pre>{JSON.stringify(out, null, 2)}</pre>}
|
|
476
|
+
</>
|
|
477
|
+
);
|
|
478
|
+
}
|
|
261
479
|
```
|
|
262
480
|
|
|
481
|
+
Set global config once in your app entry (e.g. `main.jsx`):
|
|
482
|
+
|
|
263
483
|
```js
|
|
264
|
-
import {
|
|
484
|
+
import { configure } from '@three-ws/x402-modal';
|
|
485
|
+
configure({ brand: { label: 'Powered by Acme', href: 'https://acme.com' } });
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
The same pattern works in Vue, Svelte, Solid, or any framework — import `pay`,
|
|
489
|
+
call it from an event handler, await the result.
|
|
490
|
+
|
|
491
|
+
### `<script>`-only CDN (no build, no npm)
|
|
492
|
+
|
|
493
|
+
Use `window.X402` directly. Nothing to install:
|
|
494
|
+
|
|
495
|
+
```html
|
|
496
|
+
<button id="buy">Buy article — $0.05</button>
|
|
497
|
+
<article id="content" hidden></article>
|
|
498
|
+
|
|
499
|
+
<script type="module" src="https://unpkg.com/@three-ws/x402-modal/global"></script>
|
|
500
|
+
<script>
|
|
501
|
+
document.getElementById('buy').addEventListener('click', async () => {
|
|
502
|
+
try {
|
|
503
|
+
const out = await window.X402.pay({
|
|
504
|
+
endpoint: '/api/article/42',
|
|
505
|
+
merchant: 'The Daily',
|
|
506
|
+
action: 'Unlock article',
|
|
507
|
+
});
|
|
508
|
+
const el = document.getElementById('content');
|
|
509
|
+
el.textContent = out.result.body;
|
|
510
|
+
el.hidden = false;
|
|
511
|
+
} catch (err) {
|
|
512
|
+
if (err.code !== 'cancelled') alert(err.message);
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
</script>
|
|
265
516
|
```
|
|
266
517
|
|
|
267
|
-
|
|
268
|
-
|
|
518
|
+
`window.X402` exposes `{ pay, init, configure, version }`.
|
|
519
|
+
|
|
520
|
+
More end-to-end walkthroughs — content paywall, SPA checkout, self-hosting,
|
|
521
|
+
agents with caps — live in [`docs/EXAMPLES.md`](./docs/EXAMPLES.md) and
|
|
522
|
+
[`TUTORIAL.md`](./TUTORIAL.md), with runnable code in [`examples/`](./examples).
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
|
|
526
|
+
## Wallets & assets
|
|
527
|
+
|
|
528
|
+
| Network | Wallet | Scheme | Backend? |
|
|
529
|
+
|--------------------------------------|-------------------------------------|-----------------------------------------|----------|
|
|
530
|
+
| **Base** (`eip155:8453`) | MetaMask / any `window.ethereum` | EIP-3009 `transferWithAuthorization` | **No** — fully client-side |
|
|
531
|
+
| Base Sepolia, Arbitrum, Optimism | same | EIP-3009 | No |
|
|
532
|
+
| **Solana** (`solana:*`) | Phantom | `exact` (facilitator-settled) | Yes — `prepare`/`encode` helper |
|
|
533
|
+
|
|
534
|
+
- **USDC is the default asset.** Base USDC is signed via EIP-3009 (the domain
|
|
535
|
+
version is pinned to the on-chain separator). Solana USDC settles through the
|
|
536
|
+
facilitator.
|
|
537
|
+
- **Arbitrary SPL tokens** are supported on the Solana path — the `402`'s
|
|
538
|
+
`accept.asset` is the SPL mint, and `accept.extra` carries `name` / `decimals`.
|
|
539
|
+
Only stablecoins map exactly to µUSD for caps; cap non-stable assets
|
|
540
|
+
server-side.
|
|
541
|
+
- **No wallet adapter or WalletConnect needed.** Solana uses the injected
|
|
542
|
+
Phantom provider; EVM uses the injected `window.ethereum`.
|
|
543
|
+
- **The payer never pays gas.** On EVM, EIP-3009 is a gasless signed
|
|
544
|
+
authorization the facilitator submits. On Solana the facilitator is the
|
|
545
|
+
fee-payer.
|
|
546
|
+
|
|
547
|
+
### The Solana backend
|
|
548
|
+
|
|
549
|
+
EVM/Base needs no backend. **Solana needs one tiny helper**, because a browser
|
|
550
|
+
wallet *signs* transactions but does not *build* them (building needs RPC access
|
|
551
|
+
and the facilitator's fee-payer). The modal POSTs to two actions at
|
|
552
|
+
`{apiOrigin}/api/x402-checkout`:
|
|
553
|
+
|
|
554
|
+
| action | request | response |
|
|
555
|
+
|-------------------|-------------------------------------------------------------|----------|
|
|
556
|
+
| `?action=prepare` | `{ accept, buyer }` | `{ tx_base64 }` — an unsigned/partially-signed `VersionedTransaction` |
|
|
557
|
+
| `?action=encode` | `{ accept, signed_tx_base64, resource_url, builder_code? }` | `{ x_payment }` — the base64 `X-PAYMENT` value to send to the merchant |
|
|
558
|
+
|
|
559
|
+
`apiOrigin` defaults to the origin that served the script, so when you self-host
|
|
560
|
+
both the script and this helper there is nothing to configure. The full contract,
|
|
561
|
+
a reference implementation, and a hardening checklist are in
|
|
562
|
+
[`docs/BACKEND.md`](./docs/BACKEND.md); runnable code is in
|
|
563
|
+
[`examples/server.mjs`](./examples/server.mjs).
|
|
269
564
|
|
|
270
|
-
|
|
565
|
+
---
|
|
566
|
+
|
|
567
|
+
## Theming & styling hooks
|
|
568
|
+
|
|
569
|
+
The modal injects one self-contained stylesheet (`#x402-styles`) scoped to
|
|
570
|
+
`.x402-*` classes, with full `prefers-color-scheme` light/dark support and a
|
|
571
|
+
`prefers-reduced-motion`-friendly entrance animation. To restyle, override those
|
|
572
|
+
classes after the script loads:
|
|
573
|
+
|
|
574
|
+
```css
|
|
575
|
+
.x402-modal { border-radius: 8px; } /* the card */
|
|
576
|
+
.x402-pay-btn { background: #6d28d9; } /* the primary CTA */
|
|
577
|
+
.x402-wallet-btn:hover:not(:disabled) { border-color: #6d28d9; }
|
|
578
|
+
.x402-price { letter-spacing: -0.02em; } /* the price line */
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
Key hooks: `.x402-overlay`, `.x402-modal`, `.x402-head`, `.x402-merchant`,
|
|
582
|
+
`.x402-price-row`, `.x402-network`, `.x402-step` (`.x402-active` / `.x402-done` /
|
|
583
|
+
`.x402-error`), `.x402-wallet-btn`, `.x402-pay-btn`, `.x402-close`. The two
|
|
584
|
+
header lines are set by `merchant` / `action`; the footer text/link by `brand`.
|
|
585
|
+
|
|
586
|
+
---
|
|
271
587
|
|
|
272
|
-
|
|
588
|
+
## Security
|
|
589
|
+
|
|
590
|
+
- **The modal never holds keys.** Signing happens in the user's wallet; the
|
|
273
591
|
signed payload goes to your endpoint.
|
|
274
|
-
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
-
|
|
279
|
-
|
|
280
|
-
|
|
592
|
+
- **Client-side spending caps** are advisory guardrails enforced in
|
|
593
|
+
`localStorage` — they stop *this browser* from overspending and survive
|
|
594
|
+
reloads, but a determined user can clear storage. Treat them as a UX guardrail
|
|
595
|
+
for agents and humans, and enforce authoritative limits server-side.
|
|
596
|
+
- **Idempotent retries.** A `429` from the merchant is retried with the *same*
|
|
597
|
+
signed payment — safe, because x402 settles only after the work succeeds.
|
|
598
|
+
- **No leakage.** Upstream throttle/billing text is never relayed to the buyer
|
|
599
|
+
verbatim; all endpoint-supplied strings are HTML-escaped before rendering.
|
|
600
|
+
- **CSP note.** On the Solana path the dynamic CDN import can be blocked by a
|
|
601
|
+
strict Content-Security-Policy; either allow it, repoint it via `solanaWeb3Url`
|
|
602
|
+
/ `nobleHashesUrl`, or steer users to the dependency-free Base path.
|
|
603
|
+
|
|
604
|
+
---
|
|
605
|
+
|
|
606
|
+
## FAQ & troubleshooting
|
|
281
607
|
|
|
282
|
-
|
|
608
|
+
**The modal opens but immediately errors with "Endpoint returned … but no
|
|
609
|
+
`accepts`".** Your endpoint isn't returning a valid x402 `402`. The modal expects
|
|
610
|
+
a `402` with a JSON body containing a non-empty `accepts[]` array (or a `401`
|
|
611
|
+
with a base64-JSON `payment-required` header). Pointing the modal at a free `200`
|
|
612
|
+
endpoint is treated as an error on purpose.
|
|
283
613
|
|
|
284
614
|
**Do I need a wallet adapter / WalletConnect?** No. Solana uses the injected
|
|
285
|
-
Phantom provider; EVM uses the injected `window.ethereum`.
|
|
615
|
+
Phantom provider; EVM uses the injected `window.ethereum`. If neither is present,
|
|
616
|
+
the modal tells the user to install a wallet.
|
|
286
617
|
|
|
287
|
-
**Does the payer pay gas?** On EVM,
|
|
618
|
+
**Does the payer pay gas?** No. On EVM, EIP-3009 is a gasless signed
|
|
288
619
|
authorization your facilitator submits. On Solana the facilitator is the
|
|
289
620
|
fee-payer.
|
|
290
621
|
|
|
291
|
-
**
|
|
292
|
-
|
|
293
|
-
|
|
622
|
+
**My Solana payment fails but Base works.** Base is fully client-side; Solana
|
|
623
|
+
needs the `prepare`/`encode` backend helper. Confirm `apiOrigin` resolves to a
|
|
624
|
+
host serving `/api/x402-checkout` (see [`docs/BACKEND.md`](./docs/BACKEND.md)),
|
|
625
|
+
and that a strict CSP isn't blocking the `@solana/web3.js` CDN import — repoint
|
|
626
|
+
it with `solanaWeb3Url` if so.
|
|
627
|
+
|
|
628
|
+
**`pay()` rejected — how do I tell a cancel from a real error?** A user-closed
|
|
629
|
+
modal rejects with `err.code === 'cancelled'`; everything else is a genuine
|
|
630
|
+
failure. The declarative path never fires `x402:error` on a cancel.
|
|
631
|
+
|
|
632
|
+
**A payment got rejected before the wallet even opened.** A spending cap was hit.
|
|
633
|
+
Caps are per-wallet in `localStorage`, bucketed by UTC hour/day. Raise the cap,
|
|
634
|
+
or clear the bucket by waiting out the window.
|
|
635
|
+
|
|
636
|
+
**Can I theme it / does it support dark mode?** Yes — see
|
|
637
|
+
[Theming](#theming--styling-hooks). It honors `prefers-color-scheme` out of the
|
|
638
|
+
box and exposes `.x402-*` classes for overrides.
|
|
639
|
+
|
|
640
|
+
**Does it work in React/Vue/Svelte?** Yes — it's framework-agnostic. Import
|
|
641
|
+
`pay()` and call it from a handler, or drop the `/global` script and use `data-*`
|
|
642
|
+
buttons. See [Framework guides](#framework-guides).
|
|
294
643
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
644
|
+
---
|
|
645
|
+
|
|
646
|
+
## Related packages
|
|
647
|
+
|
|
648
|
+
- **[`@three-ws/x402-payment-modal`](https://www.npmjs.com/package/@three-ws/x402-payment-modal)**
|
|
649
|
+
— a sibling package. If you arrived looking for that one, note the difference:
|
|
650
|
+
this package (`@three-ws/x402-modal`) is the **dependency-free, bundler-free
|
|
651
|
+
drop-in** focused on the smallest possible client (one `<script>` tag,
|
|
652
|
+
`window.X402`, `data-*` binding). Pick `x402-modal` when you want a CDN drop-in
|
|
653
|
+
with zero install.
|
|
654
|
+
- **[x402 spec](https://x402.org)** — the open `402 Payment Required` payment
|
|
655
|
+
protocol this modal implements.
|
|
656
|
+
|
|
657
|
+
---
|
|
658
|
+
|
|
659
|
+
## Documentation
|
|
298
660
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
661
|
+
- [`docs/PROTOCOL.md`](./docs/PROTOCOL.md) — the four-step flow in detail.
|
|
662
|
+
- [`docs/CONFIGURATION.md`](./docs/CONFIGURATION.md) — every option and attribute.
|
|
663
|
+
- [`docs/BACKEND.md`](./docs/BACKEND.md) — the Solana `prepare`/`encode` helper.
|
|
664
|
+
- [`docs/EXAMPLES.md`](./docs/EXAMPLES.md) — runnable recipes per framework.
|
|
665
|
+
- [`TUTORIAL.md`](./TUTORIAL.md) — hands-on, copy-paste walkthroughs.
|
|
666
|
+
- [`CONTRIBUTING.md`](./CONTRIBUTING.md) — build, test, and release.
|
|
667
|
+
|
|
668
|
+
---
|
|
302
669
|
|
|
303
670
|
## License
|
|
304
671
|
|
|
305
|
-
|
|
306
|
-
platform for building, animating, rigging, and monetizing 3D AI agents.
|
|
672
|
+
Proprietary — Copyright (c) 2026 nirholas. All Rights Reserved. Unauthorized use, copying, modification, or distribution is prohibited. See [LICENSE](./LICENSE).
|