@dexterai/x402 1.4.0 → 1.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
@@ -40,7 +40,7 @@ This SDK handles the entire flow automatically—you just call `fetch()` and pay
40
40
 
41
41
  **Multi-chain.** Solana and Base (Ethereum L2) with the same API. Add wallets for both and the SDK picks the right one automatically.
42
42
 
43
- **Works out of the box.** Built-in RPC proxy, pre-flight balance checks, automatic retry on 402. Uses the [Dexter facilitator](https://x402.dexter.cash) by default—the only x402 facilitator with full Phantom wallet support on Solana mainnet.
43
+ **Works out of the box.** Built-in RPC proxy, pre-flight balance checks, automatic retry on 402. Uses the [Dexter facilitator](https://x402.dexter.cash) by default—Solana's most feature-rich x402 facilitator.
44
44
 
45
45
  ---
46
46
 
@@ -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,158 @@ 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
+ headers: {
775
+ ...init?.headers || {},
776
+ "PAYMENT-SIGNATURE": paymentSignatureHeader
777
+ }
778
+ });
779
+ if (!passResponse.ok) {
780
+ log("Pass purchase failed:", passResponse.status);
781
+ return null;
782
+ }
783
+ const accessPassJwt = passResponse.headers.get("ACCESS-PASS");
784
+ if (!accessPassJwt) {
785
+ return passResponse;
786
+ }
787
+ cachePass(url, accessPassJwt);
788
+ log("Access pass purchased and cached");
789
+ const retryResponse = await customFetch(input, {
790
+ ...init,
791
+ headers: {
792
+ ...init?.headers || {},
793
+ "Authorization": `Bearer ${accessPassJwt}`
794
+ }
795
+ });
796
+ return retryResponse;
797
+ }
655
798
  async function x402Fetch(input, init) {
656
- log("Making request:", input);
799
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
800
+ log("Making request:", url);
801
+ if (accessPassConfig) {
802
+ const cachedJwt = getCachedPass(url);
803
+ if (cachedJwt) {
804
+ log("Using cached access pass");
805
+ const passResponse = await customFetch(input, {
806
+ ...init,
807
+ headers: {
808
+ ...init?.headers || {},
809
+ "Authorization": `Bearer ${cachedJwt}`
810
+ }
811
+ });
812
+ if (passResponse.status !== 401 && passResponse.status !== 402) {
813
+ return passResponse;
814
+ }
815
+ log("Cached pass rejected (status", passResponse.status, "), purchasing new pass");
816
+ try {
817
+ passCache.delete(new URL(url).host);
818
+ } catch {
819
+ }
820
+ }
821
+ }
657
822
  const response = await customFetch(input, init);
658
823
  if (response.status !== 402) {
659
824
  return response;
660
825
  }
661
826
  log("Received 402 Payment Required");
827
+ const passTiersHeader = response.headers.get("X-ACCESS-PASS-TIERS");
828
+ if (accessPassConfig && passTiersHeader) {
829
+ log("Server offers access passes, purchasing...");
830
+ try {
831
+ const passInfo = JSON.parse(atob(passTiersHeader));
832
+ const passResponse = await purchaseAccessPass(input, init, response, passInfo, url);
833
+ if (passResponse) return passResponse;
834
+ } catch (e) {
835
+ log("Access pass purchase failed, falling back to per-request payment:", e);
836
+ }
837
+ }
662
838
  const paymentRequiredHeader = response.headers.get("PAYMENT-REQUIRED");
663
839
  if (!paymentRequiredHeader) {
664
840
  throw new X402Error(
@@ -875,7 +1051,8 @@ function wrapFetch(fetchImpl, options) {
875
1051
  // facilitatorUrl is reserved for future use when we add facilitator selection
876
1052
  rpcUrls,
877
1053
  maxAmountAtomic,
878
- verbose
1054
+ verbose,
1055
+ accessPass
879
1056
  } = options;
880
1057
  if (!walletPrivateKey && !evmPrivateKey) {
881
1058
  throw new Error("At least one wallet private key is required (walletPrivateKey or evmPrivateKey)");
@@ -893,7 +1070,8 @@ function wrapFetch(fetchImpl, options) {
893
1070
  rpcUrls,
894
1071
  maxAmountAtomic,
895
1072
  fetch: fetchImpl,
896
- verbose
1073
+ verbose,
1074
+ accessPass
897
1075
  };
898
1076
  const client = createX402Client(clientConfig);
899
1077
  return client.fetch.bind(client);