@dexterai/x402 2.0.0 → 3.0.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.
@@ -379,16 +379,40 @@ function createSolanaAdapter(config) {
379
379
  }
380
380
 
381
381
  // src/adapters/evm.ts
382
+ var PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
383
+ var X402_EXACT_PERMIT2_PROXY = "0x402085c248EeA27D92E8b30b2C58ed07f9E20001";
384
+ var PERMIT2_WITNESS_TYPES = {
385
+ PermitWitnessTransferFrom: [
386
+ { name: "permitted", type: "TokenPermissions" },
387
+ { name: "spender", type: "address" },
388
+ { name: "nonce", type: "uint256" },
389
+ { name: "deadline", type: "uint256" },
390
+ { name: "witness", type: "Witness" }
391
+ ],
392
+ TokenPermissions: [
393
+ { name: "token", type: "address" },
394
+ { name: "amount", type: "uint256" }
395
+ ],
396
+ Witness: [
397
+ { name: "to", type: "address" },
398
+ { name: "validAfter", type: "uint256" }
399
+ ]
400
+ };
401
+ var MAX_UINT256 = BigInt("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
382
402
  var BASE_MAINNET = "eip155:8453";
383
403
  var BASE_SEPOLIA = "eip155:84532";
384
404
  var ARBITRUM_ONE = "eip155:42161";
385
405
  var POLYGON = "eip155:137";
386
406
  var OPTIMISM = "eip155:10";
387
407
  var AVALANCHE = "eip155:43114";
408
+ var BSC_MAINNET = "eip155:56";
388
409
  var SKALE_BASE = "eip155:1187947933";
389
410
  var SKALE_BASE_SEPOLIA = "eip155:324705682";
390
411
  var ETHEREUM_MAINNET = "eip155:1";
412
+ var BSC_USDT = "0x55d398326f99059fF775485246999027B3197955";
413
+ var BSC_USDC = "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d";
391
414
  var CHAIN_IDS = {
415
+ [BSC_MAINNET]: 56,
392
416
  [BASE_MAINNET]: 8453,
393
417
  [BASE_SEPOLIA]: 84532,
394
418
  [ARBITRUM_ONE]: 42161,
@@ -400,6 +424,7 @@ var CHAIN_IDS = {
400
424
  [ETHEREUM_MAINNET]: 1
401
425
  };
402
426
  var DEFAULT_RPC_URLS2 = {
427
+ [BSC_MAINNET]: "https://bsc-dataseed1.binance.org",
403
428
  [BASE_MAINNET]: "https://api.dexter.cash/api/base/rpc",
404
429
  [BASE_SEPOLIA]: "https://sepolia.base.org",
405
430
  [ARBITRUM_ONE]: "https://arb1.arbitrum.io/rpc",
@@ -411,6 +436,7 @@ var DEFAULT_RPC_URLS2 = {
411
436
  [ETHEREUM_MAINNET]: "https://eth.llamarpc.com"
412
437
  };
413
438
  var USDC_ADDRESSES = {
439
+ [BSC_MAINNET]: BSC_USDC,
414
440
  [BASE_MAINNET]: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
415
441
  [BASE_SEPOLIA]: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
416
442
  [ARBITRUM_ONE]: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
@@ -421,6 +447,10 @@ var USDC_ADDRESSES = {
421
447
  [SKALE_BASE_SEPOLIA]: "0x2e08028E3C4c2356572E096d8EF835cD5C6030bD",
422
448
  [ETHEREUM_MAINNET]: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
423
449
  };
450
+ var BSC_STABLECOIN_ADDRESSES = {
451
+ [BSC_USDT]: { symbol: "USDT", decimals: 18 },
452
+ [BSC_USDC]: { symbol: "USDC", decimals: 18 }
453
+ };
424
454
  function isEvmWallet(wallet) {
425
455
  if (!wallet || typeof wallet !== "object") return false;
426
456
  const w = wallet;
@@ -428,7 +458,7 @@ function isEvmWallet(wallet) {
428
458
  }
429
459
  var EvmAdapter = class {
430
460
  name = "EVM";
431
- networks = [BASE_MAINNET, BASE_SEPOLIA, ETHEREUM_MAINNET, ARBITRUM_ONE];
461
+ networks = [BSC_MAINNET, BASE_MAINNET, BASE_SEPOLIA, ETHEREUM_MAINNET, ARBITRUM_ONE];
432
462
  config;
433
463
  log;
434
464
  constructor(config = {}) {
@@ -439,6 +469,7 @@ var EvmAdapter = class {
439
469
  canHandle(network) {
440
470
  if (this.networks.includes(network)) return true;
441
471
  if (network === "base") return true;
472
+ if (network === "bsc") return true;
442
473
  if (network === "ethereum") return true;
443
474
  if (network === "arbitrum") return true;
444
475
  if (network.startsWith("eip155:")) return true;
@@ -452,6 +483,7 @@ var EvmAdapter = class {
452
483
  return DEFAULT_RPC_URLS2[network];
453
484
  }
454
485
  if (network === "base") return DEFAULT_RPC_URLS2[BASE_MAINNET];
486
+ if (network === "bsc") return DEFAULT_RPC_URLS2[BSC_MAINNET];
455
487
  if (network === "ethereum") return DEFAULT_RPC_URLS2[ETHEREUM_MAINNET];
456
488
  if (network === "arbitrum") return DEFAULT_RPC_URLS2[ARBITRUM_ONE];
457
489
  return DEFAULT_RPC_URLS2[BASE_MAINNET];
@@ -471,6 +503,7 @@ var EvmAdapter = class {
471
503
  return parseInt(chainIdStr, 10);
472
504
  }
473
505
  if (network === "base") return 8453;
506
+ if (network === "bsc") return 56;
474
507
  if (network === "ethereum") return 1;
475
508
  if (network === "arbitrum") return 42161;
476
509
  return 8453;
@@ -520,13 +553,19 @@ var EvmAdapter = class {
520
553
  const paddedAddress = address.slice(2).toLowerCase().padStart(64, "0");
521
554
  return selector + paddedAddress;
522
555
  }
523
- async buildTransaction(accept, wallet, _rpcUrl) {
556
+ async buildTransaction(accept, wallet, rpcUrl) {
524
557
  if (!isEvmWallet(wallet)) {
525
558
  throw new Error("Invalid EVM wallet");
526
559
  }
527
560
  if (!wallet.address) {
528
561
  throw new Error("Wallet not connected");
529
562
  }
563
+ if (accept.scheme === "exact-approval") {
564
+ return this.buildApprovalTransaction(accept, wallet, rpcUrl);
565
+ }
566
+ if (accept.extra?.assetTransferMethod === "permit2") {
567
+ return this.buildPermit2Transaction(accept, wallet, rpcUrl);
568
+ }
530
569
  const { payTo, asset, extra } = accept;
531
570
  const amount = accept.amount ?? accept.maxAmountRequired;
532
571
  if (!amount) {
@@ -597,6 +636,393 @@ var EvmAdapter = class {
597
636
  signature
598
637
  };
599
638
  }
639
+ // ===========================================================================
640
+ // exact-approval: BSC and other chains without EIP-3009
641
+ // ===========================================================================
642
+ /**
643
+ * Build a payment transaction for chains that use the approval-based scheme.
644
+ * The facilitator's /supported response provides the EIP-712 domain and types
645
+ * in accept.extra, so the client doesn't hardcode any contract addresses.
646
+ */
647
+ async buildApprovalTransaction(accept, wallet, rpcUrl) {
648
+ const { payTo, asset, extra } = accept;
649
+ const amount = accept.amount ?? accept.maxAmountRequired;
650
+ if (!amount) {
651
+ throw new Error("Missing amount in payment requirements");
652
+ }
653
+ const facilitatorContract = extra?.facilitatorContract;
654
+ if (!facilitatorContract) {
655
+ throw new Error(
656
+ "exact-approval scheme requires extra.facilitatorContract from the facilitator. The /supported endpoint should provide this."
657
+ );
658
+ }
659
+ if (!wallet.signTypedData) {
660
+ throw new Error("Wallet does not support signTypedData (EIP-712)");
661
+ }
662
+ this.log("Building approval-based transaction:", {
663
+ from: wallet.address,
664
+ to: payTo,
665
+ amount,
666
+ asset,
667
+ network: accept.network,
668
+ facilitatorContract
669
+ });
670
+ const url = rpcUrl || this.getDefaultRpcUrl(accept.network);
671
+ const fee = extra?.fee ?? "0";
672
+ const totalNeeded = BigInt(amount) + BigInt(fee);
673
+ const currentAllowance = await this.readAllowance(url, asset, wallet.address, facilitatorContract);
674
+ if (currentAllowance < totalNeeded) {
675
+ if (!wallet.sendTransaction) {
676
+ throw new Error(
677
+ "BSC payments require a wallet that supports sendTransaction for the one-time token approval. Use createEvmKeypairWallet() or a browser wallet with transaction support."
678
+ );
679
+ }
680
+ const approvalAmount = this.calculateApprovalAmount(amount, fee, extra?.approvalStrategy);
681
+ this.log(`Approving ${approvalAmount} for ${facilitatorContract} (current allowance: ${currentAllowance})`);
682
+ const approveTxHash = await wallet.sendTransaction({
683
+ to: asset,
684
+ data: this.encodeApprove(facilitatorContract, approvalAmount),
685
+ value: 0n
686
+ });
687
+ this.log(`Approval tx sent: ${approveTxHash}`);
688
+ await this.waitForReceipt(url, approveTxHash);
689
+ this.log("Approval confirmed");
690
+ } else {
691
+ this.log("Sufficient allowance, skipping approval");
692
+ }
693
+ const nonceBytes = new Uint8Array(16);
694
+ (globalThis.crypto ?? (await import("crypto")).webcrypto).getRandomValues(nonceBytes);
695
+ const nonce = [...nonceBytes].reduce((acc, b) => acc * 256n + BigInt(b), 0n).toString();
696
+ const paymentIdBytes = new Uint8Array(32);
697
+ (globalThis.crypto ?? (await import("crypto")).webcrypto).getRandomValues(paymentIdBytes);
698
+ const paymentId = "0x" + [...paymentIdBytes].map((b) => b.toString(16).padStart(2, "0")).join("");
699
+ const now = Math.floor(Date.now() / 1e3);
700
+ const deadline = now + (accept.maxTimeoutSeconds || 300);
701
+ const eip712Domain = extra?.eip712Domain;
702
+ const domain = eip712Domain ? {
703
+ name: eip712Domain.name,
704
+ version: eip712Domain.version,
705
+ chainId: BigInt(eip712Domain.chainId),
706
+ verifyingContract: eip712Domain.verifyingContract
707
+ } : {
708
+ name: "DexterBSCFacilitator",
709
+ version: "1",
710
+ chainId: BigInt(this.getChainId(accept.network)),
711
+ verifyingContract: facilitatorContract
712
+ };
713
+ const types = extra?.eip712Types ?? {
714
+ Payment: [
715
+ { name: "from", type: "address" },
716
+ { name: "to", type: "address" },
717
+ { name: "token", type: "address" },
718
+ { name: "amount", type: "uint256" },
719
+ { name: "fee", type: "uint256" },
720
+ { name: "nonce", type: "uint256" },
721
+ { name: "deadline", type: "uint256" },
722
+ { name: "paymentId", type: "bytes32" }
723
+ ]
724
+ };
725
+ const message = {
726
+ from: wallet.address,
727
+ to: payTo,
728
+ token: asset,
729
+ amount: BigInt(amount),
730
+ fee: BigInt(fee),
731
+ nonce: BigInt(nonce),
732
+ deadline: BigInt(deadline),
733
+ paymentId
734
+ };
735
+ const signature = await wallet.signTypedData({
736
+ domain,
737
+ types,
738
+ primaryType: "Payment",
739
+ message
740
+ });
741
+ this.log("EIP-712 Payment signature obtained");
742
+ const payload = {
743
+ from: wallet.address,
744
+ to: payTo,
745
+ token: asset,
746
+ amount,
747
+ fee,
748
+ nonce,
749
+ deadline,
750
+ paymentId,
751
+ signature
752
+ };
753
+ return {
754
+ serialized: JSON.stringify(payload),
755
+ signature
756
+ };
757
+ }
758
+ // ===========================================================================
759
+ // Permit2: Universal ERC-20 payments via Uniswap's Permit2 contract
760
+ // ===========================================================================
761
+ /**
762
+ * Build a Permit2 payment transaction. Used when the facilitator signals
763
+ * assetTransferMethod: "permit2" in extra (e.g., BSC where EIP-3009 is unavailable).
764
+ *
765
+ * Flow:
766
+ * 1. Check if token has approved the Permit2 contract. If not, approve(Permit2, maxUint256).
767
+ * 2. Sign EIP-712 PermitWitnessTransferFrom against the Permit2 contract.
768
+ * 3. Return { permit2Authorization, signature } payload for the facilitator.
769
+ */
770
+ async buildPermit2Transaction(accept, wallet, rpcUrl) {
771
+ const { payTo, asset } = accept;
772
+ const amount = accept.amount ?? accept.maxAmountRequired;
773
+ if (!amount) {
774
+ throw new Error("Missing amount in payment requirements");
775
+ }
776
+ if (!wallet.signTypedData) {
777
+ throw new Error("Wallet does not support signTypedData (EIP-712)");
778
+ }
779
+ this.log("Building Permit2 transaction:", {
780
+ from: wallet.address,
781
+ to: payTo,
782
+ amount,
783
+ asset,
784
+ network: accept.network
785
+ });
786
+ const url = rpcUrl || this.getDefaultRpcUrl(accept.network);
787
+ const currentAllowance = await this.readAllowance(url, asset, wallet.address, PERMIT2_ADDRESS);
788
+ let approvalExtension;
789
+ if (currentAllowance < BigInt(amount)) {
790
+ const approveData = this.encodeApprove(PERMIT2_ADDRESS, MAX_UINT256);
791
+ if (wallet.signTransaction) {
792
+ this.log(`Signing Permit2 approval for relay (current allowance: ${currentAllowance})`);
793
+ const chainId2 = this.getChainId(accept.network);
794
+ const gasPrice = await this.readGasPrice(url);
795
+ const nonce2 = await this.readNonce(url, wallet.address);
796
+ const signedTx = await wallet.signTransaction({
797
+ to: asset,
798
+ data: approveData,
799
+ chainId: chainId2,
800
+ gas: 50000n,
801
+ // standard ERC-20 approve
802
+ gasPrice,
803
+ nonce: nonce2
804
+ });
805
+ approvalExtension = {
806
+ erc20ApprovalGasSponsoring: {
807
+ info: {
808
+ from: wallet.address,
809
+ asset,
810
+ spender: PERMIT2_ADDRESS,
811
+ amount: MAX_UINT256.toString(),
812
+ signedTransaction: signedTx,
813
+ version: "1"
814
+ }
815
+ }
816
+ };
817
+ this.log("Permit2 approval signed for facilitator relay");
818
+ } else if (wallet.sendTransaction) {
819
+ this.log(`Approving Permit2 directly (current allowance: ${currentAllowance})`);
820
+ const approveTxHash = await wallet.sendTransaction({
821
+ to: asset,
822
+ data: approveData,
823
+ value: 0n
824
+ });
825
+ this.log(`Permit2 approval tx sent: ${approveTxHash}`);
826
+ await this.waitForReceipt(url, approveTxHash);
827
+ this.log("Permit2 approval confirmed");
828
+ } else {
829
+ throw new Error(
830
+ "Permit2 payments require a wallet that supports signTransaction or sendTransaction for the one-time Permit2 approval. Use createEvmKeypairWallet() or a browser wallet with transaction support."
831
+ );
832
+ }
833
+ } else {
834
+ this.log("Sufficient Permit2 allowance, skipping approval");
835
+ }
836
+ const nonceBytes = new Uint8Array(32);
837
+ (globalThis.crypto ?? (await import("crypto")).webcrypto).getRandomValues(nonceBytes);
838
+ const nonce = [...nonceBytes].reduce((acc, b) => acc * 256n + BigInt(b), 0n);
839
+ const now = Math.floor(Date.now() / 1e3);
840
+ const validAfter = now - 600;
841
+ const deadline = now + (accept.maxTimeoutSeconds || 300);
842
+ const chainId = this.getChainId(accept.network);
843
+ const domain = {
844
+ name: "Permit2",
845
+ chainId: BigInt(chainId),
846
+ verifyingContract: PERMIT2_ADDRESS
847
+ };
848
+ const message = {
849
+ permitted: {
850
+ token: asset,
851
+ amount: BigInt(amount)
852
+ },
853
+ spender: X402_EXACT_PERMIT2_PROXY,
854
+ nonce,
855
+ deadline: BigInt(deadline),
856
+ witness: {
857
+ to: payTo,
858
+ validAfter: BigInt(validAfter)
859
+ }
860
+ };
861
+ const signature = await wallet.signTypedData({
862
+ domain,
863
+ types: PERMIT2_WITNESS_TYPES,
864
+ primaryType: "PermitWitnessTransferFrom",
865
+ message
866
+ });
867
+ this.log("Permit2 PermitWitnessTransferFrom signature obtained");
868
+ const payload = {
869
+ signature,
870
+ permit2Authorization: {
871
+ from: wallet.address,
872
+ permitted: {
873
+ token: asset,
874
+ amount
875
+ },
876
+ spender: X402_EXACT_PERMIT2_PROXY,
877
+ nonce: nonce.toString(),
878
+ deadline: String(deadline),
879
+ witness: {
880
+ to: payTo,
881
+ validAfter: String(validAfter)
882
+ }
883
+ }
884
+ };
885
+ return {
886
+ serialized: JSON.stringify(payload),
887
+ signature,
888
+ extensions: approvalExtension
889
+ };
890
+ }
891
+ /**
892
+ * Read ERC-20 allowance via raw eth_call (no viem dependency needed).
893
+ */
894
+ async readAllowance(rpcUrl, token, owner, spender) {
895
+ const selector = "0xdd62ed3e";
896
+ const paddedOwner = owner.slice(2).toLowerCase().padStart(64, "0");
897
+ const paddedSpender = spender.slice(2).toLowerCase().padStart(64, "0");
898
+ const data = selector + paddedOwner + paddedSpender;
899
+ try {
900
+ const response = await fetch(rpcUrl, {
901
+ method: "POST",
902
+ headers: { "Content-Type": "application/json" },
903
+ body: JSON.stringify({
904
+ jsonrpc: "2.0",
905
+ id: 1,
906
+ method: "eth_call",
907
+ params: [{ to: token, data }, "latest"]
908
+ })
909
+ });
910
+ const result = await response.json();
911
+ if (result.error || !result.result || result.result === "0x") return 0n;
912
+ return BigInt(result.result);
913
+ } catch {
914
+ return 0n;
915
+ }
916
+ }
917
+ /**
918
+ * Encode ERC-20 approve(address,uint256) calldata.
919
+ */
920
+ encodeApprove(spender, amount) {
921
+ const selector = "0x095ea7b3";
922
+ const paddedSpender = spender.slice(2).toLowerCase().padStart(64, "0");
923
+ const paddedAmount = amount.toString(16).padStart(64, "0");
924
+ return selector + paddedSpender + paddedAmount;
925
+ }
926
+ /**
927
+ * Wait for a transaction receipt by polling eth_getTransactionReceipt.
928
+ */
929
+ async waitForReceipt(rpcUrl, txHash, timeoutMs = 3e4) {
930
+ const start = Date.now();
931
+ while (Date.now() - start < timeoutMs) {
932
+ try {
933
+ const response = await fetch(rpcUrl, {
934
+ method: "POST",
935
+ headers: { "Content-Type": "application/json" },
936
+ body: JSON.stringify({
937
+ jsonrpc: "2.0",
938
+ id: 1,
939
+ method: "eth_getTransactionReceipt",
940
+ params: [txHash]
941
+ })
942
+ });
943
+ const result = await response.json();
944
+ if (result.result) {
945
+ if (result.result.status === "0x0") {
946
+ throw new Error(`Approval transaction reverted: ${txHash}`);
947
+ }
948
+ return;
949
+ }
950
+ } catch (err) {
951
+ if (err instanceof Error && err.message.includes("reverted")) throw err;
952
+ }
953
+ await new Promise((r) => setTimeout(r, 2e3));
954
+ }
955
+ throw new Error(`Approval transaction receipt timeout after ${timeoutMs}ms: ${txHash}`);
956
+ }
957
+ /**
958
+ * Read gas price via eth_gasPrice RPC call.
959
+ */
960
+ async readGasPrice(rpcUrl) {
961
+ try {
962
+ const response = await fetch(rpcUrl, {
963
+ method: "POST",
964
+ headers: { "Content-Type": "application/json" },
965
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_gasPrice", params: [] })
966
+ });
967
+ const result = await response.json();
968
+ return result.result ? BigInt(result.result) : 50000000n;
969
+ } catch {
970
+ return 50000000n;
971
+ }
972
+ }
973
+ /**
974
+ * Read transaction count (nonce) via eth_getTransactionCount RPC call.
975
+ */
976
+ async readNonce(rpcUrl, address) {
977
+ try {
978
+ const response = await fetch(rpcUrl, {
979
+ method: "POST",
980
+ headers: { "Content-Type": "application/json" },
981
+ body: JSON.stringify({
982
+ jsonrpc: "2.0",
983
+ id: 1,
984
+ method: "eth_getTransactionCount",
985
+ params: [address, "latest"]
986
+ })
987
+ });
988
+ const result = await response.json();
989
+ return result.result ? parseInt(result.result, 16) : 0;
990
+ } catch {
991
+ return 0;
992
+ }
993
+ }
994
+ /**
995
+ * Calculate how much to approve based on the facilitator's approval strategy.
996
+ * Buffered approvals reduce the number of on-chain approval txs for micropayments.
997
+ */
998
+ calculateApprovalAmount(paymentAmount, fee, strategy) {
999
+ const total = BigInt(paymentAmount) + BigInt(fee);
1000
+ if (!strategy || strategy.mode === "exact") {
1001
+ return total;
1002
+ }
1003
+ const multiple = BigInt(strategy.defaultMultiple ?? 10);
1004
+ const buffered = total * multiple;
1005
+ if (strategy.maxCapUsd) {
1006
+ const decimals = this.inferDecimals(paymentAmount);
1007
+ const maxCap = BigInt(Math.floor(strategy.maxCapUsd * Math.pow(10, decimals)));
1008
+ if (buffered > maxCap) return maxCap;
1009
+ }
1010
+ if (strategy.exactAboveUsd) {
1011
+ const decimals = this.inferDecimals(paymentAmount);
1012
+ const threshold = BigInt(Math.floor(strategy.exactAboveUsd * Math.pow(10, decimals)));
1013
+ if (BigInt(paymentAmount) > threshold) return total;
1014
+ }
1015
+ return buffered;
1016
+ }
1017
+ /**
1018
+ * Infer token decimals from payment amount magnitude.
1019
+ * BSC stablecoins use 18 decimals, all others use 6.
1020
+ * A $1 payment is 1000000 (6 dec) or 1000000000000000000 (18 dec).
1021
+ * If the amount has > 12 digits, it's almost certainly 18 decimals.
1022
+ */
1023
+ inferDecimals(amount) {
1024
+ return amount.length > 12 ? 18 : 6;
1025
+ }
600
1026
  };
601
1027
  function createEvmAdapter(config) {
602
1028
  return new EvmAdapter(config);
@@ -610,6 +1036,9 @@ function isKnownUSDC(asset) {
610
1036
  for (const addr of Object.values(USDC_ADDRESSES)) {
611
1037
  if (addr.toLowerCase() === lc) return true;
612
1038
  }
1039
+ for (const addr of Object.keys(BSC_STABLECOIN_ADDRESSES)) {
1040
+ if (addr.toLowerCase() === lc) return true;
1041
+ }
613
1042
  return false;
614
1043
  }
615
1044
 
@@ -718,6 +1147,19 @@ function createX402Client(config) {
718
1147
  }
719
1148
  return candidates[0];
720
1149
  }
1150
+ function getChainDisplayName(network, adapterName) {
1151
+ const names = {
1152
+ "eip155:56": "BSC",
1153
+ "eip155:8453": "Base",
1154
+ "eip155:84532": "Base Sepolia",
1155
+ "eip155:42161": "Arbitrum",
1156
+ "eip155:137": "Polygon",
1157
+ "eip155:10": "Optimism",
1158
+ "eip155:43114": "Avalanche",
1159
+ "eip155:1": "Ethereum"
1160
+ };
1161
+ return names[network] || adapterName;
1162
+ }
721
1163
  function getRpcUrl(network, adapter) {
722
1164
  return rpcUrls[network] || adapter.getDefaultRpcUrl(network);
723
1165
  }
@@ -810,6 +1252,9 @@ function createX402Client(config) {
810
1252
  accepted: accept,
811
1253
  payload
812
1254
  };
1255
+ if (signedTx.extensions) {
1256
+ paymentSignature.extensions = signedTx.extensions;
1257
+ }
813
1258
  const paymentSignatureHeader = btoa(JSON.stringify(paymentSignature));
814
1259
  const passResponse = await customFetch(passUrl, {
815
1260
  ...init,
@@ -932,10 +1377,10 @@ function createX402Client(config) {
932
1377
  const balance = await adapter.getBalance(accept, wallet, rpcUrl);
933
1378
  const requiredAmount = Number(paymentAmount) / Math.pow(10, decimals);
934
1379
  if (balance < requiredAmount) {
935
- const network = adapter.name === "EVM" ? "Base" : "Solana";
1380
+ const chainName = getChainDisplayName(accept.network, adapter.name);
936
1381
  throw new X402Error(
937
1382
  "insufficient_balance",
938
- `Insufficient USDC balance on ${network}. Have $${balance.toFixed(4)}, need $${requiredAmount.toFixed(4)}`
1383
+ `Insufficient balance on ${chainName}. Have $${balance.toFixed(4)}, need $${requiredAmount.toFixed(4)}`
939
1384
  );
940
1385
  }
941
1386
  log(`Balance OK: $${balance.toFixed(4)} >= $${requiredAmount.toFixed(4)}`);
@@ -988,6 +1433,9 @@ function createX402Client(config) {
988
1433
  accepted: accept,
989
1434
  payload
990
1435
  };
1436
+ if (signedTx.extensions) {
1437
+ paymentSignature.extensions = signedTx.extensions;
1438
+ }
991
1439
  const paymentSignatureHeader = btoa(JSON.stringify(paymentSignature));
992
1440
  log("Retrying request with payment...");
993
1441
  const retryResponse = await fetchWithRetry(input, {
@@ -1116,7 +1564,16 @@ async function createEvmKeypairWallet(privateKey) {
1116
1564
  const account = privateKeyToAccount(normalizedKey);
1117
1565
  return {
1118
1566
  address: account.address,
1119
- signTypedData: (params) => account.signTypedData(params)
1567
+ signTypedData: (params) => account.signTypedData(params),
1568
+ signTransaction: (params) => account.signTransaction({
1569
+ to: params.to,
1570
+ data: params.data,
1571
+ chainId: params.chainId,
1572
+ gas: params.gas,
1573
+ gasPrice: params.gasPrice,
1574
+ nonce: params.nonce,
1575
+ type: "legacy"
1576
+ })
1120
1577
  };
1121
1578
  }
1122
1579
  function isEvmKeypairWallet(wallet) {
@@ -1183,55 +1640,84 @@ function wrapFetch(fetchImpl, options) {
1183
1640
  }
1184
1641
 
1185
1642
  // src/client/discovery.ts
1186
- var DEFAULT_MARKETPLACE = "https://x402.dexter.cash/api/facilitator/marketplace/resources";
1187
- async function searchAPIs(options = {}) {
1643
+ var DEFAULT_CAPABILITY_ENDPOINT = "https://x402.dexter.cash/api/x402gle/capability";
1644
+ function formatPriceLabel(priceUsdc) {
1645
+ if (priceUsdc == null) return "price on request";
1646
+ if (priceUsdc === 0) return "free";
1647
+ if (priceUsdc < 0.01) return `$${priceUsdc.toFixed(4)}`;
1648
+ return `$${priceUsdc.toFixed(2)}`;
1649
+ }
1650
+ function mapResult(r) {
1651
+ return {
1652
+ resourceId: r.resourceId,
1653
+ name: r.displayName ?? r.resourceUrl,
1654
+ url: r.resourceUrl,
1655
+ method: r.method || "GET",
1656
+ price: formatPriceLabel(r.pricing.usdc),
1657
+ priceUsdc: r.pricing.usdc,
1658
+ network: r.pricing.network,
1659
+ description: r.description ?? "",
1660
+ category: r.category ?? "uncategorized",
1661
+ qualityScore: r.verification.qualityScore,
1662
+ verified: r.verification.status === "pass",
1663
+ verificationStatus: r.verification.status,
1664
+ totalCalls: r.usage.totalSettlements,
1665
+ totalVolumeUsdc: r.usage.totalVolumeUsdc,
1666
+ iconUrl: r.icon,
1667
+ host: r.host,
1668
+ gamingFlags: r.gaming.flags,
1669
+ gamingSuspicious: r.gaming.suspicious,
1670
+ tier: r.tier,
1671
+ similarity: Math.round(r.similarity * 1e3) / 1e3,
1672
+ why: r.why,
1673
+ score: r.score
1674
+ };
1675
+ }
1676
+ async function capabilitySearch(options) {
1677
+ if (!options?.query || !options.query.trim()) {
1678
+ throw new Error("capabilitySearch: query is required");
1679
+ }
1188
1680
  const {
1189
1681
  query,
1190
- category,
1191
- network,
1192
- maxPrice,
1193
- verifiedOnly,
1194
- sort = "marketplace",
1195
1682
  limit = 20,
1196
- marketplaceUrl = DEFAULT_MARKETPLACE
1683
+ unverified,
1684
+ testnets,
1685
+ rerank,
1686
+ endpoint = DEFAULT_CAPABILITY_ENDPOINT
1197
1687
  } = options;
1198
1688
  const params = new URLSearchParams();
1199
- if (query) params.set("search", query);
1200
- if (category) params.set("category", category);
1201
- if (network) params.set("network", network);
1202
- if (maxPrice !== void 0) params.set("maxPrice", String(maxPrice));
1203
- if (verifiedOnly) params.set("verified", "true");
1204
- params.set("sort", sort);
1205
- params.set("order", "desc");
1206
- params.set("limit", String(Math.min(limit, 50)));
1207
- const url = `${marketplaceUrl}?${params.toString()}`;
1689
+ params.set("q", query);
1690
+ params.set("limit", String(Math.min(Math.max(limit, 1), 50)));
1691
+ if (unverified) params.set("unverified", "true");
1692
+ if (testnets) params.set("testnets", "true");
1693
+ if (rerank === false) params.set("rerank", "false");
1694
+ const url = `${endpoint}?${params.toString()}`;
1208
1695
  const response = await fetch(url, {
1209
- headers: { "Accept": "application/json" },
1210
- signal: AbortSignal.timeout(15e3)
1696
+ headers: { Accept: "application/json" },
1697
+ signal: AbortSignal.timeout(2e4)
1211
1698
  });
1212
1699
  if (!response.ok) {
1213
- throw new Error(`Marketplace search failed: ${response.status}`);
1700
+ const body = await response.text().catch(() => "");
1701
+ throw new Error(`Capability search failed: ${response.status} ${body.slice(0, 400)}`);
1214
1702
  }
1215
1703
  const data = await response.json();
1216
- if (!data.resources) return [];
1217
- return data.resources.map((r) => ({
1218
- name: r.displayName || r.resourceUrl,
1219
- url: r.resourceUrl,
1220
- method: r.method || "GET",
1221
- price: r.priceLabel || (r.priceUsdc ? `$${r.priceUsdc.toFixed(4)}` : "free"),
1222
- priceUsdc: r.priceUsdc ?? null,
1223
- network: r.priceNetwork ?? null,
1224
- description: r.description || "",
1225
- category: r.category || "uncategorized",
1226
- qualityScore: r.qualityScore ?? null,
1227
- verified: r.verificationStatus === "pass",
1228
- totalCalls: r.totalSettlements || 0,
1229
- totalVolume: r.totalVolumeUsdc ? `$${r.totalVolumeUsdc.toLocaleString("en-US", { minimumFractionDigits: 2 })}` : null,
1230
- seller: r.seller?.displayName ?? null,
1231
- sellerReputation: r.reputationScore ?? null,
1232
- authRequired: r.authRequired || false,
1233
- lastActive: r.lastSettlementAt ?? null
1234
- }));
1704
+ if (!data.ok) {
1705
+ throw new Error(
1706
+ `Capability search error${data.stage ? ` at stage ${data.stage}` : ""}: ${data.error ?? "unknown"}`
1707
+ );
1708
+ }
1709
+ return {
1710
+ query: data.query,
1711
+ strongResults: data.strongResults.map(mapResult),
1712
+ relatedResults: data.relatedResults.map(mapResult),
1713
+ strongCount: data.strongCount,
1714
+ relatedCount: data.relatedCount,
1715
+ topSimilarity: data.topSimilarity,
1716
+ noMatchReason: data.noMatchReason,
1717
+ rerank: data.rerank,
1718
+ intent: data.intent,
1719
+ durationMs: data.durationMs
1720
+ };
1235
1721
  }
1236
1722
 
1237
1723
  // src/client/budget-account.ts
@@ -1368,6 +1854,7 @@ export {
1368
1854
  SOLANA_MAINNET,
1369
1855
  USDC_MINT,
1370
1856
  X402Error,
1857
+ capabilitySearch,
1371
1858
  createBudgetAccount,
1372
1859
  createEvmAdapter,
1373
1860
  createEvmKeypairWallet,
@@ -1380,6 +1867,5 @@ export {
1380
1867
  getSponsoredRecommendations,
1381
1868
  isEvmKeypairWallet,
1382
1869
  isKeypairWallet,
1383
- searchAPIs,
1384
1870
  wrapFetch
1385
1871
  };