@alleyboss/micropay-solana-x402-paywall 3.3.15 → 3.5.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/README.md CHANGED
@@ -21,6 +21,7 @@ npm install @alleyboss/micropay-solana-x402-paywall @x402/core @x402/svm @solana
21
21
 
22
22
  | Feature | Description | Status |
23
23
  |---------|-------------|--------|
24
+ | 🔥 **x402Fetch** | Drop-in `fetch()` replacement with auto-payment | ✅ **NEW in v3.4** |
24
25
  | 💰 **SOL & USDC** | Native SOL and SPL tokens (USDC, USDT) | ✅ Verified by `@x402/svm` |
25
26
  | 🔐 **x402 Protocol** | Full HTTP 402 compliance | ✅ Powered by `@x402/core` |
26
27
  | 🌐 **PayAI Format** | Multi-chain payment format support | ✅ Built-in |
@@ -33,6 +34,96 @@ npm install @alleyboss/micropay-solana-x402-paywall @x402/core @x402/svm @solana
33
34
  | 📦 **Versioned Tx** | v0 Transaction support | ✅ Native (x402 SDK) |
34
35
  | 🌳 **Tree-Shakeable** | Modular exports | ✅ Built-in |
35
36
 
37
+ ## 🔥 x402Fetch — The Killer Feature (NEW in v3.4)
38
+
39
+ Replace `fetch()` with `x402Fetch()` and **402 responses are handled automatically**.
40
+
41
+ ```typescript
42
+ import { createX402Fetch } from '@alleyboss/micropay-solana-x402-paywall/fetch';
43
+ import { useWallet } from '@solana/wallet-adapter-react';
44
+
45
+ // Create configured fetch instance
46
+ const x402Fetch = createX402Fetch({
47
+ wallet: useWallet(), // Browser wallet adapter
48
+ network: 'mainnet-beta',
49
+ onPaymentRequired: async (req) => {
50
+ return confirm(`Pay ${req.amount} lamports to ${req.payTo}?`);
51
+ },
52
+ });
53
+
54
+ // Use it like fetch — that's it!
55
+ const response = await x402Fetch('https://api.example.com/premium-data');
56
+ const data = await response.json();
57
+ ```
58
+
59
+ ### Server-Side / AI Agent Usage
60
+
61
+ ```typescript
62
+ import { createX402Fetch } from '@alleyboss/micropay-solana-x402-paywall/fetch';
63
+ import { Keypair } from '@solana/web3.js';
64
+ import bs58 from 'bs58';
65
+
66
+ const agentKeypair = Keypair.fromSecretKey(
67
+ bs58.decode(process.env.AGENT_PRIVATE_KEY!)
68
+ );
69
+
70
+ const x402Fetch = createX402Fetch({
71
+ wallet: agentKeypair, // Server-side keypair
72
+ network: 'mainnet-beta',
73
+ });
74
+
75
+ // Autonomous payment — no user interaction needed
76
+ const response = await x402Fetch('https://api.example.com/ai-data');
77
+ ```
78
+
79
+ ### Error Handling
80
+
81
+ ```typescript
82
+ import { X402PaymentError, isUserRejection } from '@alleyboss/micropay-solana-x402-paywall/fetch';
83
+
84
+ try {
85
+ await x402Fetch('/api/premium');
86
+ } catch (error) {
87
+ if (isUserRejection(error)) {
88
+ console.log('User declined payment');
89
+ } else if (error instanceof X402PaymentError) {
90
+ console.log('Payment failed:', error.code);
91
+ // Codes: USER_REJECTED, INSUFFICIENT_BALANCE, TRANSACTION_FAILED, etc.
92
+ }
93
+ }
94
+ ```
95
+
96
+ ### Security Configuration (v3.4.1)
97
+
98
+ Protect your wallet from malicious 402 responses:
99
+
100
+ ```typescript
101
+ const x402Fetch = createX402Fetch({
102
+ wallet,
103
+ network: 'mainnet-beta',
104
+
105
+ // 🔒 Critical: Maximum payment per request (prevents wallet drain)
106
+ maxPaymentPerRequest: BigInt(10_000_000), // Max 0.01 SOL
107
+
108
+ // 🔒 Critical: Whitelist of allowed recipients (prevents phishing)
109
+ allowedRecipients: ['7fPjN...', 'ABC123...'],
110
+
111
+ // ⚡ UX: Transaction speed (processed=fast, finalized=safe)
112
+ commitment: 'confirmed', // 'processed' | 'confirmed' | 'finalized'
113
+
114
+ // 🛡️ Rate limiting (prevents infinite loops)
115
+ rateLimit: {
116
+ maxPayments: 10, // Max 10 payments
117
+ windowMs: 60_000, // Per minute
118
+ },
119
+ });
120
+ ```
121
+
122
+ **Security Error Codes:**
123
+ - `AMOUNT_EXCEEDS_LIMIT` — Payment blocked: amount exceeds `maxPaymentPerRequest`
124
+ - `RECIPIENT_NOT_ALLOWED` — Payment blocked: recipient not in whitelist
125
+ - `RATE_LIMIT_EXCEEDED` — Payment blocked: too many payments in time window
126
+
36
127
  ## 📦 Quick Example (Express.js)
