@dexterai/x402 1.6.0 → 1.6.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.
@@ -315,8 +315,9 @@ declare function x402Middleware(config: X402MiddlewareConfig): RequestHandler;
315
315
  * when a browser (Accept: text/html) receives a 402 Payment Required response.
316
316
  * API clients continue to receive the standard JSON response unchanged.
317
317
  *
318
- * This is a protocol-level concern: the 402 response is part of x402, and
319
- * providing a human-readable payment page for browsers is the natural UX.
318
+ * Includes a functional "Pay" button using the Solana Wallet Standard --
319
+ * detects Phantom/Solflare/Backpack, constructs a USDC transfer, signs,
320
+ * and submits the payment automatically.
320
321
  *
321
322
  * @example
322
323
  * ```typescript
@@ -325,7 +326,7 @@ declare function x402Middleware(config: X402MiddlewareConfig): RequestHandler;
325
326
  *
326
327
  * const app = express();
327
328
  * app.use(express.json());
328
- * app.use(x402BrowserSupport()); // one line -- all 402s render HTML for browsers
329
+ * app.use(x402BrowserSupport());
329
330
  *
330
331
  * app.post('/api/data',
331
332
  * x402Middleware({ payTo: '...', amount: '0.01' }),
@@ -336,29 +337,18 @@ declare function x402Middleware(config: X402MiddlewareConfig): RequestHandler;
336
337
 
337
338
  /**
338
339
  * Configuration for x402BrowserSupport middleware.
339
- * All fields are optional -- sensible defaults are used.
340
340
  */
341
341
  interface X402BrowserSupportConfig {
342
- /**
343
- * Custom title shown on the paywall page.
344
- * @default 'Payment Required'
345
- */
342
+ /** Custom title shown on the paywall page. @default 'Payment Required' */
346
343
  title?: string;
347
- /**
348
- * Custom branding text shown at the bottom.
349
- * @default 'Powered by x402'
350
- */
344
+ /** Custom branding text. @default 'Powered by x402' */
351
345
  branding?: string;
352
- /**
353
- * URL to link for SDK/documentation.
354
- * @default 'https://x402.org'
355
- */
346
+ /** URL to link for SDK/documentation. @default 'https://x402.org' */
356
347
  sdkUrl?: string;
357
- /**
358
- * Whether to include the request method and path on the page.
359
- * @default true
360
- */
348
+ /** Whether to include the request method and path. @default true */
361
349
  showEndpoint?: boolean;
350
+ /** Solana RPC URL for wallet transactions. @default 'https://api.dexter.cash/api/solana/rpc' */
351
+ rpcUrl?: string;
362
352
  }
363
353
  /**
364
354
  * Create x402 browser support middleware.
@@ -369,9 +359,6 @@ interface X402BrowserSupportConfig {
369
359
  * instead of raw JSON.
370
360
  *
371
361
  * API clients are completely unaffected -- they receive normal JSON.
372
- *
373
- * @param config - Optional configuration
374
- * @returns Express middleware
375
362
  */
376
363
  declare function x402BrowserSupport(config?: X402BrowserSupportConfig): RequestHandler;
377
364
 
@@ -315,8 +315,9 @@ declare function x402Middleware(config: X402MiddlewareConfig): RequestHandler;
315
315
  * when a browser (Accept: text/html) receives a 402 Payment Required response.
316
316
  * API clients continue to receive the standard JSON response unchanged.
317
317
  *
318
- * This is a protocol-level concern: the 402 response is part of x402, and
319
- * providing a human-readable payment page for browsers is the natural UX.
318
+ * Includes a functional "Pay" button using the Solana Wallet Standard --
319
+ * detects Phantom/Solflare/Backpack, constructs a USDC transfer, signs,
320
+ * and submits the payment automatically.
320
321
  *
321
322
  * @example
322
323
  * ```typescript
@@ -325,7 +326,7 @@ declare function x402Middleware(config: X402MiddlewareConfig): RequestHandler;
325
326
  *
326
327
  * const app = express();
327
328
  * app.use(express.json());
328
- * app.use(x402BrowserSupport()); // one line -- all 402s render HTML for browsers
329
+ * app.use(x402BrowserSupport());
329
330
  *
