@alleyboss/micropay-solana-x402-paywall 2.0.0 β†’ 2.0.2

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
@@ -1,165 +1,113 @@
1
1
  # @alleyboss/micropay-solana-x402-paywall
2
2
 
3
- > Production-ready Solana micropayments library implementing the x402 protocol with SPL token support.
3
+ > Production-ready Solana micropayments library implementing the x402 protocol.
4
4
 
5
- [![npm version](https://img.shields.io/npm/v/@alleyboss/micropay-solana-x402-paywall)](https://www.npmjs.com/package/@alleyboss/micropay-solana-x402-paywall)
5
+ [![npm](https://img.shields.io/npm/v/@alleyboss/micropay-solana-x402-paywall)](https://www.npmjs.com/package/@alleyboss/micropay-solana-x402-paywall)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
7
+ [![Bundle Size](https://img.shields.io/badge/bundle-38KB-green)](https://bundlephobia.com/package/@alleyboss/micropay-solana-x402-paywall)
7
8
 
8
- ## Features
9
+ ## πŸš€ What It Does
9
10
 
10
- - πŸ” **x402 Protocol** β€” HTTP 402 Payment Required standard
11
- - ⚑ **Solana Native** β€” Fast, low-cost SOL & USDC micropayments
12
- - πŸ”‘ **JWT Sessions** β€” Secure unlock tracking with anti-replay
13
- - πŸ“¦ **Framework Agnostic** β€” Express, Next.js, Fastify ready
14
- - 🌳 **Tree-shakeable** β€” Import only what you need (35KB full, 1-13KB per module)
15
- - πŸ’° **SPL Tokens** β€” USDC, USDT, and custom token support
16
- - πŸ”„ **Retry Logic** β€” Built-in resilience for RPC failures
17
-
18
- ## Installation
11
+ Turn any content into paid content with **one-time micropayments** on Solana. No subscriptions, no recurring chargesβ€”just pay to unlock.
19
12
 
20
13
  ```bash
21
14
  npm install @alleyboss/micropay-solana-x402-paywall @solana/web3.js
22
15
  ```
23
16
 
24
- ## Quick Start
17
+ ## ✨ Features
18
+
19
+ | Feature | Description |
20
+ |---------|-------------|
21
+ | πŸ’° **SOL & USDC Payments** | Native SOL and SPL tokens (USDC, USDT) |
22
+ | πŸ” **x402 Protocol** | HTTP 402 Payment Required standard |
23
+ | πŸ”‘ **JWT Sessions** | Secure unlock tracking with anti-replay |
24
+ | �️ **Signature Store** | Prevent double-spend at app layer |
25
+ | πŸ”Œ **Express & Next.js** | Zero-boilerplate middleware |
26
+ | οΏ½ **Price Conversion** | USD↔SOL with multi-provider fallback |
27
+ | 🌳 **Tree-Shakeable** | Import only what you need |
25
28
 
26
- ### Verify a Payment (SOL or USDC)
29
+ ## πŸ“¦ Quick Example
27
30
 
28
31
  ```typescript
29
- import { verifyPayment, verifySPLPayment } from '@alleyboss/micropay-solana-x402-paywall';
32
+ import { verifyPayment, createSession } from '@alleyboss/micropay-solana-x402-paywall';
30
33
 
31
- // SOL payment
34
+ // Verify on-chain payment
32
35
  const result = await verifyPayment({
33
36
  signature: 'tx...',
34
37
  expectedRecipient: 'CreatorWallet',
35
38
  expectedAmount: 10_000_000n, // 0.01 SOL
36
- clientConfig: { network: 'devnet' },
37
- });
38
-
39
- // USDC payment
40
- const usdcResult = await verifySPLPayment({
41
- signature: 'tx...',
42
- expectedRecipient: 'CreatorWallet',
43
- expectedAmount: 1_000_000n, // 1 USDC
44
- asset: 'usdc',
45
39
  clientConfig: { network: 'mainnet-beta' },
46
40
  });
47
- ```
48
-
49
- ### Express Middleware (Zero Boilerplate)
50
41
 
51
- ```typescript
52
- import express from 'express';
53
- import { createExpressMiddleware } from '@alleyboss/micropay-solana-x402-paywall/middleware';
54
-
55
- const app = express();
56
-
57
- app.use('/api/premium', createExpressMiddleware({
58
- sessionSecret: process.env.SESSION_SECRET!,
59
- protectedPaths: ['/**'],
60
- }));
61
-
62
- app.get('/api/premium/content', (req, res) => {
63
- res.json({ content: 'Premium!', wallet: req.session?.walletAddress });
64
- });
42
+ // Create session for unlocked content
43
+ if (result.valid) {
44
+ const { token } = await createSession(
45
+ result.from!,
46
+ 'article-123',
47
+ { secret: process.env.SESSION_SECRET!, durationHours: 24 }
48
+ );
49
+ }
65
50
  ```
66
51
 
67
- ### Next.js Middleware
68
-
69
- ```typescript
70
- // middleware.ts
71
- import { createPaywallMiddleware } from '@alleyboss/micropay-solana-x402-paywall/middleware';
72
-
73
- export const middleware = createPaywallMiddleware({
74
- sessionSecret: process.env.SESSION_SECRET!,
75
- protectedPaths: ['/api/premium/*', '/api/content/*'],
76
- });
77
-
78
- export const config = { matcher: ['/api/premium/:path*'] };
79
- ```
52
+ ## πŸ”§ Modules
80
53
 
81
- ### Prevent Signature Replay (Anti-Double-Spend)
54
+ 9 tree-shakeable entry points for minimal bundle size:
82
55
 
83
56
  ```typescript
84
- import { createMemoryStore, createRedisStore } from '@alleyboss/micropay-solana-x402-paywall/store';
85
-
86
- // Development
87
- const store = createMemoryStore();
57
+ // Core verification
58
+ import { verifyPayment, verifySPLPayment } from '@alleyboss/micropay-solana-x402-paywall/solana';
88
59
 
89
- // Production (with ioredis or node-redis)
90
- const store = createRedisStore({ client: redisClient });
91
-
92
- // Check before verification
93
- if (await store.hasBeenUsed(signature)) {
94
- throw new Error('Payment already used');
95
- }
60
+ // Session management
61
+ import { createSession, validateSession } from '@alleyboss/micropay-solana-x402-paywall/session';
96
62
 
97
- // Mark after successful verification
98
- await store.markAsUsed(signature, articleId, new Date(Date.now() + 86400000));
99
- ```
63
+ // x402 protocol
64
+ import { buildPaymentRequirement } from '@alleyboss/micropay-solana-x402-paywall/x402';
100
65
 
101
- ### Client-Side Payment Flow
66
+ // Express/Next.js middleware
67
+ import { createExpressMiddleware, createPaywallMiddleware } from '@alleyboss/micropay-solana-x402-paywall/middleware';
102
68
 
103
- ```typescript
104
- import { createPaymentFlow, formatPriceDisplay } from '@alleyboss/micropay-solana-x402-paywall';
69
+ // Anti-replay signature store
70
+ import { createMemoryStore, createRedisStore } from '@alleyboss/micropay-solana-x402-paywall/store';
105
71
 
106
- const flow = createPaymentFlow({
107
- network: 'mainnet-beta',
108
- recipientWallet: 'CreatorWallet',
109
- amount: 10_000_000n,
110
- });
72
+ // Client-side helpers
73
+ import { createPaymentFlow, buildSolanaPayUrl } from '@alleyboss/micropay-solana-x402-paywall/client';
111
74
 
112
- // Generate QR code for mobile wallets
113
- const qrUrl = flow.getSolanaPayUrl({ label: 'Unlock Article' });
75
+ // Price conversion (4-provider rotation)
76
+ import { getSolPrice, formatPriceDisplay, configurePricing } from '@alleyboss/micropay-solana-x402-paywall/pricing';
114
77
 
115
- // Display price with USD equivalent
116
- const price = await formatPriceDisplay(10_000_000n);
117
- // "0.0100 SOL (~$1.50)"
78
+ // Retry utilities
79
+ import { withRetry } from '@alleyboss/micropay-solana-x402-paywall/utils';
118
80
  ```
119
81
 
120
- ## Module Exports
121
-
122
- Import only what you need for minimal bundle size:
82
+ ## πŸ”₯ New in v2.0
123
83
 
124
- | Import Path | Size | Functions |
125
- |-------------|------|-----------|
126
- | `@.../solana` | 13KB | `verifyPayment`, `verifySPLPayment`, `getConnection` |
127
- | `@.../session` | 4.5KB | `createSession`, `validateSession`, `isArticleUnlocked` |
128
- | `@.../x402` | 10KB | `buildPaymentRequirement`, `verifyX402Payment` |
129
- | `@.../middleware` | 8KB | `createExpressMiddleware`, `createPaywallMiddleware` |
130
- | `@.../store` | 2.6KB | `createMemoryStore`, `createRedisStore` |
131
- | `@.../client` | 3.3KB | `createPaymentFlow`, `buildSolanaPayUrl` |
132
- | `@.../pricing` | 2KB | `getSolPrice`, `formatPriceDisplay` |
133
- | `@.../utils` | 1.7KB | `withRetry`, `isRetryableRPCError` |
84
+ - **SPL Token Support** β€” USDC, USDT, custom tokens
85
+ - **Multi-Provider Pricing** β€” CoinCap β†’ Binance β†’ CoinGecko β†’ Kraken fallback
86
+ - **Custom Price API** β€” `configurePricing({ customProvider: yourFn })`
87
+ - **Express Middleware** β€” Works with Express, Fastify, Polka
88
+ - **Signature Store** β€” Memory & Redis adapters for anti-replay
89
+ - **Client Helpers** β€” Solana Pay URLs for QR codes
134
90
 
135
- ## TypeScript Support
91
+ ## πŸ“š Documentation
136
92
 
137
- Full TypeScript with exported types:
93
+ **Full documentation, API reference, and examples:**
138
94
 
139
- ```typescript
140
- import type {
141
- PaymentRequirement,
142
- PaymentAsset,
143
- SessionData,
144
- SignatureStore,
145
- PaywallMiddlewareConfig,
146
- } from '@alleyboss/micropay-solana-x402-paywall';
147
- ```
95
+ πŸ‘‰ **[solana-x402-paywall.vercel.app/docs](https://solana-x402-paywall.vercel.app/docs)**
148
96
 
149
- ## RPC Configuration
97
+ ## πŸ› οΈ RPC Providers
150
98
 
151
- Supports multiple RPC providers:
99
+ Works with any Solana RPC provider:
152
100
 
153
101
  ```typescript
154
- const clientConfig = {
102
+ const config = {
155
103
  network: 'mainnet-beta',
156
- // Option 1: Tatum.io
157
- tatumApiKey: 'your-api-key',
158
- // Option 2: Custom RPC (Helius, QuickNode, etc.)
159
- rpcUrl: 'https://your-rpc-endpoint.com',
104
+ // Tatum.io
105
+ tatumApiKey: 'your-key',
106
+ // Or custom (Helius, QuickNode, etc.)
107
+ rpcUrl: 'https://your-rpc.com',
160
108
  };
161
109
  ```
162
110
 
163
- ## License
111
+ ## πŸ“„ License
164
112
 
165
113
  MIT Β© AlleyBoss
package/dist/index.cjs CHANGED
@@ -15,8 +15,8 @@ var TOKEN_MINTS = {
15
15
  };
16
16
  var cachedConnection = null;
17
17
  var cachedNetwork = null;
18
- function buildRpcUrl(config) {
19
- const { network, rpcUrl, tatumApiKey } = config;
18
+ function buildRpcUrl(config2) {
19
+ const { network, rpcUrl, tatumApiKey } = config2;
20
20
  if (rpcUrl) {
21
21
  if (rpcUrl.includes("tatum.io") && tatumApiKey && !rpcUrl.includes(tatumApiKey)) {
22
22
  return rpcUrl.endsWith("/") ? `${rpcUrl}${tatumApiKey}` : `${rpcUrl}/${tatumApiKey}`;
@@ -29,12 +29,12 @@ function buildRpcUrl(config) {
29
29
  }
30
30
  return web3_js.clusterApiUrl(network);
31
31
  }
32
- function getConnection(config) {
33
- const { network } = config;
32
+ function getConnection(config2) {
33
+ const { network } = config2;
34
34
  if (cachedConnection && cachedNetwork === network) {
35
35
  return cachedConnection;
36
36
  }
37
- const rpcUrl = buildRpcUrl(config);
37
+ const rpcUrl = buildRpcUrl(config2);
38
38
  cachedConnection = new web3_js.Connection(rpcUrl, {
39
39
  commitment: "confirmed",
40
40
  confirmTransactionInitialTimeout: 6e4
@@ -420,19 +420,19 @@ function validateArticleId(articleId) {
420
420
  const safeIdRegex = /^[a-zA-Z0-9_-]+$/;
421
421
  return safeIdRegex.test(articleId);
422
422
  }
423
- async function createSession(walletAddress, articleId, config, siteWide = false) {
423
+ async function createSession(walletAddress, articleId, config2, siteWide = false) {
424
424
  if (!validateWalletAddress(walletAddress)) {
425
425
  throw new Error("Invalid wallet address format");
426
426
  }
427
427
  if (!validateArticleId(articleId)) {
428
428
  throw new Error("Invalid article ID format");
429
429
  }
430
- if (!config.durationHours || config.durationHours <= 0 || config.durationHours > 720) {
430
+ if (!config2.durationHours || config2.durationHours <= 0 || config2.durationHours > 720) {
431
431
  throw new Error("Session duration must be between 1 and 720 hours");
432
432
  }
433
433
  const sessionId = uuid.v4();
434
434
  const now = Math.floor(Date.now() / 1e3);
435
- const expiresAt = now + config.durationHours * 3600;
435
+ const expiresAt = now + config2.durationHours * 3600;
436
436
  const session = {
437
437
  id: sessionId,
438
438
  walletAddress,
@@ -449,7 +449,7 @@ async function createSession(walletAddress, articleId, config, siteWide = false)
449
449
  iat: now,
450
450
  exp: expiresAt
451
451
  };
452
- const token = await new jose.SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(`${config.durationHours}h`).sign(getSecretKey(config.secret));
452
+ const token = await new jose.SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(`${config2.durationHours}h`).sign(getSecretKey(config2.secret));
453
453
  return { token, session };
454
454
  }
455
455
  async function validateSession(token, secret) {
@@ -806,8 +806,8 @@ function matchesProtectedPath(path, patterns) {
806
806
  }
807
807
  return false;
808
808
  }
809
- async function checkPaywallAccess(path, sessionToken, config) {
810
- if (!matchesProtectedPath(path, config.protectedPaths)) {
809
+ async function checkPaywallAccess(path, sessionToken, config2) {
810
+ if (!matchesProtectedPath(path, config2.protectedPaths)) {
811
811
  return { allowed: true };
812
812
  }
813
813
  if (!sessionToken) {
@@ -817,7 +817,7 @@ async function checkPaywallAccess(path, sessionToken, config) {
817
817
  requiresPayment: true
818
818
  };
819
819
  }
820
- const validation = await validateSession(sessionToken, config.sessionSecret);
820
+ const validation = await validateSession(sessionToken, config2.sessionSecret);
821
821
  if (!validation.valid || !validation.session) {
822
822
  return {
823
823
  allowed: false,
@@ -830,8 +830,8 @@ async function checkPaywallAccess(path, sessionToken, config) {
830
830
  session: validation.session
831
831
  };
832
832
  }
833
- function createPaywallMiddleware(config) {
834
- const { cookieName = "x402_session" } = config;
833
+ function createPaywallMiddleware(config2) {
834
+ const { cookieName = "x402_session" } = config2;
835
835
  return async function middleware(request) {
836
836
  const url = new URL(request.url);
837
837
  const path = url.pathname;
@@ -843,9 +843,9 @@ function createPaywallMiddleware(config) {
843
843
  })
844
844
  );
845
845
  const sessionToken = cookies[cookieName];
846
- const result = await checkPaywallAccess(path, sessionToken, config);
846
+ const result = await checkPaywallAccess(path, sessionToken, config2);
847
847
  if (!result.allowed && result.requiresPayment) {
848
- const body = config.custom402Response ? config.custom402Response(path) : {
848
+ const body = config2.custom402Response ? config2.custom402Response(path) : {
849
849
  error: "Payment Required",
850
850
  message: "This resource requires payment to access",
851
851
  path
@@ -981,8 +981,8 @@ function buildSolanaPayUrl(params) {
981
981
  }
982
982
  return url.toString();
983
983
  }
984
- function createPaymentFlow(config) {
985
- const { network, recipientWallet, amount, asset = "native", memo } = config;
984
+ function createPaymentFlow(config2) {
985
+ const { network, recipientWallet, amount, asset = "native", memo } = config2;
986
986
  let decimals = 9;
987
987
  let mintAddress;
988
988
  if (asset === "usdc") {
@@ -998,7 +998,7 @@ function createPaymentFlow(config) {
998
998
  const naturalAmount = Number(amount) / Math.pow(10, decimals);
999
999
  return {
1000
1000
  /** Get the payment configuration */
1001
- getConfig: () => ({ ...config }),
1001
+ getConfig: () => ({ ...config2 }),
1002
1002
  /** Get amount in natural display units (e.g., 0.01 SOL) */
1003
1003
  getDisplayAmount: () => naturalAmount,
1004
1004
  /** Get amount formatted with symbol */
@@ -1044,43 +1044,100 @@ function createPaymentReference() {
1044
1044
 
1045
1045
  // src/pricing/index.ts
1046
1046
  var cachedPrice = null;
1047
- var CACHE_TTL_MS = 6e4;
1048
- async function getSolPrice() {
1049
- if (cachedPrice && Date.now() - cachedPrice.fetchedAt.getTime() < CACHE_TTL_MS) {
1050
- return cachedPrice;
1051
- }
1047
+ var config = {};
1048
+ var lastProviderIndex = -1;
1049
+ function configurePricing(newConfig) {
1050
+ config = { ...config, ...newConfig };
1051
+ cachedPrice = null;
1052
+ }
1053
+ var PROVIDERS = [
1054
+ {
1055
+ name: "coincap",
1056
+ url: "https://api.coincap.io/v2/assets/solana",
1057
+ parse: (data) => parseFloat(data.data?.priceUsd || "0")
1058
+ },
1059
+ {
1060
+ name: "binance",
1061
+ url: "https://api.binance.com/api/v3/ticker/price?symbol=SOLUSDT",
1062
+ parse: (data) => parseFloat(data.price || "0")
1063
+ },
1064
+ {
1065
+ name: "coingecko",
1066
+ url: "https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd",
1067
+ parse: (data) => data.solana?.usd || 0
1068
+ },
1069
+ {
1070
+ name: "kraken",
1071
+ url: "https://api.kraken.com/0/public/Ticker?pair=SOLUSD",
1072
+ parse: (data) => parseFloat(data.result?.SOLUSD?.c?.[0] || "0")
1073
+ }
1074
+ ];
1075
+ async function fetchFromProvider(provider, timeout) {
1076
+ const controller = new AbortController();
1077
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
1052
1078
  try {
1053
- const response = await fetch(
1054
- "https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd",
1055
- {
1056
- headers: { "Accept": "application/json" },
1057
- signal: AbortSignal.timeout(5e3)
1058
- }
1059
- );
1079
+ const response = await fetch(provider.url, {
1080
+ headers: { "Accept": "application/json" },
1081
+ signal: controller.signal
1082
+ });
1060
1083
  if (!response.ok) {
1061
- throw new Error(`Price fetch failed: ${response.status}`);
1084
+ throw new Error(`HTTP ${response.status}`);
1062
1085
  }
1063
1086
  const data = await response.json();
1064
- if (!data.solana?.usd) {
1065
- throw new Error("Invalid price response");
1087
+ const price = provider.parse(data);
1088
+ if (!price || price <= 0) {
1089
+ throw new Error("Invalid price");
1066
1090
  }
1067
- cachedPrice = {
1068
- solPrice: data.solana.usd,
1069
- fetchedAt: /* @__PURE__ */ new Date(),
1070
- source: "coingecko"
1071
- };
1091
+ return price;
1092
+ } finally {
1093
+ clearTimeout(timeoutId);
1094
+ }
1095
+ }
1096
+ async function getSolPrice() {
1097
+ const cacheTTL = config.cacheTTL ?? 6e4;
1098
+ const timeout = config.timeout ?? 5e3;
1099
+ if (cachedPrice && Date.now() - cachedPrice.fetchedAt.getTime() < cacheTTL) {
1072
1100
  return cachedPrice;
1073
- } catch (error) {
1074
- if (cachedPrice) {
1101
+ }
1102
+ if (config.customProvider) {
1103
+ try {
1104
+ const price = await config.customProvider();
1105
+ if (price > 0) {
1106
+ cachedPrice = {
1107
+ solPrice: price,
1108
+ fetchedAt: /* @__PURE__ */ new Date(),
1109
+ source: "custom"
1110
+ };
1111
+ return cachedPrice;
1112
+ }
1113
+ } catch {
1114
+ }
1115
+ }
1116
+ for (let i = 0; i < PROVIDERS.length; i++) {
1117
+ const idx = (lastProviderIndex + 1 + i) % PROVIDERS.length;
1118
+ const provider = PROVIDERS[idx];
1119
+ try {
1120
+ const price = await fetchFromProvider(provider, timeout);
1121
+ lastProviderIndex = idx;
1122
+ cachedPrice = {
1123
+ solPrice: price,
1124
+ fetchedAt: /* @__PURE__ */ new Date(),
1125
+ source: provider.name
1126
+ };
1075
1127
  return cachedPrice;
1128
+ } catch {
1129
+ continue;
1076
1130
  }
1077
- return {
1078
- solPrice: 150,
1079
- // Fallback price
1080
- fetchedAt: /* @__PURE__ */ new Date(),
1081
- source: "fallback"
1082
- };
1083
1131
  }
1132
+ if (cachedPrice) {
1133
+ return cachedPrice;
1134
+ }
1135
+ return {
1136
+ solPrice: 150,
1137
+ // Reasonable fallback
1138
+ fetchedAt: /* @__PURE__ */ new Date(),
1139
+ source: "fallback"
1140
+ };
1084
1141
  }
1085
1142
  async function lamportsToUsd(lamports) {
1086
1143
  const { solPrice } = await getSolPrice();
@@ -1109,6 +1166,10 @@ function formatPriceSync(lamports, solPrice) {
1109
1166
  }
1110
1167
  function clearPriceCache() {
1111
1168
  cachedPrice = null;
1169
+ lastProviderIndex = -1;
1170
+ }
1171
+ function getProviders() {
1172
+ return PROVIDERS.map((p) => ({ name: p.name, url: p.url }));
1112
1173
  }
1113
1174
 
1114
1175
  exports.TOKEN_MINTS = TOKEN_MINTS;
@@ -1118,6 +1179,7 @@ exports.buildPaymentRequirement = buildPaymentRequirement;
1118
1179
  exports.buildSolanaPayUrl = buildSolanaPayUrl;
1119
1180
  exports.checkPaywallAccess = checkPaywallAccess;
1120
1181
  exports.clearPriceCache = clearPriceCache;
1182
+ exports.configurePricing = configurePricing;
1121
1183
  exports.create402Headers = create402Headers;
1122
1184
  exports.create402ResponseBody = create402ResponseBody;
1123
1185
  exports.createMemoryStore = createMemoryStore;
@@ -1132,6 +1194,7 @@ exports.encodePaymentResponse = encodePaymentResponse;
1132
1194
  exports.formatPriceDisplay = formatPriceDisplay;
1133
1195
  exports.formatPriceSync = formatPriceSync;
1134
1196
  exports.getConnection = getConnection;
1197
+ exports.getProviders = getProviders;
1135
1198
  exports.getSolPrice = getSolPrice;
1136
1199
  exports.getTokenDecimals = getTokenDecimals;
1137
1200
  exports.getWalletTransactions = getWalletTransactions;