@dexterai/x402 1.4.1 → 1.5.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/README.md CHANGED
@@ -24,7 +24,7 @@
24
24
 
25
25
  x402 is a protocol for HTTP-native micropayments. When a server returns HTTP status `402 Payment Required`, it includes payment details in a `PAYMENT-REQUIRED` header. The client signs a payment transaction and retries the request with a `PAYMENT-SIGNATURE` header. The server verifies and settles the payment, then returns the protected content.
26
26
 
27
- This SDK handles the entire flow automatically—you just call `fetch()` and payments happen transparently.
27
+ This SDK handles the entire flow automatically—you just call `fetch()` and payments happen transparently. With **Access Pass** mode, buyers pay once and get unlimited access for a time window—no per-request signing needed.
28
28
 
29
29
  ---
30
30
 
@@ -36,6 +36,8 @@ This SDK handles the entire flow automatically—you just call `fetch()` and pay
36
36
 
37
37
  **Token-accurate LLM pricing.** Built-in [tiktoken](https://github.com/openai/tiktoken) support prices AI requests by actual token count. Works with OpenAI models out of the box, or bring your own rates for Anthropic, Gemini, Mistral, or local models.
38
38
 
39
+ **Access Pass.** Pay once, get unlimited access for a time window. Buyers connect a wallet, make one payment, and receive a JWT token that works like an API key—no per-request signing, no private keys in code. The Stripe replacement for crypto-native APIs.
40
+
39
41
  **Full-stack.** Client SDK for browsers, server SDK for backends. React hooks, Express middleware patterns, facilitator client—everything you need.
40
42
 
41
43
  **Multi-chain.** Solana and Base (Ethereum L2) with the same API. Add wallets for both and the SDK picks the right one automatically.
@@ -158,12 +160,18 @@ import { useX402Payment } from '@dexterai/x402/react';
158
160
  // Server - Express middleware
159
161
  import { x402Middleware } from '@dexterai/x402/server';
160
162
 
163
+ // Server - Access Pass (pay once, unlimited requests)
164
+ import { x402AccessPass } from '@dexterai/x402/server';
165
+
161
166
  // Server - manual control
162
167
  import { createX402Server } from '@dexterai/x402/server';
163
168
 
164
169
  // Server - dynamic pricing
165
170
  import { createDynamicPricing, createTokenPricing } from '@dexterai/x402/server';
166
171
 
172
+ // React - Access Pass hook
173
+ import { useAccessPass } from '@dexterai/x402/react';
174
+
167
175
  // Chain adapters (advanced)
168
176
  import { createSolanaAdapter, createEvmAdapter } from '@dexterai/x402/adapters';
169
177
 
@@ -221,6 +229,96 @@ Options:
221
229
  - `facilitatorUrl` — Override facilitator (default: x402.dexter.cash)
222
230
  - `verbose` — Enable debug logging
223
231
 
232
+ ### Access Pass — Pay Once, Unlimited Requests
233
+
234
+ Replace API keys with time-limited access passes. Buyers make one payment and get a JWT token for unlimited requests during a time window.
235
+
236
+ **Server:**
237
+
238
+ ```typescript
239
+ import express from 'express';
240
+ import { x402AccessPass } from '@dexterai/x402/server';
241
+
242
+ const app = express();
243
+
244
+ // Protect all /api routes with access pass
245
+ app.use('/api', x402AccessPass({
246
+ payTo: 'YourSolanaAddress...',
247
+ tiers: {
248
+ '1h': '0.50', // $0.50 for 1 hour
249
+ '24h': '2.00', // $2.00 for 24 hours
250
+ },
251
+ ratePerHour: '0.50', // also accept custom durations
252
+ }));
253
+
254
+ app.get('/api/data', (req, res) => {
255
+ // Only runs with a valid access pass
256
+ res.json({ data: 'premium content' });
257
+ });
258
+ ```
259
+
260
+ **Client (Node.js):**
261
+
262
+ ```typescript
263
+ import { wrapFetch } from '@dexterai/x402/client';
264
+
265
+ const x402Fetch = wrapFetch(fetch, {
266
+ walletPrivateKey: process.env.SOLANA_PRIVATE_KEY,
267
+ accessPass: { preferTier: '1h', maxSpend: '1.00' },
268
+ });
269
+
270
+ // First call: auto-purchases a 1-hour pass ($0.50 USDC)
271
+ const res1 = await x402Fetch('https://api.example.com/api/data');
272
+
273
+ // All subsequent calls for the next hour: uses cached JWT, zero payment
274
+ const res2 = await x402Fetch('https://api.example.com/api/data');
275
+ const res3 = await x402Fetch('https://api.example.com/api/data');
276
+ ```
277
+
278
+ **React:**
279
+
280
+ ```tsx
281
+ import { useAccessPass } from '@dexterai/x402/react';
282
+
283
+ function Dashboard() {
284
+ const { tiers, pass, isPassValid, purchasePass, fetch: apFetch } = useAccessPass({
285
+ wallets: { solana: solanaWallet },
286
+ resourceUrl: 'https://api.example.com',
287
+ });
288
+
289
+ return (
290
+ <div>
291
+ {!isPassValid && tiers?.map(t => (
292
+ <button key={t.id} onClick={() => purchasePass(t.id)}>
293
+ {t.label} — ${t.price}
294
+ </button>
295
+ ))}
296
+ {isPassValid && <p>Pass active! {pass?.remainingSeconds}s remaining</p>}
297
+ <button onClick={() => apFetch('/api/data')}>Fetch Data</button>
298
+ </div>
299
+ );
300
+ }
301
+ ```
302
+
303
+ **How it works:**
304
+ 1. Client requests a protected endpoint → Server returns `402` with `X-ACCESS-PASS-TIERS` header
305
+ 2. Client selects a tier and pays via x402 → Server verifies, settles, issues a JWT
306
+ 3. Server returns `200` with `ACCESS-PASS` header containing the JWT
307
+ 4. Client caches the JWT and includes it as `Authorization: Bearer <token>` on all subsequent requests
308
+ 5. Server validates the JWT locally (no facilitator call) → instant response
309
+
310
+ Options:
311
+ - `payTo` — Address to receive payments
312
+ - `tiers` — Named duration tiers with prices (e.g., `{ '1h': '0.50' }`)
313
+ - `ratePerHour` — Rate for custom durations (buyer sends `?duration=<seconds>`)
314
+ - `network` — CAIP-2 network (default: Solana mainnet)
315
+ - `secret` — HMAC secret for JWT signing (auto-generated if not provided)
316
+ - `facilitatorUrl` — Override facilitator (default: x402.dexter.cash)
317
+
318
+ **[Live demo →](https://dexter.cash/access-pass)**
319
+
320
+ ---
321
+
224
322
  ### Manual Server (Advanced)
225
323
 
226
324
  For more control over the payment flow:
@@ -429,6 +527,18 @@ tiktoken's default encoding works well for most transformer models. Only use a c
429
527
  | `maxAmountAtomic` | `string` | No | Maximum payment cap |
430
528
  | `verbose` | `boolean` | No | Enable debug logging |
431
529
 
530
+ ### `x402AccessPass(options)`
531
+
532
+ | Option | Type | Required | Description |
533
+ |--------|------|----------|-------------|
534
+ | `payTo` | `string` | Yes | Address to receive payments |
535
+ | `tiers` | `Record<string, string>` | One of `tiers` or `ratePerHour` | Named tiers (e.g., `{ '1h': '0.50' }`) |
536
+ | `ratePerHour` | `string` | One of `tiers` or `ratePerHour` | USD rate for custom durations |
537
+ | `network` | `string` | No | CAIP-2 network (default: Solana mainnet) |
538
+ | `secret` | `Buffer` | No | HMAC secret for JWT (auto-generated) |
539
+ | `facilitatorUrl` | `string` | No | Facilitator URL (default: x402.dexter.cash) |
540
+ | `verbose` | `boolean` | No | Enable debug logging |
541
+
432
542
  ### `useX402Payment(options)`
433
543
 
434
544
  Returns:
@@ -444,6 +554,27 @@ Returns:
444
554
  | `balances` | `Balance[]` | Token balances per chain |
445
555
  | `refreshBalances` | `function` | Manual refresh |
446
556
  | `reset` | `function` | Clear state |
557
+ | `accessPass` | `object?` | Active pass state (tier, expiresAt, remainingSeconds) |
558
+
559
+ ### `useAccessPass(options)`
560
+
561
+ | Option | Type | Required | Description |
562
+ |--------|------|----------|-------------|
563
+ | `wallets` | `{ solana?, evm? }` | Yes | Multi-chain wallets |
564
+ | `resourceUrl` | `string` | Yes | The x402 resource base URL |
565
+ | `preferredNetwork` | `string` | No | Prefer this network |
566
+ | `autoConnect` | `boolean` | No | Auto-fetch tiers on mount (default: true) |
567
+
568
+ Returns:
569
+
570
+ | Property | Type | Description |
571
+ |----------|------|-------------|
572
+ | `tiers` | `AccessPassTier[]?` | Available tiers from server |
573
+ | `pass` | `object?` | Active pass (jwt, tier, expiresAt, remainingSeconds) |
574
+ | `isPassValid` | `boolean` | Whether pass is active and not expired |
575
+ | `purchasePass` | `function` | Buy a pass for a tier or custom duration |
576
+ | `isPurchasing` | `boolean` | Purchase in progress |
577
+ | `fetch` | `function` | Fetch with auto pass inclusion |
447
578
 
448
579
  ---
449
580
 
@@ -466,5 +597,6 @@ MIT — see [LICENSE](./LICENSE)
466
597
  <p align="center">
467
598
  <a href="https://x402.dexter.cash">Dexter Facilitator</a> ·
468
599
  <a href="https://dexter.cash/sdk">Live Demo</a> ·
600
+ <a href="https://dexter.cash/access-pass">Access Pass Demo</a> ·
469
601
  <a href="https://dexter.cash/onboard">Become a Seller</a>
470
602
  </p>
@@ -1,7 +1,7 @@
1
- import { C as ChainAdapter } from '../types-CQGDK_7X.cjs';
2
- export { A as AdapterConfig, B as BalanceInfo, G as GenericWallet, S as SignedTransaction, W as WalletSet } from '../types-CQGDK_7X.cjs';
3
- export { A as ARBITRUM_ONE, B as BASE_MAINNET, h as BASE_SEPOLIA, j as ETHEREUM_MAINNET, E as EvmAdapter, k as EvmWallet, d as SOLANA_DEVNET, S as SOLANA_MAINNET, e as SOLANA_TESTNET, b as SolanaAdapter, f as SolanaWallet, a as createEvmAdapter, c as createSolanaAdapter, g as isEvmWallet, i as isSolanaWallet } from '../evm-BaoETN1Y.cjs';
4
- import '../types-B7T6dZ-y.cjs';
1
+ import { C as ChainAdapter } from '../types--r7urkVI.cjs';
2
+ export { A as AdapterConfig, B as BalanceInfo, G as GenericWallet, S as SignedTransaction, W as WalletSet } from '../types--r7urkVI.cjs';
3
+ export { A as ARBITRUM_ONE, B as BASE_MAINNET, h as BASE_SEPOLIA, j as ETHEREUM_MAINNET, E as EvmAdapter, k as EvmWallet, d as SOLANA_DEVNET, S as SOLANA_MAINNET, e as SOLANA_TESTNET, b as SolanaAdapter, f as SolanaWallet, a as createEvmAdapter, c as createSolanaAdapter, g as isEvmWallet, i as isSolanaWallet } from '../evm-BYjwU6ZW.cjs';
4
+ import '../types-CcVAaoro.cjs';
5
5
 
6
6
  /**
7
7
  * Create all default adapters
@@ -1,7 +1,7 @@
1
- import { C as ChainAdapter } from '../types-DNx7-QUN.js';
2
- export { A as AdapterConfig, B as BalanceInfo, G as GenericWallet, S as SignedTransaction, W as WalletSet } from '../types-DNx7-QUN.js';
3
- export { A as ARBITRUM_ONE, B as BASE_MAINNET, h as BASE_SEPOLIA, j as ETHEREUM_MAINNET, E as EvmAdapter, k as EvmWallet, d as SOLANA_DEVNET, S as SOLANA_MAINNET, e as SOLANA_TESTNET, b as SolanaAdapter, f as SolanaWallet, a as createEvmAdapter, c as createSolanaAdapter, g as isEvmWallet, i as isSolanaWallet } from '../evm-ZDwQi4QL.js';
4
- import '../types-B7T6dZ-y.js';
1
+ import { C as ChainAdapter } from '../types-BtpD4ULe.js';
2
+ export { A as AdapterConfig, B as BalanceInfo, G as GenericWallet, S as SignedTransaction, W as WalletSet } from '../types-BtpD4ULe.js';
3
+ export { A as ARBITRUM_ONE, B as BASE_MAINNET, h as BASE_SEPOLIA, j as ETHEREUM_MAINNET, E as EvmAdapter, k as EvmWallet, d as SOLANA_DEVNET, S as SOLANA_MAINNET, e as SOLANA_TESTNET, b as SolanaAdapter, f as SolanaWallet, a as createEvmAdapter, c as createSolanaAdapter, g as isEvmWallet, i as isSolanaWallet } from '../evm-71SZ7cjW.js';
4
+ import '../types-CcVAaoro.js';
5
5
 
6
6
  /**
7
7
  * Create all default adapters
@@ -614,10 +614,41 @@ function createX402Client(config) {
614
614
  rpcUrls = {},
615
615
  maxAmountAtomic,
616
616
  fetch: customFetch = globalThis.fetch,
617
- verbose = false
617
+ verbose = false,
618
+ accessPass: accessPassConfig
618
619
  } = config;
619
620
  const log = verbose ? console.log.bind(console, "[x402]") : () => {
620
621
  };
622
+ const passCache = /* @__PURE__ */ new Map();
623
+ function getCachedPass(url) {
624
+ try {
625
+ const host = new URL(url).host;
626
+ const cached = passCache.get(host);
627
+ if (cached && cached.expiresAt > Date.now() / 1e3 + 10) {
628
+ return cached.jwt;
629
+ }
630
+ if (cached) {
631
+ passCache.delete(host);
632
+ }
633
+ } catch {
634
+ }
635
+ return null;
636
+ }
637
+ function cachePass(url, jwt) {
638
+ try {
639
+ const host = new URL(url).host;
640
+ const parts = jwt.split(".");
641
+ if (parts.length === 3) {
642
+ const payload = JSON.parse(atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")));
643
+ if (payload.exp) {
644
+ passCache.set(host, { jwt, expiresAt: payload.exp });
645
+ log("Access pass cached for", host, "| expires:", new Date(payload.exp * 1e3).toISOString());
646
+ }
647
+ }
648
+ } catch {
649
+ log("Failed to cache access pass");
650
+ }
651
+ }
621
652
  const wallets = walletSet || {};
622
653
  if (legacyWallet && !wallets.solana && isSolanaWallet(legacyWallet)) {
623
654
  wallets.solana = legacyWallet;
@@ -652,13 +683,160 @@ function createX402Client(config) {
652
683
  function getRpcUrl(network, adapter) {
653
684
  return rpcUrls[network] || adapter.getDefaultRpcUrl(network);
654
685
  }
686
+ async function purchaseAccessPass(input, init, originalResponse, passInfo, url) {
687
+ let tierQuery = "";
688
+ if (accessPassConfig?.preferTier && passInfo.tiers) {
689
+ const match2 = passInfo.tiers.find((t) => t.id === accessPassConfig.preferTier);
690
+ if (match2) {
691
+ if (accessPassConfig.maxSpend && parseFloat(match2.price) > parseFloat(accessPassConfig.maxSpend)) {
692
+ throw new X402Error(
693
+ "access_pass_exceeds_max_spend",
694
+ `Access pass tier "${match2.id}" costs $${match2.price}, exceeds max spend $${accessPassConfig.maxSpend}`
695
+ );
696
+ }
697
+ tierQuery = `tier=${match2.id}`;
698
+ }
699
+ } else if (accessPassConfig?.preferDuration && passInfo.ratePerHour) {
700
+ tierQuery = `duration=${accessPassConfig.preferDuration}`;
701
+ } else if (passInfo.tiers && passInfo.tiers.length > 0) {
702
+ const cheapest = passInfo.tiers[0];
703
+ if (accessPassConfig?.maxSpend && parseFloat(cheapest.price) > parseFloat(accessPassConfig.maxSpend)) {
704
+ throw new X402Error(
705
+ "access_pass_exceeds_max_spend",
706
+ `Cheapest access pass costs $${cheapest.price}, exceeds max spend $${accessPassConfig?.maxSpend}`
707
+ );
708
+ }
709
+ tierQuery = `tier=${cheapest.id}`;
710
+ }
711
+ const passUrl = tierQuery ? url.includes("?") ? `${url}&${tierQuery}` : `${url}?${tierQuery}` : url;
712
+ log("Purchasing access pass:", tierQuery || "default tier");
713
+ const paymentRequiredHeader = originalResponse.headers.get("PAYMENT-REQUIRED");
714
+ if (!paymentRequiredHeader) return null;
715
+ let requirements;
716
+ try {
717
+ requirements = JSON.parse(atob(paymentRequiredHeader));
718
+ } catch {
719
+ return null;
720
+ }
721
+ const match = findPaymentOption(requirements.accepts);
722
+ if (!match) return null;
723
+ const { accept, adapter, wallet } = match;
724
+ if (adapter.name === "Solana" && !accept.extra?.feePayer) return null;
725
+ const USDC_MINTS = [
726
+ "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
727
+ "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
728
+ "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
729
+ "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
730
+ ];
731
+ const decimals = accept.extra?.decimals ?? (USDC_MINTS.includes(accept.asset) ? 6 : void 0);
732
+ if (typeof decimals !== "number") return null;
733
+ const paymentAmount = accept.amount || accept.maxAmountRequired;
734
+ if (!paymentAmount) return null;
735
+ const rpcUrl = getRpcUrl(accept.network, adapter);
736
+ const balance = await adapter.getBalance(accept, wallet, rpcUrl);
737
+ const requiredAmount = Number(paymentAmount) / Math.pow(10, decimals);
738
+ if (balance < requiredAmount) {
739
+ throw new X402Error(
740
+ "insufficient_balance",
741
+ `Insufficient balance for access pass. Have $${balance.toFixed(4)}, need $${requiredAmount.toFixed(4)}`
742
+ );
743
+ }
744
+ const signedTx = await adapter.buildTransaction(accept, wallet, rpcUrl);
745
+ let payload;
746
+ if (adapter.name === "EVM") {
747
+ payload = JSON.parse(signedTx.serialized);
748
+ } else {
749
+ payload = { transaction: signedTx.serialized };
750
+ }
751
+ const originalUrl = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
752
+ let resolvedResource = requirements.resource;
753
+ if (typeof requirements.resource === "string") {
754
+ try {
755
+ resolvedResource = new URL(requirements.resource, originalUrl).toString();
756
+ } catch {
757
+ }
758
+ } else if (requirements.resource && typeof requirements.resource === "object" && "url" in requirements.resource) {
759
+ const rObj = requirements.resource;
760
+ try {
761
+ resolvedResource = { ...rObj, url: new URL(rObj.url, originalUrl).toString() };
762
+ } catch {
763
+ }
764
+ }
765
+ const paymentSignature = {
766
+ x402Version: accept.x402Version ?? 2,
767
+ resource: resolvedResource,
768
+ accepted: accept,
769
+ payload
770
+ };
771
+ const paymentSignatureHeader = btoa(JSON.stringify(paymentSignature));
772
+ const passResponse = await customFetch(passUrl, {
773
+ ...init,
774
+ method: "POST",
775
+ headers: {
776
+ ...init?.headers || {},
777
+ "Content-Type": "application/json",
778
+ "PAYMENT-SIGNATURE": paymentSignatureHeader
779
+ }
780
+ });
781
+ if (!passResponse.ok) {
782
+ log("Pass purchase failed:", passResponse.status);
783
+ return null;
784
+ }
785
+ const accessPassJwt = passResponse.headers.get("ACCESS-PASS");
786
+ if (!accessPassJwt) {
787
+ return passResponse;
788
+ }
789
+ cachePass(url, accessPassJwt);
790
+ log("Access pass purchased and cached");
791
+ const retryResponse = await customFetch(input, {
792
+ ...init,
793
+ headers: {
794
+ ...init?.headers || {},
795
+ "Authorization": `Bearer ${accessPassJwt}`
796
+ }
797
+ });
798
+ return retryResponse;
799
+ }
655
800
  async function x402Fetch(input, init) {
656
- log("Making request:", input);
801
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
802
+ log("Making request:", url);
803
+ if (accessPassConfig) {
804
+ const cachedJwt = getCachedPass(url);
805
+ if (cachedJwt) {
806
+ log("Using cached access pass");
807
+ const passResponse = await customFetch(input, {
808
+ ...init,
809
+ headers: {
810
+ ...init?.headers || {},
811
+ "Authorization": `Bearer ${cachedJwt}`
812
+ }
813
+ });
814
+ if (passResponse.status !== 401 && passResponse.status !== 402) {
815
+ return passResponse;
816
+ }
817
+ log("Cached pass rejected (status", passResponse.status, "), purchasing new pass");
818
+ try {
819
+ passCache.delete(new URL(url).host);
820
+ } catch {
821
+ }
822
+ }
823
+ }
657
824
  const response = await customFetch(input, init);
658
825
  if (response.status !== 402) {
659
826
  return response;
660
827
  }
661
828
  log("Received 402 Payment Required");
829
+ const passTiersHeader = response.headers.get("X-ACCESS-PASS-TIERS");
830
+ if (accessPassConfig && passTiersHeader) {
831
+ log("Server offers access passes, purchasing...");
832
+ try {
833
+ const passInfo = JSON.parse(atob(passTiersHeader));
834
+ const passResponse = await purchaseAccessPass(input, init, response, passInfo, url);
835
+ if (passResponse) return passResponse;
836
+ } catch (e) {
837
+ log("Access pass purchase failed, falling back to per-request payment:", e);
838
+ }
839
+ }
662
840
  const paymentRequiredHeader = response.headers.get("PAYMENT-REQUIRED");
663
841
  if (!paymentRequiredHeader) {
664
842
  throw new X402Error(
@@ -875,7 +1053,8 @@ function wrapFetch(fetchImpl, options) {
875
1053
  // facilitatorUrl is reserved for future use when we add facilitator selection
876
1054
  rpcUrls,
877
1055
  maxAmountAtomic,
878
- verbose
1056
+ verbose,
1057
+ accessPass
879
1058
  } = options;
880
1059
  if (!walletPrivateKey && !evmPrivateKey) {
881
1060
  throw new Error("At least one wallet private key is required (walletPrivateKey or evmPrivateKey)");
@@ -893,7 +1072,8 @@ function wrapFetch(fetchImpl, options) {
893
1072
  rpcUrls,
894
1073
  maxAmountAtomic,
895
1074
  fetch: fetchImpl,
896
- verbose
1075
+ verbose,
1076
+ accessPass
897
1077
  };
898
1078
  const client = createX402Client(clientConfig);
899
1079
  return client.fetch.bind(client);