37
128
 
38
129
  ```typescript
@@ -92,6 +183,9 @@ export const GET = withMicropay(handler, {
92
183
  Import only what you need:
93
184
 
94
185
  ```typescript
186
+ // 🔥 x402Fetch - Drop-in fetch replacement (NEW!)
187
+ import { createX402Fetch } from '@alleyboss/micropay-solana-x402-paywall/fetch';
188
+
95
189
  // Express Middleware
96
190
  import { x402Middleware } from '@alleyboss/micropay-solana-x402-paywall/express';
97
191
 
@@ -0,0 +1,421 @@
1
+ 'use strict';
2
+
3
+ var web3_js = require('@solana/web3.js');
4
+
5
+ // src/fetch/x402Fetch.ts
6
+
7
+ // src/fetch/types.ts
8
+ var X402ErrorCode = {
9
+ /** User rejected the payment */
10
+ USER_REJECTED: "USER_REJECTED",
11
+ /** Insufficient wallet balance */
12
+ INSUFFICIENT_BALANCE: "INSUFFICIENT_BALANCE",
13
+ /** Transaction failed on-chain */
14
+ TRANSACTION_FAILED: "TRANSACTION_FAILED",
15
+ /** Payment verification failed */
16
+ VERIFICATION_FAILED: "VERIFICATION_FAILED",
17
+ /** Network/RPC error */
18
+ NETWORK_ERROR: "NETWORK_ERROR",
19
+ /** Invalid 402 response format */
20
+ INVALID_402_RESPONSE: "INVALID_402_RESPONSE",
21
+ /** Payment timeout */
22
+ TIMEOUT: "TIMEOUT",
23
+ /** Wallet not connected */
24
+ WALLET_NOT_CONNECTED: "WALLET_NOT_CONNECTED",
25
+ /** Payment amount exceeds maxPaymentPerRequest */
26
+ AMOUNT_EXCEEDS_LIMIT: "AMOUNT_EXCEEDS_LIMIT",
27
+ /** Recipient address not in allowedRecipients whitelist */
28
+ RECIPIENT_NOT_ALLOWED: "RECIPIENT_NOT_ALLOWED",
29
+ /** Rate limit exceeded */
30
+ RATE_LIMIT_EXCEEDED: "RATE_LIMIT_EXCEEDED"
31
+ };
32
+
33
+ // src/fetch/errors.ts
34
+ var X402PaymentError = class _X402PaymentError extends Error {
35
+ constructor(message, code, requirements, cause) {
36
+ super(message);
37
+ this.code = code;
38
+ this.requirements = requirements;
39
+ this.cause = cause;
40
+ if (Error.captureStackTrace) {
41
+ Error.captureStackTrace(this, _X402PaymentError);
42
+ }
43
+ }
44
+ name = "X402PaymentError";
45
+ /**
46
+ * Check if error is retryable
47
+ */
48
+ get isRetryable() {
49
+ const retryableCodes = [
50
+ X402ErrorCode.NETWORK_ERROR,
51
+ X402ErrorCode.TIMEOUT,
52
+ X402ErrorCode.TRANSACTION_FAILED
53
+ ];
54
+ return retryableCodes.includes(this.code);
55
+ }
56
+ /**
57
+ * Convert to JSON-serializable object
58
+ */
59
+ toJSON() {
60
+ return {
61
+ name: this.name,
62
+ message: this.message,
63
+ code: this.code,
64
+ requirements: this.requirements,
65
+ isRetryable: this.isRetryable
66
+ };
67
+ }
68
+ };
69
+ function userRejectedError(requirements) {
70
+ return new X402PaymentError(
71
+ "User rejected the payment request",
72
+ X402ErrorCode.USER_REJECTED,
73
+ requirements
74
+ );
75
+ }
76
+ function insufficientBalanceError(requirements, balance) {
77
+ return new X402PaymentError(
78
+ `Insufficient balance: have ${balance} lamports, need ${requirements.amount}`,
79
+ X402ErrorCode.INSUFFICIENT_BALANCE,
80
+ requirements
81
+ );
82
+ }
83
+ function transactionFailedError(requirements, cause) {
84
+ return new X402PaymentError(
85
+ `Transaction failed: ${cause?.message ?? "Unknown error"}`,
86
+ X402ErrorCode.TRANSACTION_FAILED,
87
+ requirements,
88
+ cause
89
+ );
90
+ }
91
+ function verificationFailedError(requirements, reason) {
92
+ return new X402PaymentError(
93
+ `Payment verification failed: ${reason ?? "Unknown reason"}`,
94
+ X402ErrorCode.VERIFICATION_FAILED,
95
+ requirements
96
+ );
97
+ }
98
+ function networkError(cause) {
99
+ return new X402PaymentError(
100
+ `Network error: ${cause?.message ?? "Connection failed"}`,
101
+ X402ErrorCode.NETWORK_ERROR,
102
+ void 0,
103
+ cause
104
+ );
105
+ }
106
+ function invalid402ResponseError(details) {
107
+ return new X402PaymentError(
108
+ `Invalid 402 response: ${details ?? "Missing or malformed payment requirements"}`,
109
+ X402ErrorCode.INVALID_402_RESPONSE
110
+ );
111
+ }
112
+ function timeoutError(requirements) {
113
+ return new X402PaymentError(
114
+ "Payment flow timed out",
115
+ X402ErrorCode.TIMEOUT,
116
+ requirements
117
+ );
118
+ }
119
+ function walletNotConnectedError() {
120
+ return new X402PaymentError(
121
+ "Wallet is not connected",
122
+ X402ErrorCode.WALLET_NOT_CONNECTED
123
+ );
124
+ }
125
+ function amountExceedsLimitError(requirements, limit) {
126
+ return new X402PaymentError(
127
+ `Payment amount ${requirements.amount} exceeds limit of ${limit} lamports`,
128
+ X402ErrorCode.AMOUNT_EXCEEDS_LIMIT,
129
+ requirements
130
+ );
131
+ }
132
+ function recipientNotAllowedError(requirements, recipient) {
133
+ return new X402PaymentError(
134
+ `Recipient ${recipient} is not in the allowed recipients list`,
135
+ X402ErrorCode.RECIPIENT_NOT_ALLOWED,
136
+ requirements
137
+ );
138
+ }
139
+ function rateLimitExceededError(limit, windowMs) {
140
+ return new X402PaymentError(
141
+ `Rate limit exceeded: max ${limit} payments per ${windowMs / 1e3}s`,
142
+ X402ErrorCode.RATE_LIMIT_EXCEEDED
143
+ );
144
+ }
145
+ function isX402PaymentError(error) {
146
+ return error instanceof X402PaymentError;
147
+ }
148
+ function isUserRejection(error) {
149
+ return isX402PaymentError(error) && error.code === X402ErrorCode.USER_REJECTED;
150
+ }
151
+
152
+ // src/shared/constants.ts
153
+ var RPC_ENDPOINTS = {
154
+ "mainnet-beta": "https://api.mainnet-beta.solana.com",
155
+ "devnet": "https://api.devnet.solana.com",
156
+ "testnet": "https://api.testnet.solana.com"
157
+ };
158
+ var DEFAULT_CONFIRMATION_TIMEOUT = 3e4;
159
+ var DEFAULT_MAX_RETRIES = 3;
160
+ var DEFAULT_RATE_LIMIT_WINDOW_MS = 6e4;
161
+ var DEFAULT_RATE_LIMIT_MAX_PAYMENTS = 10;
162
+
163
+ // src/fetch/x402Fetch.ts
164
+ function isKeypair(wallet) {
165
+ return wallet instanceof web3_js.Keypair;
166
+ }
167
+ function isWalletConnected(wallet) {
168
+ if (isKeypair(wallet)) return true;
169
+ return wallet.connected && wallet.publicKey != null;
170
+ }
171
+ function getPublicKey(wallet) {
172
+ if (isKeypair(wallet)) return wallet.publicKey;
173
+ if (!wallet.publicKey) throw walletNotConnectedError();
174
+ return wallet.publicKey;
175
+ }
176
+ function parse402Response(response) {
177
+ const x402Header = response.headers.get("X-Payment-Requirements");
178
+ if (x402Header) {
179
+ try {
180
+ const parsed = JSON.parse(x402Header);
181
+ return {
182
+ payTo: parsed.payTo ?? parsed.recipient,
183
+ amount: String(parsed.amount),
184
+ asset: parsed.asset ?? "SOL",
185
+ network: parsed.network ?? "solana-mainnet",
186
+ description: parsed.description,
187
+ resource: parsed.resource,
188
+ maxAge: parsed.maxAge
189
+ };
190
+ } catch {
191
+ throw invalid402ResponseError("Invalid X-Payment-Requirements header");
192
+ }
193
+ }
194
+ const wwwAuth = response.headers.get("WWW-Authenticate");
195
+ if (wwwAuth?.startsWith("X402")) {
196
+ try {
197
+ const base64Part = wwwAuth.slice(5).trim();
198
+ const jsonStr = atob(base64Part);
199
+ const parsed = JSON.parse(jsonStr);
200
+ return {
201
+ payTo: parsed.payTo ?? parsed.recipient,
202
+ amount: String(parsed.amount),
203
+ asset: parsed.asset ?? "SOL",
204
+ network: parsed.network ?? "solana-mainnet",
205
+ description: parsed.description,
206
+ resource: parsed.resource,
207
+ maxAge: parsed.maxAge
208
+ };
209
+ } catch {
210
+ throw invalid402ResponseError("Invalid WWW-Authenticate header");
211
+ }
212
+ }
213
+ throw invalid402ResponseError("No payment requirements found in 402 response");
214
+ }
215
+ function buildPaymentHeader(signature) {
216
+ const payload = {
217
+ x402Version: 2,
218
+ scheme: "exact",
219
+ payload: { signature }
220
+ };
221
+ return `X402 ${btoa(JSON.stringify(payload))}`;
222
+ }
223
+ function createX402Fetch(config) {
224
+ const {
225
+ wallet,
226
+ network = "mainnet-beta",
227
+ connection: providedConnection,
228
+ facilitatorUrl: _facilitatorUrl,
229
+ // Reserved for future facilitator integration
230
+ onPaymentRequired,
231
+ onPaymentSuccess,
232
+ onPaymentError,
233
+ priorityFee,
234
+ maxRetries = DEFAULT_MAX_RETRIES,
235
+ timeout = DEFAULT_CONFIRMATION_TIMEOUT,
236
+ // Security options
237
+ maxPaymentPerRequest,
238
+ allowedRecipients,
239
+ // UX options
240
+ commitment = "confirmed",
241
+ rateLimit
242
+ } = config;
243
+ const paymentTimestamps = [];
244
+ const rateLimitMax = rateLimit?.maxPayments ?? DEFAULT_RATE_LIMIT_MAX_PAYMENTS;
245
+ const rateLimitWindow = rateLimit?.windowMs ?? DEFAULT_RATE_LIMIT_WINDOW_MS;
246
+ function checkRateLimit() {
247
+ const now = Date.now();
248
+ while (paymentTimestamps.length > 0 && paymentTimestamps[0] < now - rateLimitWindow) {
249
+ paymentTimestamps.shift();
250
+ }
251
+ if (paymentTimestamps.length >= rateLimitMax) {
252
+ throw rateLimitExceededError(rateLimitMax, rateLimitWindow);
253
+ }
254
+ }
255
+ function recordPayment() {
256
+ paymentTimestamps.push(Date.now());
257
+ }
258
+ function validateSecurityRequirements(requirements) {
259
+ const amountLamports = BigInt(requirements.amount);
260
+ if (maxPaymentPerRequest !== void 0 && amountLamports > maxPaymentPerRequest) {
261
+ throw amountExceedsLimitError(requirements, maxPaymentPerRequest);
262
+ }
263
+ if (allowedRecipients !== void 0 && allowedRecipients.length > 0) {
264
+ if (!allowedRecipients.includes(requirements.payTo)) {
265
+ throw recipientNotAllowedError(requirements, requirements.payTo);
266
+ }
267
+ }
268
+ }
269
+ const connection = providedConnection ?? new web3_js.Connection(RPC_ENDPOINTS[network], {
270
+ commitment
271
+ });
272
+ async function executePayment(requirements) {
273
+ const payer = getPublicKey(wallet);
274
+ const recipient = new web3_js.PublicKey(requirements.payTo);
275
+ const amountLamports = BigInt(requirements.amount);
276
+ const balance = await connection.getBalance(payer);
277
+ if (BigInt(balance) < amountLamports) {
278
+ throw insufficientBalanceError(requirements, BigInt(balance));
279
+ }
280
+ const instructions = [];
281
+ if (priorityFee?.enabled) {
282
+ instructions.push(
283
+ web3_js.ComputeBudgetProgram.setComputeUnitPrice({
284
+ microLamports: priorityFee.microLamports ?? 5e3
285
+ })
286
+ );
287
+ }
288
+ instructions.push(
289
+ web3_js.SystemProgram.transfer({
290
+ fromPubkey: payer,
291
+ toPubkey: recipient,
292
+ lamports: amountLamports
293
+ })
294
+ );
295
+ const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
296
+ const messageV0 = new web3_js.TransactionMessage({
297
+ payerKey: payer,
298
+ recentBlockhash: blockhash,
299
+ instructions
300
+ }).compileToV0Message();
301
+ const tx = new web3_js.VersionedTransaction(messageV0);
302
+ if (isKeypair(wallet)) {
303
+ tx.sign([wallet]);
304
+ } else {
305
+ const signerWallet = wallet;
306
+ if (!signerWallet.signTransaction) {
307
+ throw new X402PaymentError(
308
+ "Wallet does not support transaction signing. Use a SignerWalletAdapter.",
309
+ "WALLET_NOT_CONNECTED"
310
+ );
311
+ }
312
+ const signedTx = await signerWallet.signTransaction(tx);
313
+ if (signedTx.signatures[0]) {
314
+ tx.signatures[0] = signedTx.signatures[0];
315
+ }
316
+ }
317
+ const signature = await connection.sendTransaction(tx, {
318
+ maxRetries
319
+ });
320
+ await connection.confirmTransaction({
321
+ signature,
322
+ blockhash,
323
+ lastValidBlockHeight
324
+ }, commitment);
325
+ return signature;
326
+ }
327
+ async function x402Fetch(input, init) {
328
+ const { skipPayment, paymentOverride, ...fetchInit } = init ?? {};
329
+ let response;
330
+ try {
331
+ response = await fetch(input, fetchInit);
332
+ } catch (error) {
333
+ throw networkError(error instanceof Error ? error : void 0);
334
+ }
335
+ if (response.status !== 402) {
336
+ return response;
337
+ }
338
+ if (skipPayment) {
339
+ return response;
340
+ }
341
+ if (!isWalletConnected(wallet)) {
342
+ throw walletNotConnectedError();
343
+ }
344
+ let requirements;
345
+ try {
346
+ requirements = parse402Response(response);
347
+ if (paymentOverride) {
348
+ requirements = { ...requirements, ...paymentOverride };
349
+ }
350
+ } catch (error) {
351
+ if (error instanceof X402PaymentError) throw error;
352
+ throw invalid402ResponseError(error instanceof Error ? error.message : void 0);
353
+ }
354
+ validateSecurityRequirements(requirements);
355
+ checkRateLimit();
356
+ if (onPaymentRequired) {
357
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
358
+ const shouldProceed = await onPaymentRequired(requirements, url);
359
+ if (!shouldProceed) {
360
+ throw userRejectedError(requirements);
361
+ }
362
+ }
363
+ let signature;
364
+ try {
365
+ const paymentPromise = executePayment(requirements);
366
+ const timeoutPromise = new Promise((_, reject) => {
367
+ setTimeout(() => reject(timeoutError(requirements)), timeout);
368
+ });
369
+ signature = await Promise.race([paymentPromise, timeoutPromise]);
370
+ recordPayment();
371
+ if (onPaymentSuccess) {
372
+ await onPaymentSuccess(signature, requirements);
373
+ }
374
+ } catch (error) {
375
+ if (error instanceof X402PaymentError) {
376
+ if (onPaymentError) {
377
+ await onPaymentError(error, requirements);
378
+ }
379
+ throw error;
380
+ }
381
+ const wrappedError = transactionFailedError(
382
+ requirements,
383
+ error instanceof Error ? error : void 0
384
+ );
385
+ if (onPaymentError) {
386
+ await onPaymentError(wrappedError, requirements);
387
+ }
388
+ throw wrappedError;
389
+ }
390
+ const retryHeaders = new Headers(fetchInit?.headers);
391
+ retryHeaders.set("Authorization", buildPaymentHeader(signature));
392
+ try {
393
+ return await fetch(input, {
394
+ ...fetchInit,
395
+ headers: retryHeaders
396
+ });
397
+ } catch (error) {
398
+ throw networkError(error instanceof Error ? error : void 0);
399
+ }
400
+ }
401
+ return x402Fetch;
402
+ }
403
+
404
+ exports.X402ErrorCode = X402ErrorCode;
405
+ exports.X402PaymentError = X402PaymentError;
406
+ exports.amountExceedsLimitError = amountExceedsLimitError;
407
+ exports.buildPaymentHeader = buildPaymentHeader;
408
+ exports.createX402Fetch = createX402Fetch;
409
+ exports.insufficientBalanceError = insufficientBalanceError;
410
+ exports.invalid402ResponseError = invalid402ResponseError;
411
+ exports.isUserRejection = isUserRejection;
412
+ exports.isX402PaymentError = isX402PaymentError;
413
+ exports.networkError = networkError;
414
+ exports.parsePaymentRequirements = parse402Response;
415
+ exports.rateLimitExceededError = rateLimitExceededError;
416
+ exports.recipientNotAllowedError = recipientNotAllowedError;
417
+ exports.timeoutError = timeoutError;
418
+ exports.transactionFailedError = transactionFailedError;
419
+ exports.userRejectedError = userRejectedError;
420
+ exports.verificationFailedError = verificationFailedError;
421
+ exports.walletNotConnectedError = walletNotConnectedError;