330
331
  * app.post('/api/data',
331
332
  * x402Middleware({ payTo: '...', amount: '0.01' }),
@@ -336,29 +337,18 @@ declare function x402Middleware(config: X402MiddlewareConfig): RequestHandler;
336
337
 
337
338
  /**
338
339
  * Configuration for x402BrowserSupport middleware.
339
- * All fields are optional -- sensible defaults are used.
340
340
  */
341
341
  interface X402BrowserSupportConfig {
342
- /**
343
- * Custom title shown on the paywall page.
344
- * @default 'Payment Required'
345
- */
342
+ /** Custom title shown on the paywall page. @default 'Payment Required' */
346
343
  title?: string;
347
- /**
348
- * Custom branding text shown at the bottom.
349
- * @default 'Powered by x402'
350
- */
344
+ /** Custom branding text. @default 'Powered by x402' */
351
345
  branding?: string;
352
- /**
353
- * URL to link for SDK/documentation.
354
- * @default 'https://x402.org'
355
- */
346
+ /** URL to link for SDK/documentation. @default 'https://x402.org' */
356
347
  sdkUrl?: string;
357
- /**
358
- * Whether to include the request method and path on the page.
359
- * @default true
360
- */
348
+ /** Whether to include the request method and path. @default true */
361
349
  showEndpoint?: boolean;
350
+ /** Solana RPC URL for wallet transactions. @default 'https://api.dexter.cash/api/solana/rpc' */
351
+ rpcUrl?: string;
362
352
  }
363
353
  /**
364
354
  * Create x402 browser support middleware.
@@ -369,9 +359,6 @@ interface X402BrowserSupportConfig {
369
359
  * instead of raw JSON.
370
360
  *
371
361
  * API clients are completely unaffected -- they receive normal JSON.
372
- *
373
- * @param config - Optional configuration
374
- * @returns Express middleware
375
362
  */
376
363
  declare function x402BrowserSupport(config?: X402BrowserSupportConfig): RequestHandler;
377
364
 
@@ -358,7 +358,242 @@ function x402Middleware(config) {
358
358
  }
359
359
 
360
360
  // src/server/browser-support.ts
361
- function generatePaywallHtml(paymentRequiredHeader, requestUrl, method, config) {
361
+ var DEXTER_CREST_SVG = `<svg width="36" height="36" viewBox="0 0 300 300" xmlns="http://www.w3.org/2000/svg"><g><path fill="#F2681A" d="m324.93,313.11c-115.5,0-231,0-350,0l350,0z"/><path fill="#FDFAF5" d="m230.43,50.62c1.1.85 2.19 1.7 3.32 2.57 6.02 4.8 11.77 9.88 17.46 15.07.92.84.92.84 1.86 1.69 1.82 1.69 3.59 3.42 5.35 5.16.61.56 1.22 1.13 1.84 1.71 5.66 5.76 6.18 10.43 6.13 18.3.02 1.16.04 2.32.06 3.52.06 3.83.06 7.65.07 11.48.02 2.68.05 5.35.08 8.03.05 5.6.09 11.21.1 16.81.02 7.15.09 14.31.17 21.46.06 5.53.1 11.05.13 16.58.02 2.64.04 5.27.07 7.91.18 17.58.12 32.82-11.24 47.32-7.35 7.27-16.54 12.06-25.42 17.22-1.97 1.16-3.94 2.33-5.91 3.49-7.16 4.24-14.34 8.44-21.53 12.62-4.8 2.79-9.59 5.6-14.38 8.42-1.25.73-2.5 1.47-3.79 2.23-2.32 1.36-4.64 2.73-6.96 4.1-27.47 16.09-27.47 16.09-42.16 12.93-8.06-2.28-14.94-5.82-22.16-10.02-1.17-.67-2.34-1.34-3.54-2.04-24.55-14.25-43.58-27.03-51.9-55.58-1.07-4.58-1.54-8.92-1.52-13.61.28-9.5.28-9.5-3.3-17.97-1.81-1.49-3.68-2.92-5.59-4.28-9.19-7.06-12.7-20.03-14.18-31.06-.54-5.77-.55-11.56-.6-17.35-.03-1.32-.07-2.63-.1-3.99-.01-1.26-.02-2.53-.03-3.83-.02-1.15-.03-2.29-.05-3.47.72-4.02 1.94-5.36 5.21-7.74 2.89-.53 2.89-.53 6.07-.46 1.71.02 1.71.02 3.46.05 1.19.04 2.37.08 3.59.12 1.2.02 2.41.04 3.65.06 2.97.05 5.93.13 8.9.23.14-1.35.29-2.7.43-4.08.63-5 1.78-9.74 3.14-14.58.22-.79.43-1.59.66-2.4.53-1.92 1.06-3.84 1.6-5.76-1.55-.45-1.55-.45-3.13-.9-9.52-3.52-17.1-10.95-21.37-20.1-3.81-9.26-3.87-20.34-.29-29.68 6.49-13.99 16.36-23.23 30.66-29.01 49.81-17.69 115.79 8.35 155.13 38.85z"/><path fill="#F2671A" d="m142.93,22.62c.86.19 1.73.39 2.62.59 36.12 8.21 68.79 24.98 95.38 50.75 1.02.98 2.03 1.97 3.08 2.98 10.84 10.66 10.84 10.66 11.05 14.62-2.06 3.55-5.44 4.18-9.17 5.3-.79.25-1.59.49-2.41.75-28.13 8.43-60.95 6.37-87.13-7.16-.86-.49-1.71-.97-2.6-1.48-7.37-4.05-12.59-3.36-20.59-1.54-22.76 4-48.47 1.53-68.69-9.74-4.88-3.88-8.23-8.29-10.21-14.22-.93-10.38-.67-18.44 5.83-26.83 19.57-23.38 55.99-20.36 82.83-14z"/><path fill="#F16619" d="m44.93,129.12c27.36-.03 54.72-.05 82.08-.06 12.7-.01 25.41-.01 38.11-.03 11.07-.01 22.14-.02 33.2-.02 5.86 0 11.73-.01 17.59-.01 5.51-.01 11.03-.01 16.54-.01 2.03 0 4.06 0 6.09-.01 2.76-.01 5.52 0 8.28 0 .81 0 1.63-.01 2.47-.01 5.51.02 5.51.02 6.81 1.32.22 3.43.22 3.43 0 7-2.75 2.75-3.42 2.66-7.15 2.82-1.41.07-1.41.07-2.85.14-1.47.05-1.47.05-2.98.11-1.49.07-1.49.07-3 .14-2.45.11-4.9.21-7.35.3-.2 1.3-.4 2.59-.6 3.93-2.57 16.08-5.93 29.89-18.89 40.86-10.35 7.28-21.87 8.49-34.17 7.71-13.11-2.33-22.52-9.19-30.33-19.83-4.49-7.64-4.8-17.05-5.83-25.67-4.24.39-8.47.77-12.83 1.17-.28 1.84-.28 1.84-.56 3.71-2.32 14.39-5.63 23.35-16.95 33.11-2.32 1.67-2.32 1.67-4.65 1.67 4 4.67 9.06 6.59 14.87 8.24 3.79 1.09 3.79 1.09 6.12 3.43-.65 5.31-.65 5.31-2.33 7-8.42-.27-15.13-2.29-22.17-7-1.09-1.21-2.17-2.43-3.25-3.65-2.72-2.81-4.45-3.84-8.36-4.16-1.67-.02-3.34-.02-5.01.01-1.77-.04-3.54-.09-5.3-.15-1.27-.04-1.27-.04-2.56-.08-9.26-.54-17.6-4.56-24.51-10.64-9.58-11.11-11.03-22.56-10.72-36.82.02-1.4.03-2.8.05-4.24.04-3.42.1-6.85.17-10.27z"/><path fill="#F26117" d="m172.68,203.08c7.27.09 13.23 1.97 18.87 6.65 2.88 3.07 3.86 5.12 4.25 9.32-.12 1.01-.24 2.02-.36 3.06-2.55.95-2.55.95-5.83 1.17-3.28-2.84-3.28-2.84-5.83-5.83-.36.58-.71 1.16-1.08 1.75-7.6 11.29-20.06 17.74-33.05 21.09-20.36 3.1-36.81-1.66-53.37-13.73-2.33-2.11-2.33-2.11-4.67-5.61.42-3.45.99-4.49 3.5-7 4.07.37 5.95 2.13 8.75 4.96 9.81 8.93 22.53 11.87 35.51 11.69 11.74-1.05 22.38-5.85 31.57-13.15 2.06-2.45 2.06-2.45 3.5-4.67-1.66.07-1.66.07-3.35.15-3.65-.15-3.65-.15-5.98-2.48.75-6.18 1.46-7.19 7.58-7.36z"/></g></svg>`;
362
+ var DEXTER_STYLES = `
363
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Orbitron:wght@500;700&display=swap');
364
+ *{margin:0;padding:0;box-sizing:border-box}
365
+ body{font-family:'Inter',system-ui,-apple-system,sans-serif;background:#0a0a0a;color:#e2e8f0;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:1rem}
366
+ .card{max-width:460px;width:100%;background:rgba(20,20,20,.85);border:1px solid rgba(242,107,26,.12);border-radius:8px;padding:2rem 2rem 1.75rem;text-align:center;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px)}
367
+ .crest{margin:0 auto .75rem}
368
+ h1{font-family:'Orbitron',sans-serif;font-size:1.15rem;font-weight:700;color:#f1f5f9;letter-spacing:.04em;margin-bottom:.35rem}
369
+ .desc{color:#94a3b8;font-size:.9rem;margin-bottom:1.25rem;line-height:1.5}
370
+ .price{font-family:'Orbitron',sans-serif;font-size:1.6rem;font-weight:700;color:#F26B1A;margin:.75rem 0 .25rem}
371
+ .chain{color:#525252;font-size:.75rem;margin-bottom:1.25rem;letter-spacing:.03em}
372
+ .endpoint{background:rgba(242,107,26,.06);border:1px solid rgba(242,107,26,.12);border-radius:6px;padding:.5rem .75rem;margin-bottom:1.25rem}
373
+ .endpoint code{font-family:'SF Mono',Monaco,Consolas,monospace;font-size:.8rem;color:#F26B1A}
374
+ .info{background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.06);border-radius:6px;padding:.85rem 1rem;font-size:.82rem;color:#737373;line-height:1.6;text-align:left}
375
+ .info strong{color:#a3a3a3}
376
+ .info code{background:rgba(242,107,26,.08);padding:2px 5px;border-radius:3px;font-size:.78rem;color:#F26B1A;font-family:'SF Mono',Monaco,Consolas,monospace}
377
+ .info a{color:#F26B1A;text-decoration:none;font-weight:600}
378
+ .info a:hover{text-decoration:underline}
379
+ .footer{margin-top:1.25rem;display:flex;align-items:center;justify-content:center;gap:.75rem;font-size:.7rem;color:#404040}
380
+ .footer a{color:#525252;text-decoration:none}
381
+ .footer a:hover{color:#737373}
382
+ .sep{width:3px;height:3px;border-radius:50%;background:#333}
383
+ `;
384
+ var PAY_BUTTON_STYLES = `
385
+ .pay-section{margin:1.25rem 0}
386
+ .pay-btn{display:inline-flex;align-items:center;justify-content:center;gap:.5rem;background:linear-gradient(135deg,#F26B1A,#D13F00);color:#fff;border:none;padding:.65rem 2rem;border-radius:6px;font-family:'Inter',sans-serif;font-size:.95rem;font-weight:600;cursor:pointer;transition:opacity .15s,transform .1s;min-width:180px}
387
+ .pay-btn:hover:not(:disabled){opacity:.9;transform:translateY(-1px)}
388
+ .pay-btn:active:not(:disabled){transform:translateY(0)}
389
+ .pay-btn:disabled{opacity:.6;cursor:not-allowed}
390
+ .pay-btn .spinner{width:16px;height:16px;border:2px solid rgba(255,255,255,.3);border-top-color:#fff;border-radius:50%;animation:spin .6s linear infinite}
391
+ @keyframes spin{to{transform:rotate(360deg)}}
392
+ .pay-status{font-size:.8rem;color:#737373;margin-top:.5rem;min-height:1.2em}
393
+ .pay-status.error{color:#ef4444}
394
+ .pay-status.success{color:#22c55e}
395
+ .pay-alt{font-size:.78rem;color:#404040;margin-top:.75rem}
396
+ .pay-alt a{color:#F26B1A;text-decoration:none}
397
+ .result-box{background:rgba(34,197,94,.06);border:1px solid rgba(34,197,94,.15);border-radius:6px;padding:.75rem;margin-top:.75rem;text-align:left;font-size:.78rem;max-height:200px;overflow:auto}
398
+ .result-box pre{color:#94a3b8;font-family:'SF Mono',Monaco,Consolas,monospace;white-space:pre-wrap;word-break:break-all}
399
+ .no-wallet{font-size:.82rem;color:#737373;margin:1rem 0}
400
+ `;
401
+ var PAY_SCRIPT = `
402
+ <script type="module">
403
+ // Payment data is embedded in #x402-data attributes
404
+ const dataEl = document.getElementById('x402-data');
405
+ if (!dataEl) throw new Error('Missing payment data');
406
+
407
+ const requirements = JSON.parse(atob(dataEl.dataset.requirements));
408
+ const requestMethod = dataEl.dataset.method;
409
+ const requestUrl = dataEl.dataset.url;
410
+ const rpcUrl = dataEl.dataset.rpc || 'https://api.dexter.cash/api/solana/rpc';
411
+
412
+ // Detect wallet provider
413
+ function getWalletProvider() {
414
+ if (window.phantom?.solana?.isPhantom) return { name: 'Phantom', provider: window.phantom.solana };
415
+ if (window.solflare?.isSolflare) return { name: 'Solflare', provider: window.solflare };
416
+ if (window.backpack) return { name: 'Backpack', provider: window.backpack };
417
+ // Generic wallet-standard fallback
418
+ if (window.solana) return { name: 'Wallet', provider: window.solana };
419
+ return null;
420
+ }
421
+
422
+ const walletInfo = getWalletProvider();
423
+ const btn = document.getElementById('pay-btn');
424
+ const status = document.getElementById('pay-status');
425
+ const section = document.getElementById('pay-section');
426
+ const noWallet = document.getElementById('no-wallet');
427
+
428
+ if (walletInfo && btn) {
429
+ section.style.display = 'block';
430
+ if (noWallet) noWallet.style.display = 'none';
431
+ } else if (noWallet) {
432
+ noWallet.style.display = 'block';
433
+ if (section) section.style.display = 'none';
434
+ }
435
+
436
+ // Preload Solana libraries in background
437
+ let solanaLibs = null;
438
+ const preload = (async () => {
439
+ try {
440
+ const [web3, spl] = await Promise.all([
441
+ import('https://esm.sh/@solana/web3.js@1.98.0'),
442
+ import('https://esm.sh/@solana/spl-token@0.4.9'),
443
+ ]);
444
+ solanaLibs = { web3, spl };
445
+ } catch (e) {
446
+ console.warn('[x402] Failed to preload Solana libraries:', e);
447
+ }
448
+ })();
449
+
450
+ function setStatus(msg, type) {
451
+ if (!status) return;
452
+ status.textContent = msg;
453
+ status.className = 'pay-status' + (type ? ' ' + type : '');
454
+ }
455
+
456
+ function setBtnState(text, disabled, loading) {
457
+ if (!btn) return;
458
+ btn.disabled = disabled;
459
+ btn.innerHTML = loading
460
+ ? '<span class="spinner"></span>' + text
461
+ : text;
462
+ }
463
+
464
+ if (btn) {
465
+ btn.addEventListener('click', async () => {
466
+ if (!walletInfo) return;
467
+ const { provider } = walletInfo;
468
+
469
+ try {
470
+ // 1. Connect wallet
471
+ setBtnState('Connecting...', true, true);
472
+ setStatus('');
473
+ await provider.connect();
474
+
475
+ if (!provider.publicKey) {
476
+ throw new Error('Wallet did not provide a public key');
477
+ }
478
+
479
+ // 2. Load Solana libraries (should already be cached from preload)
480
+ setBtnState('Preparing...', true, true);
481
+ await preload;
482
+ if (!solanaLibs) {
483
+ // Retry once
484
+ const [web3, spl] = await Promise.all([
485
+ import('https://esm.sh/@solana/web3.js@1.98.0'),
486
+ import('https://esm.sh/@solana/spl-token@0.4.9'),
487
+ ]);
488
+ solanaLibs = { web3, spl };
489
+ }
490
+
491
+ const { web3, spl } = solanaLibs;
492
+ const { PublicKey, Connection, TransactionMessage, VersionedTransaction, ComputeBudgetProgram } = web3;
493
+ const { getAssociatedTokenAddress, createTransferCheckedInstruction, getMint, TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID } = spl;
494
+
495
+ // 3. Parse payment requirements
496
+ const accept = requirements.accepts[0];
497
+ if (!accept) throw new Error('No payment method available');
498
+
499
+ const payTo = new PublicKey(accept.payTo);
500
+ const amount = BigInt(accept.amount || accept.maxAmountRequired);
501
+ const mintPubkey = new PublicKey(accept.asset);
502
+ const feePayer = accept.extra?.feePayer ? new PublicKey(accept.extra.feePayer) : provider.publicKey;
503
+ const userPubkey = provider.publicKey;
504
+
505
+ // 4. Build transaction
506
+ setBtnState('Building tx...', true, true);
507
+ const connection = new Connection(rpcUrl, 'confirmed');
508
+
509
+ const instructions = [];
510
+
511
+ // ComputeBudget
512
+ instructions.push(ComputeBudgetProgram.setComputeUnitLimit({ units: 12000 }));
513
+ instructions.push(ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 1 }));
514
+
515
+ // Determine token program
516
+ const mintInfo = await connection.getAccountInfo(mintPubkey, 'confirmed');
517
+ if (!mintInfo) throw new Error('Token mint not found');
518
+ const programId = mintInfo.owner.toBase58() === TOKEN_2022_PROGRAM_ID.toBase58() ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID;
519
+
520
+ const mint = await getMint(connection, mintPubkey, undefined, programId);
521
+
522
+ // ATAs
523
+ const sourceAta = await getAssociatedTokenAddress(mintPubkey, userPubkey, false, programId);
524
+ const destAta = await getAssociatedTokenAddress(mintPubkey, payTo, false, programId);
525
+
526
+ // Verify source exists
527
+ const sourceInfo = await connection.getAccountInfo(sourceAta, 'confirmed');
528
+ if (!sourceInfo) throw new Error('No USDC token account found. Make sure you have USDC in your wallet.');
529
+
530
+ // TransferChecked
531
+ instructions.push(createTransferCheckedInstruction(sourceAta, mintPubkey, destAta, userPubkey, amount, mint.decimals, [], programId));
532
+
533
+ const { blockhash } = await connection.getLatestBlockhash('confirmed');
534
+ const message = new TransactionMessage({ payerKey: feePayer, recentBlockhash: blockhash, instructions }).compileToV0Message();
535
+ const transaction = new VersionedTransaction(message);
536
+
537
+ // 5. Sign
538
+ setBtnState('Sign in wallet...', true, true);
539
+ setStatus('Approve the transaction in your wallet');
540
+ const signed = await provider.signTransaction(transaction);
541
+ const serialized = signed.serialize();
542
+
543
+ // Convert Uint8Array to base64
544
+ let payload = '';
545
+ const bytes = new Uint8Array(serialized);
546
+ const chunk = 8192;
547
+ for (let i = 0; i < bytes.length; i += chunk) {
548
+ payload += String.fromCharCode.apply(null, bytes.slice(i, i + chunk));
549
+ }
550
+ payload = btoa(payload);
551
+
552
+ // 6. Build payment-signature header (x402 v2 format)
553
+ const paymentSignature = {
554
+ x402Version: accept.x402Version ?? 2,
555
+ resource: requirements.resource,
556
+ accepted: accept,
557
+ payload,
558
+ };
559
+ const paymentHeader = btoa(JSON.stringify(paymentSignature));
560
+
561
+ // 7. Submit payment
562
+ setBtnState('Verifying...', true, true);
563
+ setStatus('Payment submitted, verifying...');
564
+
565
+ const response = await fetch(requestUrl, {
566
+ method: requestMethod,
567
+ headers: {
568
+ 'Content-Type': 'application/json',
569
+ 'PAYMENT-SIGNATURE': paymentHeader,
570
+ },
571
+ body: requestMethod !== 'GET' ? '{}' : undefined,
572
+ });
573
+
574
+ if (response.ok) {
575
+ const data = await response.json();
576
+ setBtnState('Paid', true, false);
577
+ setStatus('Payment successful', 'success');
578
+ // Show response
579
+ const resultBox = document.createElement('div');
580
+ resultBox.className = 'result-box';
581
+ resultBox.innerHTML = '<pre>' + JSON.stringify(data, null, 2).replace(/</g, '&lt;') + '</pre>';
582
+ section.appendChild(resultBox);
583
+ } else {
584
+ const err = await response.json().catch(() => ({ error: 'Payment verification failed' }));
585
+ throw new Error(err.error || err.reason || 'Payment failed');
586
+ }
587
+ } catch (err) {
588
+ console.error('[x402] Payment error:', err);
589
+ setBtnState('Pay $' + document.getElementById('price-value').textContent, false, false);
590
+ setStatus(err.message || 'Payment failed', 'error');
591
+ }
592
+ });
593
+ }
594
+ </script>
595
+ `;
596
+ function generatePaywallHtml(paymentRequiredHeader, requestUrl, method, config, rpcUrl) {
362
597
  let price = "?";
363
598
  let description = "This resource requires payment";
364
599
  let network = "";
@@ -376,7 +611,7 @@ function generatePaywallHtml(paymentRequiredHeader, requestUrl, method, config)
376
611
  }
377
612
  } catch {
378
613
  }
379
- const chainName = network.includes("solana") ? "Solana" : network.includes("eip155") ? "Base" : "Unknown";
614
+ const chainName = network.includes("solana") ? "Solana" : network.includes("eip155") ? "Base" : "";
380
615
  const endpointSection = config.showEndpoint ? `<div class="endpoint"><code>${method} ${requestUrl}</code></div>` : "";
381
616
  return `<!DOCTYPE html>
382
617
  <html lang="en">
@@ -384,43 +619,46 @@ function generatePaywallHtml(paymentRequiredHeader, requestUrl, method, config)
384
619
  <meta charset="utf-8">
385
620
  <meta name="viewport" content="width=device-width,initial-scale=1">
386
621
  <title>${config.title} \u2014 $${price} USDC</title>
387
- <style>
388
- *{margin:0;padding:0;box-sizing:border-box}
389
- body{font-family:system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#0a0a0a;color:#e2e8f0;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:1rem}
390
- .paywall{max-width:460px;width:100%;background:#141414;border:1px solid #2a2a2a;border-radius:16px;padding:2.5rem;text-align:center}
391
- .paywall h1{font-size:1.35rem;margin-bottom:.35rem;color:#f1f5f9;font-weight:600}
392
- .desc{color:#94a3b8;font-size:.95rem;margin-bottom:1.5rem;line-height:1.5}
393
- .price-badge{display:inline-block;background:linear-gradient(135deg,#22c55e,#16a34a);color:#fff;padding:.6rem 2rem;border-radius:999px;font-size:1.75rem;font-weight:700;margin:1rem 0;letter-spacing:-.02em}
394
- .chain{color:#64748b;font-size:.8rem;margin-bottom:1.5rem}
395
- .endpoint{background:#1e1e1e;border:1px solid #333;border-radius:8px;padding:.6rem 1rem;margin-bottom:1.5rem}
396
- .endpoint code{font-family:'SF Mono',Monaco,Consolas,monospace;font-size:.85rem;color:#60a5fa}
397
- .info{background:#1a1a2e;border:1px solid #2d2d5e;border-radius:10px;padding:1rem 1.25rem;font-size:.85rem;color:#a0aec0;line-height:1.6;text-align:left}
398
- .info strong{color:#e2e8f0}
399
- .info code{background:#2a2a3e;padding:2px 6px;border-radius:4px;font-size:.8rem;color:#818cf8;font-family:'SF Mono',Monaco,Consolas,monospace}
400
- .info a{color:#60a5fa;text-decoration:none;font-weight:600}
401
- .info a:hover{text-decoration:underline}
402
- .powered{margin-top:1.5rem;font-size:.75rem;color:#475569}
403
- .powered a{color:#64748b;text-decoration:none}
404
- .powered a:hover{color:#94a3b8}
405
- .x402-badge{display:inline-flex;align-items:center;gap:.35rem;background:#1a1a2e;border:1px solid #2d2d5e;padding:.25rem .75rem;border-radius:999px;font-size:.7rem;color:#818cf8;margin-top:.75rem;font-weight:500}
406
- </style>
622
+ <style>${DEXTER_STYLES}${PAY_BUTTON_STYLES}</style>
407
623
  </head>
408
624
  <body>
409
- <div class="paywall">
625
+ <div class="card">
626
+ <div class="crest">${DEXTER_CREST_SVG}</div>
410
627
  <h1>${config.title}</h1>
411
628
  <p class="desc">${description}</p>
412
- <div class="price-badge">$${price} USDC</div>
413
- <div class="chain">${chainName} network</div>
629
+ <div class="price">$<span id="price-value">${price}</span> USDC</div>
630
+ <div class="chain">${chainName}${chainName ? " network" : ""}</div>
414
631
  ${endpointSection}
415
- <div class="info">
416
- <strong>How to access this endpoint:</strong><br><br>
417
- Use any x402-compatible client or SDK. The client handles wallet connection and payment automatically.<br><br>
418
- <code>npm install @dexterai/x402</code><br><br>
419
- <a href="${config.sdkUrl}">Learn more about x402 &rarr;</a>
632
+
633
+ <div id="pay-section" class="pay-section" style="display:none">
634
+ <button id="pay-btn" class="pay-btn">Pay $${price}</button>
635
+ <div id="pay-status" class="pay-status"></div>
636
+ <div class="pay-alt">or use <a href="${config.sdkUrl}">x402 SDK</a> for programmatic access</div>
637
+ </div>
638
+
639
+ <div id="no-wallet" class="no-wallet" style="display:none">
640
+ <div class="info">
641
+ <strong>Access this endpoint:</strong><br><br>
642
+ Use any x402-compatible client or a browser with a Solana wallet extension (Phantom, Solflare, Backpack).<br><br>
643
+ <code>npm install @dexterai/x402</code><br><br>
644
+ <a href="${config.sdkUrl}">x402 SDK docs &rarr;</a>
645
+ </div>
646
+ </div>
647
+
648
+ <div class="footer">
649
+ <a href="https://x402.org">x402</a>
650
+ <span class="sep"></span>
651
+ <a href="https://dexter.cash">Dexter</a>
420
652
  </div>
421
- <div class="powered">${config.branding}</div>
422
- <div class="x402-badge">x402 protocol</div>
423
653
  </div>
654
+
655
+ <div id="x402-data" style="display:none"
656
+ data-requirements="${paymentRequiredHeader}"
657
+ data-method="${method}"
658
+ data-url="${requestUrl}"
659
+ data-rpc="${rpcUrl}"
660
+ ></div>
661
+ ${PAY_SCRIPT}
424
662
  </body>
425
663
  </html>`;
426
664
  }
@@ -431,6 +669,7 @@ function x402BrowserSupport(config = {}) {
431
669
  sdkUrl: config.sdkUrl ?? "https://x402.org",
432
670
  showEndpoint: config.showEndpoint ?? true
433
671
  };
672
+ const rpcUrl = config.rpcUrl ?? "https://api.dexter.cash/api/solana/rpc";
434
673
  return (req, res, next) => {
435
674
  const originalJson = res.json.bind(res);
436
675
  res.json = function(body) {
@@ -441,7 +680,8 @@ function x402BrowserSupport(config = {}) {
441
680
  paymentRequired,
442
681
  req.originalUrl,
443
682
  req.method,
444
- resolvedConfig
683
+ resolvedConfig,
684
+ rpcUrl
445
685
  );
446
686
  res.status(402).type("html").send(html);
447
687
  return res;