@fangorn-network/westmarch 2026.10.29 → 2026.11.4-dev

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.
Files changed (360) hide show
  1. package/README.md +5 -5
  2. package/docs/app-to-agent.md +61 -625
  3. package/package.json +12 -17
  4. package/site/dist/assets/__vite-browser-external-D2U8RZpp.js +1 -0
  5. package/site/dist/assets/embed.worker-CmS57Zti.js +1 -0
  6. package/site/dist/assets/index-Djs8f08m.js +8 -0
  7. package/site/dist/assets/rolldown-runtime-COiEfhsA.js +1 -0
  8. package/site/dist/assets/{transformers.web-BTyOEJ0C.js → transformers.web-CwR_IYQV.js} +1 -1
  9. package/site/dist/index.html +3 -4
  10. package/site/worker.js +115 -338
  11. package/src/agent/agent-card.js +33 -45
  12. package/src/agent/mcp.js +168 -371
  13. package/src/agent/tools.js +2 -1
  14. package/src/agent/view-tools.js +4 -56
  15. package/src/agent/x402.js +182 -369
  16. package/src/core/roles.js +6 -6
  17. package/src/core/shard.js +4 -4
  18. package/src/discover/apps.js +67 -81
  19. package/src/discover/directory.js +2 -2
  20. package/src/market/reactions.js +21 -15
  21. package/src/publish/devnet.js +69 -18
  22. package/src/publish/init.js +1 -1
  23. package/src/publish/ship.js +74 -275
  24. package/src/publish/view.js +52 -53
  25. package/template/README.md +9 -11
  26. package/template/package.json +1 -1
  27. package/plugin/scripts/fx.mjs +0 -266
  28. package/site/dist/assets/AccountNotFoundScreen-C4njXc69-DyVH4CEz.js +0 -1
  29. package/site/dist/assets/AddFundsSelectionScreen-RAoCn7ej-Dz1--SS9.js +0 -31
  30. package/site/dist/assets/Address-H8kD1d9O-BdFZmRwI.js +0 -17
  31. package/site/dist/assets/AffirmativeConsentScreen-BG0MJxku-CxaCAisL.js +0 -6
  32. package/site/dist/assets/AllowlistRejectionScreen-Cae6e1Od-JGk7vndN.js +0 -1
  33. package/site/dist/assets/ApiController-CTLbMCcH.js +0 -5
  34. package/site/dist/assets/ArrowsRightLeftIcon-B8g9adpz.js +0 -1
  35. package/site/dist/assets/AuthenticateWithWalletScreen-CU6hIcFT-CXD9_OP8.js +0 -1
  36. package/site/dist/assets/AwaitingEvmToSolBridgingScreen-D0e_sZMS-BtJCglvU.js +0 -1
  37. package/site/dist/assets/AwaitingExternalEthereumTransferScreen-ur5HL4R7-BgC2TsqH.js +0 -1
  38. package/site/dist/assets/AwaitingPasswordlessCodeScreen-DRHz76mM-B9OzLeEO.js +0 -103
  39. package/site/dist/assets/AwaitingSolToEvmBridgingScreen-CdLRrGD2-C7r5snUL.js +0 -1
  40. package/site/dist/assets/BridgeNetworkSelectionView-CFGjB2nO-BTV0NRio.js +0 -68
  41. package/site/dist/assets/CaptchaScreen-DbpvkIGx-7rSZIuM-.js +0 -1
  42. package/site/dist/assets/CheckCircleIcon-B1l17ioc.js +0 -1
  43. package/site/dist/assets/CheckCircleIcon-BGSI8nKf.js +0 -1
  44. package/site/dist/assets/Checkbox-D1EDeo41-BUfq0w99.js +0 -52
  45. package/site/dist/assets/ChevronDownIcon-B6uJvPHu.js +0 -1
  46. package/site/dist/assets/Chip-CZKIKt9K-r9Nk__0-.js +0 -26
  47. package/site/dist/assets/CoinbaseOnrampStatusScreen-BbyCJDXL-DWWNHxmR.js +0 -16
  48. package/site/dist/assets/ConnectEmailForm-B-VRE0MN-CRQwS0Xu.js +0 -3
  49. package/site/dist/assets/ConnectLedgerScreen-DWHABGot-CZcCt31O.js +0 -10
  50. package/site/dist/assets/ConnectOnlyLandingScreen-CZYx9Bmp-C5IOf-SQ.js +0 -1
  51. package/site/dist/assets/ConnectOnlyStatusScreen-DCRzjxHx-cwg74F3A.js +0 -1
  52. package/site/dist/assets/ConnectOrCreateScreen-CUUYUO07-D28oeK6b.js +0 -1
  53. package/site/dist/assets/ConnectPhoneForm-Dek5xn4C-EEdkKeTp.js +0 -95
  54. package/site/dist/assets/ConnectWalletView-DqI-PTvX-C2mEzukj.js +0 -74
  55. package/site/dist/assets/ConnectionStatusScreen-fUfROm9m-CH_7r8iV.js +0 -7
  56. package/site/dist/assets/ConnectionStatusScreen-fUfROm9m-Z33kis0B.js +0 -1
  57. package/site/dist/assets/ConnectorUtil-DH4NePk5.js +0 -21
  58. package/site/dist/assets/ConstantsUtil-DmLsRHAl.js +0 -198
  59. package/site/dist/assets/CopyToClipboard-i_OQSBJr-_G88JCVR.js +0 -39
  60. package/site/dist/assets/CopyableText-CQapvaMr-CGGfc_zM.js +0 -28
  61. package/site/dist/assets/CrossAppAuthScreen-CLw7ymZq-C9Okoc0V.js +0 -1
  62. package/site/dist/assets/CustomLandingScreenView-Cm_MBSyX-D2l6hdSl.js +0 -22
  63. package/site/dist/assets/DelegatedActionsConsentScreen-DYF2U4y5-Dda3KKqS.js +0 -1
  64. package/site/dist/assets/DelegatedActionsRevokeScreen-bPp6Uwyh-DG8kJPt1.js +0 -1
  65. package/site/dist/assets/DepositAddressAddressStepView-4vdkb55L-CcOUfn6f.js +0 -187
  66. package/site/dist/assets/DepositAddressScreen-DBLRs7aI-DeituDNh.js +0 -8
  67. package/site/dist/assets/DepositAddressV3Screen-CDqIEMQw-FDg6K14-.js +0 -1
  68. package/site/dist/assets/EmailInputForm-Cqo1mda8-BmbxvPHC.js +0 -124
  69. package/site/dist/assets/EmbeddedWalletConnectingScreen-D4T7DUzs-B05CQ9rx.js +0 -1
  70. package/site/dist/assets/EmbeddedWalletCreatedScreen-D7WhW5ve-Cxd1b9XH.js +0 -1
  71. package/site/dist/assets/EmbeddedWalletKeyExportScreen-CqV4NYQ3-CrJy0ViX.js +0 -23
  72. package/site/dist/assets/EmbeddedWalletOnAccountCreateScreen-BoKb3Q2S-Bakaa3zu.js +0 -1
  73. package/site/dist/assets/EmbeddedWalletPasswordUpdateScreen-D3rMlqS7-j78vKrcj.js +0 -1
  74. package/site/dist/assets/EmbeddedWalletPasswordUpdateSplashScreen-BPFkWJeE-D3dr7W9_.js +0 -1
  75. package/site/dist/assets/EnrollTotp-Dibew4dp-BXbYV8nI.js +0 -66
  76. package/site/dist/assets/EnvelopeIcon-DjPofnRI.js +0 -1
  77. package/site/dist/assets/ErrorBanner-BcpGRt0h-BhH6_V-9.js +0 -21
  78. package/site/dist/assets/ErrorMessage-D8VaAP5m-Bs7uX2a9.js +0 -8
  79. package/site/dist/assets/ErrorScreen-DipdTWzE-BgPRgnOb.js +0 -4
  80. package/site/dist/assets/ErrorScreen-DipdTWzE-CvxXC6pz.js +0 -1
  81. package/site/dist/assets/ExclamationCircleIcon-BE-WGrut.js +0 -1
  82. package/site/dist/assets/ExclamationTriangleIcon-Ca1Xlc_f.js +0 -1
  83. package/site/dist/assets/FarcasterConnectStatusScreen-51tjUxqq-De2JvZdV.js +0 -75
  84. package/site/dist/assets/FarcasterSignerStatusScreen-6x-Uh95G-bLp3hXoO.js +0 -28
  85. package/site/dist/assets/FiatOnrampScreen-BAhYeiTI-Jr6D6JiZ.js +0 -491
  86. package/site/dist/assets/FingerPrintIcon-DOCbS93y.js +0 -1
  87. package/site/dist/assets/FundSolWalletWithExternalSolanaWallet-WtLwYaOl-A_G5kfa1.js +0 -1
  88. package/site/dist/assets/FundWalletMethodHeader-BWNkokCT-LPv35wkD.js +0 -1
  89. package/site/dist/assets/FundWithBankDepositScreen-DajkHxBD-0hfGrfvf.js +0 -58
  90. package/site/dist/assets/FundingEditAmountScreen-CpvpiO4M-Dz0s_NWR.js +0 -1
  91. package/site/dist/assets/FundingMethodSelectionScreen-Db7vhRlE-ZGm-3-Io.js +0 -1
  92. package/site/dist/assets/GlobeAltIcon-BBRl2MMn.js +0 -1
  93. package/site/dist/assets/GooglePay-B53WnudL-BGiVpB8y.js +0 -1
  94. package/site/dist/assets/HCaptchaWrapper-CUi9LJsp-CHdfIeD_.js +0 -4
  95. package/site/dist/assets/InAppBrowserLoginNotPossible-BMR9br1T-Cgoiow3m.js +0 -1
  96. package/site/dist/assets/InfoBanner-Cb3p1z12-_YqQmRiu.js +0 -22
  97. package/site/dist/assets/InjectedWalletIcon-DLcYOGDj-tuIrKQHH.js +0 -1
  98. package/site/dist/assets/InstallWalletScreen-DTA8dwLv-BMU_wa61.js +0 -1
  99. package/site/dist/assets/JsonTree-BHzNC-ic-DcVr0YF0.js +0 -34
  100. package/site/dist/assets/LabelXs-oqZNqbm_-CNprGVQM.js +0 -6
  101. package/site/dist/assets/LandingScreen-CGWUoSUG-fH6rsQen.js +0 -1
  102. package/site/dist/assets/Layouts-BMRfo5hw-e-sepi6F.js +0 -77
  103. package/site/dist/assets/Link-BdDilT2T-BdwlRJch.js +0 -40
  104. package/site/dist/assets/LinkConflictScreen-C3ZEPsgl-BXKN2Qm5.js +0 -77
  105. package/site/dist/assets/LinkEmailScreen-dKo9frwc-Cyn7Q8p1.js +0 -1
  106. package/site/dist/assets/LinkPasskeyScreen-zbrl_AI_-CWiqnwLM.js +0 -117
  107. package/site/dist/assets/LinkPasskeyScreen-zbrl_AI_-YmDevJzf.js +0 -1
  108. package/site/dist/assets/LinkPhoneScreen-oIUpfj0w-h7shW-Ay.js +0 -1
  109. package/site/dist/assets/Loader-UAZqxsnw-DJEVblm1.js +0 -62
  110. package/site/dist/assets/LoadingSkeleton-BMsgO5PV-D2atjsP5.js +0 -17
  111. package/site/dist/assets/LockClosedIcon-9EFzWK1C.js +0 -1
  112. package/site/dist/assets/LoginFailedScreen-CN0dJV6U-By3Si77a.js +0 -1
  113. package/site/dist/assets/ManualTransferScreen-CHqcuqbs-BAHPY8rs.js +0 -1
  114. package/site/dist/assets/MfaAuthEnrollmentFlowScreen-Dj7gPTRC-CLjffFG1.js +0 -1
  115. package/site/dist/assets/MfaAuthVerifyFlowScreen-DbFpyWMy-BmQg1Y61.js +0 -1
  116. package/site/dist/assets/MfaEnrollmentFlowScreen-DhLnGgnr-DmBtTtlw.js +0 -1
  117. package/site/dist/assets/MfaVerifyFlowScreen-C_XPjMRP-4a683Bmf.js +0 -1
  118. package/site/dist/assets/ModalFooter-BCuEK_nz-C_71qsaz.js +0 -403
  119. package/site/dist/assets/MoonpayStatusScreen-CzTUtokc-DEHHcEf8.js +0 -16
  120. package/site/dist/assets/OAuthStatusScreen-ufbOa-da-BjAb_WsY.js +0 -1
  121. package/site/dist/assets/OpenLink-CUpJ1mOr-BLWFsPOW.js +0 -39
  122. package/site/dist/assets/PasskeySelectSignupOrLogin-xh2ScC_l-2PJTJxX-.js +0 -1
  123. package/site/dist/assets/PasskeyStatusScreen-Bla8MaOk-DCEg761l.js +0 -6
  124. package/site/dist/assets/PasswordRecoveryScreen-awGXZkQk-8nVjNc8k.js +0 -20
  125. package/site/dist/assets/PhArrowCircleDown-D97G-BOA.js +0 -14
  126. package/site/dist/assets/PhArrowClockwise-DVgyFuez.js +0 -14
  127. package/site/dist/assets/PhArrowDown-87b1BizM.js +0 -14
  128. package/site/dist/assets/PhArrowLeft-Da0MfnbK.js +0 -14
  129. package/site/dist/assets/PhArrowRight-AdrIL4DW.js +0 -14
  130. package/site/dist/assets/PhArrowSquareOut-Bk2CMG3I.js +0 -14
  131. package/site/dist/assets/PhArrowUp-4sgyKPGx.js +0 -14
  132. package/site/dist/assets/PhArrowUpRight-BNghD2zq.js +0 -14
  133. package/site/dist/assets/PhArrowsClockwise-BXRn1GEl.js +0 -14
  134. package/site/dist/assets/PhArrowsDownUp-DM09Jo_3.js +0 -14
  135. package/site/dist/assets/PhArrowsLeftRight-MuKBLUoF.js +0 -14
  136. package/site/dist/assets/PhBank-fHbgCqqZ.js +0 -14
  137. package/site/dist/assets/PhBrowser-2-9m-AkM.js +0 -14
  138. package/site/dist/assets/PhCaretDown-BDNabFuL.js +0 -14
  139. package/site/dist/assets/PhCaretLeft-CI460R-A.js +0 -14
  140. package/site/dist/assets/PhCaretRight-D7ZS3Ls8.js +0 -14
  141. package/site/dist/assets/PhCaretUp-C-iIfyvO.js +0 -14
  142. package/site/dist/assets/PhCheck-DxQXn57r.js +0 -14
  143. package/site/dist/assets/PhCircleHalf-MJZeRBGF.js +0 -14
  144. package/site/dist/assets/PhClock-x3MS3Mwi.js +0 -14
  145. package/site/dist/assets/PhCompass-hYn3HhGN.js +0 -14
  146. package/site/dist/assets/PhCopy-CWNYcABI.js +0 -14
  147. package/site/dist/assets/PhCreditCard-Fy4pzwtt.js +0 -14
  148. package/site/dist/assets/PhCurrencyDollar-BgiaenCi.js +0 -14
  149. package/site/dist/assets/PhDesktop-CxrzCSv5.js +0 -14
  150. package/site/dist/assets/PhDeviceMobile-CsyA5Goi.js +0 -14
  151. package/site/dist/assets/PhDotsThree-D-k1BiYg.js +0 -14
  152. package/site/dist/assets/PhEnvelope-D8kzeNox.js +0 -14
  153. package/site/dist/assets/PhFunnelSimple-CfQs5DAW.js +0 -14
  154. package/site/dist/assets/PhGlobe-DvmSc9_A.js +0 -14
  155. package/site/dist/assets/PhIdentificationCard-CfkZm-5K.js +0 -14
  156. package/site/dist/assets/PhImage-Cf0J2ZfJ.js +0 -14
  157. package/site/dist/assets/PhInfo-Bwi51-2X.js +0 -14
  158. package/site/dist/assets/PhLightbulb-C4FZV7Es.js +0 -14
  159. package/site/dist/assets/PhMagnifyingGlass-C9u8jSCJ.js +0 -14
  160. package/site/dist/assets/PhPaperPlaneRight-zxQVvPLX.js +0 -14
  161. package/site/dist/assets/PhPlus-C4UoW-2n.js +0 -14
  162. package/site/dist/assets/PhPower-CFyWDmgD.js +0 -14
  163. package/site/dist/assets/PhPuzzlePiece-BjoNVfST.js +0 -14
  164. package/site/dist/assets/PhQrCode-CP3tFauY.js +0 -14
  165. package/site/dist/assets/PhQuestion-wOWJBd3G.js +0 -14
  166. package/site/dist/assets/PhQuestionMark-CV0k9C0Y.js +0 -14
  167. package/site/dist/assets/PhSealCheck-vIKzREuN.js +0 -14
  168. package/site/dist/assets/PhSignOut-CxELV-i0.js +0 -14
  169. package/site/dist/assets/PhSpinner-CGDmbzUd.js +0 -14
  170. package/site/dist/assets/PhTrash-C9PD0SAt.js +0 -14
  171. package/site/dist/assets/PhUser-0sWNXQeZ.js +0 -14
  172. package/site/dist/assets/PhVault-6JTskUcC.js +0 -14
  173. package/site/dist/assets/PhWarning-CoKs3gz6.js +0 -14
  174. package/site/dist/assets/PhWarningCircle-BXHw1fCt.js +0 -14
  175. package/site/dist/assets/PhX-DX0SFIqC.js +0 -14
  176. package/site/dist/assets/PhoneIcon-CZ47md80.js +0 -1
  177. package/site/dist/assets/PinInput-DbZ0b1i1-BrTXu3Km.js +0 -247
  178. package/site/dist/assets/PrivyPluginContext-DA-yS5vO-CY60yubk.js +0 -1
  179. package/site/dist/assets/QrCode-DbnZwlkH-CcMrWV5D.js +0 -22
  180. package/site/dist/assets/RecoveryOAuthStatusScreen-CsgtTI2x-DgeDnHOK.js +0 -1
  181. package/site/dist/assets/RecoveryPasswordCreateScreen-Be-qJ-LU-CvJJwLhK.js +0 -1
  182. package/site/dist/assets/RecoverySelectionScreen-D1iGtpmi-CADtGtSV.js +0 -7
  183. package/site/dist/assets/SIWXUtil-Cpiv91-M.js +0 -1
  184. package/site/dist/assets/Screen-D30meWsY-vEusJ7KX.js +0 -175
  185. package/site/dist/assets/ScreenHeader-CHmc4-Lu-BvGTLg53.js +0 -31
  186. package/site/dist/assets/ScreenLayout-OwaQQcxH-cfOTmyl0.js +0 -1
  187. package/site/dist/assets/SelectSourceAsset-CWFog6gm-DxYklZYs.js +0 -140
  188. package/site/dist/assets/SetAutomaticRecoveryScreen-BO_OEqAZ-oeqeo05x.js +0 -14
  189. package/site/dist/assets/SetWalletPasswordForm-DwCTVtAh-BxFee9UA.js +0 -11
  190. package/site/dist/assets/ShieldCheckIcon-eFjsosCA.js +0 -1
  191. package/site/dist/assets/SignRequestScreen-BHKv32KR-z8ixfIma.js +0 -26
  192. package/site/dist/assets/SolanaWalletConnector-C7Lqk-Wz-Bc_lvdiF.js +0 -1
  193. package/site/dist/assets/StackedContainer-B2vaEl56-Do5dXB8-.js +0 -24
  194. package/site/dist/assets/StandardSignAndSendTransactionScreen-CfhIXVtd-DudetZAc.js +0 -20
  195. package/site/dist/assets/Subtitle-CV-2yKE4-DV0o1av3.js +0 -13
  196. package/site/dist/assets/TelegramAuthScreen-UL9zLfO8-Bo3sONvf.js +0 -1
  197. package/site/dist/assets/Title-BnzYV3Is-C3qOPnO_.js +0 -7
  198. package/site/dist/assets/TodoList-DnyULl18-BvxvubbF.js +0 -36
  199. package/site/dist/assets/TransactionDetails-e9nesl30-WWflkOIi.js +0 -109
  200. package/site/dist/assets/TransactionErrorView-BDBNqA_f-CotYKcgf.js +0 -178
  201. package/site/dist/assets/TransferFromWalletScreen-Qvn1Yuno-sVFlWqa8.js +0 -31
  202. package/site/dist/assets/TransferOrBridgeLoadingScreen-Dl1I_iIz-CV2-mY19.js +0 -6
  203. package/site/dist/assets/TurnstileWrapper-B7sNddys-oPbuMTQt.js +0 -1
  204. package/site/dist/assets/UpdateEmailScreen-DVOhPV0H-D_mjSpAy.js +0 -1
  205. package/site/dist/assets/UpdatePhoneScreen-CXRswTNZ-6RLG0ahv.js +0 -1
  206. package/site/dist/assets/UserLimitReachedScreen-5we2DHn8-CaVN0bti.js +0 -1
  207. package/site/dist/assets/Value-DTgR824E-Bp6WGZZm.js +0 -27
  208. package/site/dist/assets/WalletCards-DH1rqayz-D3cOTb9x.js +0 -1
  209. package/site/dist/assets/WalletIcon-DOIZhb2l.js +0 -1
  210. package/site/dist/assets/WalletInfoCard-sq_2wgFg-bOqfl5MA.js +0 -29
  211. package/site/dist/assets/WalletInterstitialScreen-Sh7LfsAh-DWaVkMCA.js +0 -1
  212. package/site/dist/assets/WalletLink-DK5LB5Ha-DjUZvqs-.js +0 -20
  213. package/site/dist/assets/WalletOverflowButton-bfCnp-9T-B6P7DQXA.js +0 -1
  214. package/site/dist/assets/WarningBanner-ZZqCEtZK-Bc2YKZb7.js +0 -21
  215. package/site/dist/assets/__vite-browser-external-NciY4Vu1.js +0 -1
  216. package/site/dist/assets/_esm-BZP3cXTv.js +0 -1
  217. package/site/dist/assets/_esm-ZmzBJpEs.js +0 -6
  218. package/site/dist/assets/accounts-D4AXF17G.js +0 -1
  219. package/site/dist/assets/accounts-IkdFx9nq.js +0 -1
  220. package/site/dist/assets/analytics-mkkvFRju-BRBcadka.js +0 -1
  221. package/site/dist/assets/baseSepolia-Bn1j1Ysw.js +0 -1
  222. package/site/dist/assets/basic-Cz1YiCSk.js +0 -1575
  223. package/site/dist/assets/capitalizeFirstLetter-DmLYqXsO-DtOXx-2n.js +0 -1
  224. package/site/dist/assets/ccip-B1J3oTZx.js +0 -7
  225. package/site/dist/assets/ccip-Bd7261zy.js +0 -1
  226. package/site/dist/assets/ccip-D6EHKpjI.js +0 -1
  227. package/site/dist/assets/chain-scefkYIH.js +0 -5
  228. package/site/dist/assets/check-DHnllI8N.js +0 -1
  229. package/site/dist/assets/chevron-down-1y-r4bGO.js +0 -1
  230. package/site/dist/assets/circle-alert-OwxamXUX.js +0 -1
  231. package/site/dist/assets/circle-check-big-3sZGVC10.js +0 -1
  232. package/site/dist/assets/circle-x-DQjNUV9J.js +0 -1
  233. package/site/dist/assets/client-B-BpRtwY.js +0 -1
  234. package/site/dist/assets/client-BfIR1FvH.js +0 -9
  235. package/site/dist/assets/coinbase-DCPC2L_R-YEuIn3jh.js +0 -3
  236. package/site/dist/assets/copy-DMOX_yn7.js +0 -1
  237. package/site/dist/assets/core-GIEmxmAa.js +0 -12
  238. package/site/dist/assets/createLucideIcon-BsERoFTA.js +0 -1
  239. package/site/dist/assets/defineChain-B-SQDLTG.js +0 -1
  240. package/site/dist/assets/dijkstra-CY95afk4.js +0 -1
  241. package/site/dist/assets/dist-CoS6E-3x.js +0 -1
  242. package/site/dist/assets/dist-Cp6IXeJd.js +0 -1
  243. package/site/dist/assets/dist-CrNxmdZj.js +0 -1
  244. package/site/dist/assets/dist-DYGtf4F0.js +0 -16
  245. package/site/dist/assets/embed.worker-Cnl4VGr3.js +0 -1
  246. package/site/dist/assets/errors-BQ3N-RVt-CZPAWCgC.js +0 -1
  247. package/site/dist/assets/esm-C3DslVJx.js +0 -1
  248. package/site/dist/assets/esm-Ci1ogKuK.js +0 -1
  249. package/site/dist/assets/esm-Co94ENUV.js +0 -37
  250. package/site/dist/assets/esm-bb8SANZh.js +0 -1
  251. package/site/dist/assets/esm-n2lWo7kz.js +0 -1
  252. package/site/dist/assets/ethers-zTjbodk5-Cs648CnD.js +0 -1
  253. package/site/dist/assets/eventemitter3-Dzc_0Uq2.js +0 -1
  254. package/site/dist/assets/external-link-BBPG3rG7.js +0 -1
  255. package/site/dist/assets/farcaster-DPlSjvF5-CzBCmgyL.js +0 -1
  256. package/site/dist/assets/features-CuiHbgEJ.js +0 -2
  257. package/site/dist/assets/fingerprint-pattern-CjLtyIxB.js +0 -1
  258. package/site/dist/assets/floating-ui.react-DmaCfayr.js +0 -1
  259. package/site/dist/assets/floating-ui.react-dom-CaoMF8mP.js +0 -1
  260. package/site/dist/assets/formatErc20TokenAmount-BuPk9xcy-BVqOKQif.js +0 -1
  261. package/site/dist/assets/formatter-CVbp4SlJ.js +0 -1
  262. package/site/dist/assets/get-entropy-details-for-user-dHRH5Gpi-BNiOFXbK.js +0 -1
  263. package/site/dist/assets/getChainName-DjpPdUSc-DUyKDrnp.js +0 -1
  264. package/site/dist/assets/getErc20Balance-jXNpFf0N-q4bV8kQq.js +0 -1
  265. package/site/dist/assets/getErc20TokenInfo-BNTrzLMP-DcnTFrXN.js +0 -1
  266. package/site/dist/assets/getFormattedUsdFromLamports-De3U9GlO-Gx4_wxON.js +0 -1
  267. package/site/dist/assets/getUsdcMintAddress-BVgm9GRo-r2UnDeEy.js +0 -1
  268. package/site/dist/assets/guards-BIW-GiMJ-D3tWTQAR.js +0 -1
  269. package/site/dist/assets/hooks.module-BYZGfwDl.js +0 -1
  270. package/site/dist/assets/hourglass-D_GfQdJr.js +0 -1
  271. package/site/dist/assets/id-UqEwL2BI-CTlvRMA5.js +0 -1
  272. package/site/dist/assets/index-BdEw5wyl-kLbhqLbv.js +0 -1
  273. package/site/dist/assets/index-C2h3EneX.js +0 -8
  274. package/site/dist/assets/index-CWARkn2w-DxuckIFn.js +0 -61
  275. package/site/dist/assets/index-DvMddJcd-CNBb4VUn.js +0 -12
  276. package/site/dist/assets/index-YiCw0Y_x-Czl0tWfF.js +0 -436
  277. package/site/dist/assets/index.browser-B-i-Lj3J.js +0 -1
  278. package/site/dist/assets/index.browser-BMmUcykv.js +0 -1
  279. package/site/dist/assets/index.es-Crd0TyUS.js +0 -2
  280. package/site/dist/assets/index.es-DQbup1WG.js +0 -3
  281. package/site/dist/assets/isPaymentRequestAvailable-Bq1cemEn-Cjf3l05I.js +0 -1
  282. package/site/dist/assets/jsx-runtime-CndCZlyt.js +0 -1
  283. package/site/dist/assets/landmark-BZPqMFEf.js +0 -1
  284. package/site/dist/assets/lib-DGtzi2DK.js +0 -1
  285. package/site/dist/assets/localBatchGatewayRequest-B71J8sQ9.js +0 -29
  286. package/site/dist/assets/lock-DyN_c-ud.js +0 -1
  287. package/site/dist/assets/logger-CsXm3oIv-B0UYpNFI.js +0 -1
  288. package/site/dist/assets/mail-V_mcPb64.js +0 -1
  289. package/site/dist/assets/modal-context-CwmSeFcv-DPWNJrfK.js +0 -1
  290. package/site/dist/assets/native-BKj27wfc.js +0 -1
  291. package/site/dist/assets/ofetch.CWycOUEr-Duby3iHT.js +0 -1
  292. package/site/dist/assets/parseAbi-Csg-8t59.js +0 -2
  293. package/site/dist/assets/parseAbiParameters-BWbIMYe4.js +0 -1
  294. package/site/dist/assets/parseEther-CHS0GNkM.js +0 -1
  295. package/site/dist/assets/pkce-DXF0yKjP-7_VY-d83.js +0 -1
  296. package/site/dist/assets/preload-helper-CRzLMJPS.js +0 -1
  297. package/site/dist/assets/privy-context-81KmsBw2-DiR6-Yq8.js +0 -2
  298. package/site/dist/assets/property-DuZ9DLP4.js +0 -3
  299. package/site/dist/assets/react-BfYA1q0G.js +0 -1
  300. package/site/dist/assets/react-CK2HRI1t.js +0 -1
  301. package/site/dist/assets/react-DxGeY8B8.js +0 -1
  302. package/site/dist/assets/react-auth-sHnipzbS.js +0 -1
  303. package/site/dist/assets/react-dom-BI2Koe4o.js +0 -1
  304. package/site/dist/assets/reservoir-x-nGuZkT-BguVFH3D.js +0 -1
  305. package/site/dist/assets/rolldown-runtime-Bjp52BOI.js +0 -1
  306. package/site/dist/assets/safe-url-D7SRPu33-CkMedGro.js +0 -1
  307. package/site/dist/assets/secp256k1-BeYL8aZl.js +0 -1
  308. package/site/dist/assets/secp256k1-Bh4MSkhz.js +0 -1
  309. package/site/dist/assets/secp256k1-Ci4cqHl0.js +0 -1
  310. package/site/dist/assets/secp256k1-DsxXqbXv.js +0 -1
  311. package/site/dist/assets/secp256k1-N9RUmafO.js +0 -1
  312. package/site/dist/assets/serializeAuthorizationList-DGMik9Wx.js +0 -1
  313. package/site/dist/assets/serializeSignature-CWKpxoab.js +0 -2
  314. package/site/dist/assets/sha2-C6s7zZmb.js +0 -1
  315. package/site/dist/assets/shared-DRZUmnL5-BewE5wK8.js +0 -193
  316. package/site/dist/assets/shared-FM0rljBt-CxLACri_.js +0 -21
  317. package/site/dist/assets/shouldProceedtoEmbeddedWalletCreationFlow-B3vyavrh-BkAKsRNi.js +0 -1
  318. package/site/dist/assets/simulateTransaction-CfuOrae4-PjA_tGjX.js +0 -1
  319. package/site/dist/assets/smartphone-3p8nFSTE.js +0 -1
  320. package/site/dist/assets/src-BBjkfi-Q.js +0 -1
  321. package/site/dist/assets/storage-Ufl800Ne-1qCR2JMO.js +0 -8
  322. package/site/dist/assets/stringify-DqUj9VzO.js +0 -20
  323. package/site/dist/assets/stripe.esm-DEi2D291.js +0 -2
  324. package/site/dist/assets/styles-BsotlekN-Aq27nZGc.js +0 -9
  325. package/site/dist/assets/styles-Cm9H63WO-p_ic_dxE.js +0 -92
  326. package/site/dist/assets/styles-DVyDvTdj-CjsWqAld.js +0 -74
  327. package/site/dist/assets/styles-hslXy6xG-D5QHIrJU.js +0 -164
  328. package/site/dist/assets/telegram-B-JqnkqZ-Dqkm4rDn.js +0 -1
  329. package/site/dist/assets/to-ui-error-DWe-qwS3-Bi1HUQI9.js +0 -4
  330. package/site/dist/assets/toViemAccount-B5iDGq1b-CdFZwv07.js +0 -2
  331. package/site/dist/assets/transaction-BNTP-bFm-M5tJx8vY.js +0 -1
  332. package/site/dist/assets/transfer-6YztDh-t-BVQkTvG-.js +0 -1
  333. package/site/dist/assets/triangle-alert-BG3Qw-Hq.js +0 -1
  334. package/site/dist/assets/twitch-5IOe4sIQ-CaXHFlAu.js +0 -1
  335. package/site/dist/assets/types-4CuOD23i.js +0 -1
  336. package/site/dist/assets/use-headless-crypto-deposit-Yn-QRLB3-CQqeyhGY.js +0 -1
  337. package/site/dist/assets/use-unlink-wallet-BKK4_5Ur-C0xQN_Lp.js +0 -1
  338. package/site/dist/assets/useActiveWallet-B-yZYGc6-BkK6rFBm.js +0 -123
  339. package/site/dist/assets/useGetSolPrice-x7gfUIHJ-CLVXqv6I.js +0 -1
  340. package/site/dist/assets/useGetTokenPrice-CaoUkb79-DAXXs3rs.js +0 -1
  341. package/site/dist/assets/useI18n-C-5n_noF-CGpkgIVy.js +0 -1
  342. package/site/dist/assets/useWalletBalance-CZnP5f_f-Cfd64m4n.js +0 -1
  343. package/site/dist/assets/useWallets-X1ndvJ_U-CAYqg7Za.js +0 -1
  344. package/site/dist/assets/utils-BkR7EQjg.js +0 -1
  345. package/site/dist/assets/utils-DJhBNOZA.js +0 -1
  346. package/site/dist/assets/utils-DdckVfQr.js +0 -1
  347. package/site/dist/assets/w3m-modal-B4H_uiPH.js +0 -1174
  348. package/site/dist/assets/wallet-BNI12IYk.js +0 -1
  349. package/site/dist/assets/wallet-CITSaPlB.js +0 -2
  350. package/site/dist/assets/wallet-connect-BvJtUsDt-CVlSNIXP.js +0 -1
  351. package/site/dist/assets/walletconnect-registry-xxG8XLCb-BpzpBq5s.js +0 -1
  352. package/site/dist/assets/walletconnect-v2-3frQFwbg-CzZ6oybX.js +0 -12
  353. package/site/dist/assets/wui-ux-by-reown-XyHdtV1O.js +0 -1147
  354. package/site/dist/assets/x-gjDrY2rk.js +0 -1
  355. package/src/discover/explore.js +0 -291
  356. package/src/market/settle.js +0 -165
  357. package/src/market/terms.js +0 -200
  358. package/src/publish/cli.js +0 -336
  359. package/src/publish/envelope.js +0 -185
  360. package/src/publish/manifest.js +0 -142
package/src/agent/x402.js CHANGED
@@ -1,31 +1,33 @@
1
- // Paying for a resource over x402 (v2, "exact" scheme on an EVM chain): the buyer's half.
1
+ // Buying a record over x402 (v2, scheme "fangorn"): the buyer's half.
2
2
  //
3
- // const { body, receipt, price } = await payAndFetch(url, { privateKey, maxPrice: 50000n });
3
+ // const { fields, price, tx } = await buyRecord(quoteUrl, { privateKey, fangorn, maxPrice: 50000n });
4
4
  //
5
- // 1. GET url → 402 with its PaymentRequired in the PAYMENT-REQUIRED header (base64 JSON):
6
- // `accepts` says what it costs, in which token, on which chain ("eip155:84532"), to whom.
7
- // 2. Sign an EIP-3009 transferWithAuthorization for exactly that (EIP-712, the token's own
8
- // domain, named in `extra`). Nothing is sent on chain by the buyer: the seller's facilitator
9
- // submits it.
10
- // 3. GET url again with PAYMENT-SIGNATURE: the seller verifies, settles, and returns the
11
- // resource, with the settlement (the tx hash) in PAYMENT-RESPONSE.
5
+ // `quoteUrl` is an app's GET /paid/<publisher>/<repo>/<id> (site/worker.js). The publisher signed
6
+ // the record's price (its terms); paying is SettlementRegistry.register, sent by the facilitator.
7
+ // 1. GET → 402: the requirement (the terms, paid to the registry in USDC), the sealed fields'
8
+ // handles, the repo's deposit, and the relay
9
+ // 2. a Semaphore identity from one wallet signature, and an ERC-3009 receiveWithAuthorization to
10
+ // the registry for exactly the price. Its nonce binds the subject, the identity's commitment,
11
+ // the referrer and a salt, so nobody can reuse it for anything else.
12
+ // 3. GET again with PAYMENT-SIGNATURE: the app has the facilitator verify it and send `register`
13
+ // 4. a Semaphore proof that some identity that paid names `reader` (message = reader, scope =
14
+ // subject), sent to the relay's /relay/settle. `reader` is derived from the identity, so the
15
+ // chain doesn't link it to the paying wallet (the relay could, by timing: PLAN.md, Known gaps).
16
+ // 5. each field's key from the deposit's key server, asked as `reader`; each field decrypted (JSON)
17
+ // A retry picks up where the last one stopped: a registered identity doesn't pay again, and a
18
+ // reader that can already read doesn't settle again.
12
19
  //
13
- // `maxPrice` (token base units) is checked BEFORE signing: a seller cannot ask for more
14
- // than the caller agreed to, whatever its 402 says.
20
+ // `maxPrice` (USDC base units) is checked before anything is signed.
21
+
22
+ import { concat, createPublicClient, http, keccak256, parseSignature, toHex } from "viem";
23
+ import { privateKeyToAccount } from "viem/accounts";
24
+ import { openField, recordKey, registerNonce, repoId, subjectOf } from "@fangorn-network/sdk";
15
25
 
16
26
  const b64 = (s) => (typeof btoa === "function" ? btoa(s) : Buffer.from(s, "utf8").toString("base64"));
17
27
  const unb64 = (s) => (typeof atob === "function" ? atob(s) : Buffer.from(s, "base64").toString("utf8"));
18
- const EVM = /^eip155:(\d+)$/;
19
-
20
- /** The one requirement we can pay: exact scheme, an EVM chain, within `maxPrice`. */
21
- export function choose(accepts = [], { maxPrice } = {}) {
22
- const ok = accepts.filter((a) => a.scheme === "exact" && EVM.test(a.network));
23
- if (!ok.length) throw new Error(`no payable option: ${JSON.stringify(accepts.map((a) => `${a.scheme}/${a.network}`))}`);
24
- const cheapest = ok.sort((a, b) => (BigInt(a.amount) < BigInt(b.amount) ? -1 : 1))[0];
25
- if (maxPrice != null && BigInt(cheapest.amount) > BigInt(maxPrice))
26
- throw new Error(`price ${cheapest.amount} exceeds the cap ${maxPrice} (token base units)`);
27
- return cheapest;
28
- }
28
+ const semaphore = () => import("@semaphore-protocol/core").catch(() => {
29
+ throw new Error("buying needs @semaphore-protocol/core: npm i @semaphore-protocol/core");
30
+ });
29
31
 
30
32
  /** A 402's PaymentRequired, from its PAYMENT-REQUIRED header. */
31
33
  export function paymentRequired(res) {
@@ -34,368 +36,179 @@ export function paymentRequired(res) {
34
36
  return JSON.parse(unb64(h));
35
37
  }
36
38
 
37
- /** The PAYMENT-SIGNATURE header value for requirement `req`, signed by `account` (a viem account). */
38
- export async function paymentHeader(req, account, opts) {
39
- return b64(JSON.stringify(await signPayment(req, account, opts)));
39
+ /** The requirement we can pay: scheme "fangorn" on an EVM chain, within `maxPrice`. */
40
+ export function choose(accepts = [], { maxPrice } = {}) {
41
+ const r = accepts.find((a) => a.scheme === "fangorn" && /^eip155:\d+$/.test(a.network));
42
+ if (!r) throw new Error(`no payable option: ${JSON.stringify(accepts.map((a) => `${a.scheme}/${a.network}`))}`);
43
+ if (maxPrice != null && BigInt(r.amount) > BigInt(maxPrice)) throw new Error(`price ${r.amount} exceeds the cap ${maxPrice} (USDC base units)`);
44
+ return r;
40
45
  }
41
46
 
42
- /** A PaymentPayload for `req` (one of a 402's `accepts`), for the 402's `resource`. */
43
- export async function signPayment(req, account, { resource, now = Math.floor(Date.now() / 1000) } = {}) {
44
- const chainId = Number((EVM.exec(req.network) ?? [])[1]);
45
- if (!chainId) throw new Error(`not an EVM network: ${req.network}`);
46
- const nonce = `0x${[...crypto.getRandomValues(new Uint8Array(32))].map((b) => b.toString(16).padStart(2, "0")).join("")}`;
47
- const authorization = {
48
- from: account.address, to: req.payTo, value: String(req.amount),
49
- validAfter: String(now - 600), validBefore: String(now + (req.maxTimeoutSeconds ?? 120)), nonce,
50
- };
51
- // The token's EIP-712 name differs by chain (Base Sepolia's USDC is "USDC", Base's "USD
52
- // Coin"), so a seller that names none is refused rather than guessed at.
53
- if (!req.extra?.name || !req.extra?.version) throw new Error(`${req.network}: the seller names no EIP-712 domain for ${req.asset} (extra.name, extra.version)`);
47
+ /** The Semaphore identity and reading account a wallet derives: the same on any machine, from one signature. */
48
+ export async function identityOf(account) {
49
+ const sig = await account.signMessage({ message: "fangorn:identity:v1" });
50
+ const { Identity } = await semaphore();
51
+ return { identity: new Identity(sig), reader: privateKeyToAccount(keccak256(concat([toHex("fangorn:reader:v1"), sig]))) };
52
+ }
53
+
54
+ /** An ERC-3009 receiveWithAuthorization from `account` to `req.payTo` for exactly `req.amount`, bound to this purchase. */
55
+ export async function signPayment(req, account, { subject, commitment, salt, now = Math.floor(Date.now() / 1000) }) {
56
+ const message = { from: account.address, to: req.payTo, value: BigInt(req.amount), validAfter: 0n,
57
+ validBefore: BigInt(now + (req.maxTimeoutSeconds ?? 300)), nonce: registerNonce(subject, commitment, req.extra.referrer, salt) };
54
58
  const signature = await account.signTypedData({
55
- domain: { name: req.extra.name, version: req.extra.version, chainId, verifyingContract: req.asset },
56
- types: { TransferWithAuthorization: [
57
- { name: "from", type: "address" }, { name: "to", type: "address" }, { name: "value", type: "uint256" },
59
+ domain: { name: req.extra.name, version: req.extra.version, chainId: Number(req.network.split(":")[1]), verifyingContract: req.asset },
60
+ types: { ReceiveWithAuthorization: [{ name: "from", type: "address" }, { name: "to", type: "address" }, { name: "value", type: "uint256" },
58
61
  { name: "validAfter", type: "uint256" }, { name: "validBefore", type: "uint256" }, { name: "nonce", type: "bytes32" }] },
59
- primaryType: "TransferWithAuthorization",
60
- message: { ...authorization, value: BigInt(authorization.value), validAfter: BigInt(authorization.validAfter), validBefore: BigInt(authorization.validBefore) },
62
+ primaryType: "ReceiveWithAuthorization", message,
61
63
  });
62
- return { x402Version: 2, ...(resource ? { resource } : {}), accepted: req, payload: { signature, authorization } };
63
- }
64
-
65
- /** GET `url` (or `method` it), paying once if it answers 402. Returns { status, body, price, receipt }. */
66
- export async function payAndFetch(url, { privateKey, account, maxPrice, fetchImpl = fetch, method = "GET" } = {}) {
67
- const first = await fetchImpl(url, { method });
68
- if (first.status !== 402) return { status: first.status, body: await first.text(), price: null, receipt: null };
69
- const offer = paymentRequired(first);
70
- const req = choose(offer.accepts, { maxPrice });
71
- if (!account) {
72
- if (!privateKey) throw new Error(`${url} costs ${req.amount} (base units of ${req.asset} on ${req.network}); no wallet configured to pay`);
73
- account = (await import("viem/accounts")).privateKeyToAccount(privateKey);
74
- }
75
- const paid = await fetchImpl(url, { method, headers: { "PAYMENT-SIGNATURE": await paymentHeader(req, account, { resource: offer.resource }) } });
76
- const receiptHeader = paid.headers.get("PAYMENT-RESPONSE");
77
- const body = await paid.text();
78
- if (paid.status === 402) throw new Error(`payment refused: ${paymentRequired(paid).error ?? body}`);
79
- return { status: paid.status, body, price: req.amount, receipt: receiptHeader ? JSON.parse(unb64(receiptHeader)) : null };
80
- }
81
-
82
- // ── Sealed records ────────────────────────────────────────────────────────────────────────
83
- // An app's paid record is in its committed rows already, sealed: `paid_ct` is
84
- // base64(nonce[12] ‖ AES-256-GCM(key, record)), with key = HMAC-SHA256(PAID_SECRET, id), and
85
- // `paid_sha256` is the sha256 of the record itself. A purchase buys the key, never the data,
86
- // so the buyer has the ciphertext (and so knows the record exists) before paying, and checks
87
- // what it decrypts against the committed hash after. Sources seal with the same derivation.
88
-
89
- const hex = (u8) => [...u8].map((b) => b.toString(16).padStart(2, "0")).join("");
90
- const unhex = (h) => Uint8Array.from(h.match(/../g).map((x) => parseInt(x, 16)));
91
- const fromB64 = (s) => Uint8Array.from(unb64bytes(s));
92
- function unb64bytes(s) { return typeof atob === "function" ? [...atob(s)].map((c) => c.charCodeAt(0)) : [...Buffer.from(s, "base64")]; }
93
- const toB64 = (u8) => (typeof btoa === "function" ? btoa(String.fromCharCode(...u8)) : Buffer.from(u8).toString("base64"));
94
- const sha256 = async (bytes) => hex(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)));
95
-
96
- /** A record's key, hex: HMAC-SHA256(secret, id). The worker's keyFor; here for sources and tests. */
97
- export async function paidKey(secret, id) {
98
- const k = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
99
- return hex(new Uint8Array(await crypto.subtle.sign("HMAC", k, new TextEncoder().encode(id))));
100
- }
101
-
102
- /** Seal `text` under `keyHex`: what a source writes as `paid_ct`. */
103
- export async function sealPaid(keyHex, text, nonce = crypto.getRandomValues(new Uint8Array(12))) {
104
- const k = await crypto.subtle.importKey("raw", unhex(keyHex), "AES-GCM", false, ["encrypt"]);
105
- const ct = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv: nonce }, k, new TextEncoder().encode(text)));
106
- const out = new Uint8Array(12 + ct.length);
107
- out.set(nonce); out.set(ct, 12);
108
- return toB64(out);
109
- }
110
-
111
- /** Open a sealed record and check it against the committed sha256 (`want`, if known). */
112
- export async function openPaid(ct, keyHex, want) {
113
- const bytes = fromB64(ct);
114
- const k = await crypto.subtle.importKey("raw", unhex(keyHex), "AES-GCM", false, ["decrypt"]);
115
- const plain = new Uint8Array(await crypto.subtle.decrypt({ name: "AES-GCM", iv: bytes.slice(0, 12) }, k, bytes.slice(12)));
116
- const sha = await sha256(plain);
117
- return { sha256: sha, verified: want ? want === sha : null, record: JSON.parse(new TextDecoder().decode(plain)) };
118
- }
119
-
120
- /** A private app's pass (its card's `access`): one payment at POST `pass_url`, a bearer token back. */
121
- export async function buyPass(access, { account, privateKey, maxPrice = access.pass, fetchImpl } = {}) {
122
- const r = await payAndFetch(access.pass_url, { method: "POST", account, privateKey, maxPrice, fetchImpl });
123
- if (r.status !== 200) throw new Error(`no pass: HTTP ${r.status} ${r.body.slice(0, 200)}`);
124
- return { ...JSON.parse(r.body), price: r.price, receipt: r.receipt };
125
- }
126
-
127
- // ── Private apps ──────────────────────────────────────────────────────────────────────────
128
- // A private app's rows are sealed whole before they reach IPFS: the payload on chain is
129
- // `{sealed}`, base64(nonce[12] ‖ AES-256-GCM(rowKey, JSON(payload))), with rowKey =
130
- // HMAC-SHA256(PAID_SECRET, "fangorn.row.v1"). The app's Worker holds the secret, opens the rows
131
- // in its cron and serves the view only to those who pay. The nonce is the first 12 bytes of
132
- // HMAC(rowKey, plaintext): an unchanged row seals to the same bytes, so the same CID, and is not
133
- // embedded again; two different rows never share a nonce. (Equal rows look equal: that is all.)
134
-
135
- const ROW = "fangorn.row.v1";
136
- async function rowNonce(keyHex, text) {
137
- const k = await crypto.subtle.importKey("raw", unhex(keyHex), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
138
- return new Uint8Array(await crypto.subtle.sign("HMAC", k, new TextEncoder().encode(text))).slice(0, 12);
139
- }
140
-
141
- /** The payload a private app commits for `payload`. */
142
- export async function sealRow(secret, payload) {
143
- const key = await paidKey(secret, ROW), text = JSON.stringify(payload);
144
- return { sealed: await sealPaid(key, text, await rowNonce(key, text)) };
145
- }
146
-
147
- /** A row as committed, opened; a payload that is not sealed comes back as it is. */
148
- export async function openRow(secret, payload) {
149
- if (typeof payload?.sealed !== "string") return payload;
150
- return (await openPaid(payload.sealed, await paidKey(secret, ROW))).record;
151
- }
152
-
153
- /** Open every record a seller sent a key for. `ct[id]` is the row's paid_ct, `want[id]` its paid_sha256. */
154
- async function openAll(keys, ct, want) {
155
- const out = [];
156
- for (const [id, key] of Object.entries(keys ?? {})) {
157
- if (!ct[id]) { out.push({ id, key, error: "no sealed record for this id in the rows read" }); continue; }
158
- try { out.push({ id, ...(await openPaid(ct[id], key, want[id])) }); }
159
- catch (e) { out.push({ id, key, error: `could not open: ${e.message}` }); }
64
+ const { v, r, s, yParity } = parseSignature(signature);
65
+ return { from: message.from, value: req.amount, validAfter: "0", validBefore: String(message.validBefore), nonce: message.nonce,
66
+ v: Number(v ?? 27n + BigInt(yParity)), r, s };
67
+ }
68
+
69
+ /** A Semaphore proof that `identity` is in the group, naming `reader`, scoped to `subject`, as `settle` checks it. */
70
+ async function prove({ identity, members, reader, subject }) {
71
+ const { Group, generateProof } = await semaphore();
72
+ const p = await generateProof(identity, new Group(members), BigInt(reader), BigInt(subject));
73
+ return { merkleTreeDepth: String(p.merkleTreeDepth), merkleTreeRoot: String(p.merkleTreeRoot), nullifier: String(p.nullifier), points: p.points.map(String) };
74
+ }
75
+
76
+ /**
77
+ * Buy one record and open its sealed fields. `fangorn` is an SDK client on the record's chain:
78
+ * it reads the registry and the ciphertext. `deps` replaces the chain reads and the proof in tests.
79
+ */
80
+ export async function buyRecord(quoteUrl, { privateKey, account = privateKeyToAccount(privateKey), fangorn, maxPrice, fetchImpl = fetch, deps = {} } = {}) {
81
+ const first = await fetchImpl(quoteUrl);
82
+ if (first.status !== 402) throw new Error(`${quoteUrl}: expected a 402 quote, got HTTP ${first.status} ${(await first.text()).slice(0, 200)}`);
83
+ const req = choose(paymentRequired(first).accepts, { maxPrice });
84
+ const { record, fields, deposit, relay } = await first.json();
85
+ const terms = req.extra.terms;
86
+ // The quote is the app's word; the terms are the publisher's, and must be this record's.
87
+ if (terms.publisher.toLowerCase() !== record.publisher.toLowerCase() || terms.repo !== repoId(record.repo) || terms.key !== recordKey(record.id))
88
+ throw new Error("the quote's terms are for another record");
89
+ const subject = subjectOf(terms.publisher, terms.repo, terms.key);
90
+ // Every sealed field must be readable before anything is paid: a sale we can't open is money lost.
91
+ const storage = fangorn.getStorage(), held = new Map();
92
+ for (const h of Object.values(fields)) {
93
+ try { held.set(h.cid, await storage.getFile(h.cid)); }
94
+ catch (e) { throw new Error(`not buying: the record's sealed data (${h.cid}) can't be read: ${e.message}`); }
160
95
  }
161
- return out;
162
- }
163
-
164
- /** Buy one record an app sells (its agent card's `paid`): pay for its key, open `ct` (the
165
- * row's paid_ct) and check it against `want` (its paid_sha256). Shared by fangorn-mcp and the page. */
166
- export async function buyRecord(paid, key, { ct, want, account, privateKey, maxPrice = paid.price, fetchImpl } = {}) {
167
- if (!ct) return { id: key, error: "this row carries no sealed record (paid_ct): nothing to buy" };
168
- const r = await payAndFetch(paid.url.replace("{id}", encodeURIComponent(key)), { account, privateKey, maxPrice, fetchImpl });
169
- if (r.status !== 200) return { id: key, error: `HTTP ${r.status}: ${r.body.slice(0, 200)}` };
170
- const opened = await openPaid(ct, JSON.parse(r.body).key, want);
171
- return { id: key, price: r.price, receipt: r.receipt, digest: opened.sha256, verified: opened.verified, record: opened.record };
172
- }
173
-
174
- // ── Batches and vouchers ──────────────────────────────────────────────────────────────────
175
- // `POST <app>/paid {ids}` answers 402 with one price for every record it has; PAYMENT-SIGNATURE may then
176
- // carry several payments (a JSON array) that add up to exactly that price. A payment signs
177
- // payee, amount and expiry but not the resource (checked against the PayAI facilitator,
178
- // 2026-10-07), so a person's wallet can sign a stack of them ahead of time ("vouchers") and an
179
- // agent spends them later with no key: each pays only the app, for a fixed amount, until it
180
- // expires. Holding them is holding the right to that much of the person's money, nothing more.
181
96
 
182
- /** Sign vouchers worth `total` (base units) in `denominations` (ascending), payable to `req.payTo`
183
- * for `days`. Enough of each small one to make any multiple of the smallest up to `total`. */
184
- export async function signVouchers(req, account, { total, denominations, days = 30 } = {}) {
185
- const out = [], ds = denominations.map(BigInt);
186
- let left = BigInt(total);
187
- for (let i = 0; i < ds.length; i++) {
188
- let n = left / ds[i];
189
- if (i < ds.length - 1) { // keep a full set of this size, the rest in multiples of the next
190
- const keep = ds[i + 1] / ds[i] - 1n; // ponytail: assumes each denomination divides the next
191
- n = (left - (left - (keep < n ? keep : n) * ds[i]) / ds[i + 1] * ds[i + 1]) / ds[i];
97
+ const config = fangorn.getConfig(), settlement = fangorn.getSettlementRegistry();
98
+ const pub = deps.pub ?? createPublicClient({ transport: http(config.rpcUrl) });
99
+ const members = deps.members ?? ((s) => settlement.membersOf(s).then((m) => [...m]));
100
+ const { identity, reader } = await identityOf(account);
101
+
102
+ let tx = null;
103
+ if (!(await settlement.canRead(reader.address, terms.publisher, terms.repo, terms.key))) {
104
+ if (!(await settlement.isRegistered(subject, identity.commitment))) {
105
+ const salt = toHex(crypto.getRandomValues(new Uint8Array(32)));
106
+ const payment = await signPayment(req, account, { subject, commitment: identity.commitment, salt });
107
+ const payload = { commitment: identity.commitment.toString(), salt, payment };
108
+ const paid = await fetchImpl(quoteUrl, { headers: { "PAYMENT-SIGNATURE": b64(JSON.stringify({ x402Version: 2, accepted: req, payload })) } });
109
+ if (!paid.ok) throw new Error(`payment refused: ${(await paid.json().catch(() => ({}))).error ?? `HTTP ${paid.status}`}`);
110
+ tx = JSON.parse(unb64(paid.headers.get("PAYMENT-RESPONSE") ?? b64("{}"))).transaction || null;
111
+ if (tx) await pub.waitForTransactionReceipt({ hash: tx });
192
112
  }
193
- for (let k = 0n; k < n; k++)
194
- out.push(await signPayment({ ...req, amount: String(ds[i]), maxTimeoutSeconds: days * 86400 }, account));
195
- left -= n * ds[i];
196
- }
197
- return out;
198
- }
199
-
200
- /** Vouchers adding up to exactly `amount`, largest first; null if they cannot. */
201
- export function pick(vouchers, amount) {
202
- let left = BigInt(amount);
203
- const out = [];
204
- for (const v of [...vouchers].sort((a, b) => { const x = BigInt(a.payload.authorization.value), y = BigInt(b.payload.authorization.value); return x > y ? -1 : x < y ? 1 : 0; })) { // largest first; equal ones keep their order
205
- const x = BigInt(v.payload.authorization.value);
206
- if (x <= left) { out.push(v); left -= x; }
207
- }
208
- return left === 0n ? out : null; // ponytail: greedy; exact for denominations that divide each other
209
- }
210
-
211
- /** Run `fn` holding `<path>.lock` (Node only): two fangorn-mcp processes sharing one allowance
212
- * file would otherwise pick the same vouchers and race. A lock older than `staleMs` belongs to
213
- * a crashed purchase and is taken over.
214
- * ponytail: a takeover can race a third process for the same stale lock; add a pid check if
215
- * more than two servers ever share a file. */
216
- export async function withLock(path, fn, { staleMs = 120_000, waitMs = 30_000 } = {}) {
217
- const fs = await import("node:fs"), lock = `${path}.lock`, start = Date.now();
218
- for (;;) {
219
- try { fs.closeSync(fs.openSync(lock, "wx")); break; } catch (e) {
220
- if (e.code !== "EEXIST") throw e;
221
- let age = 0;
222
- try { age = Date.now() - fs.statSync(lock).mtimeMs; } catch { continue; } // released meanwhile
223
- if (age > staleMs) { fs.rmSync(lock, { force: true }); continue; }
224
- if (Date.now() - start > waitMs) throw new Error(`the allowance is in use by another purchase (${lock})`);
225
- await new Promise((ok) => setTimeout(ok, 100));
113
+ // A load-balanced RPC can answer from a node behind the one that mined `register`:
114
+ // our commitment is registered, so read until it shows up.
115
+ const has = (g) => !!deps.members || g.some((c) => BigInt(c) === identity.commitment);
116
+ let group = await members(subject);
117
+ for (let i = 0; i < 10 && !has(group); i++) {
118
+ await new Promise((r) => setTimeout(r, 3000));
119
+ group = await members(subject);
226
120
  }
121
+ if (!has(group)) throw new Error("our commitment is registered but the RPC doesn't show it yet; retry the purchase");
122
+ const proof = await (deps.prove ?? prove)({ identity, members: group, reader: reader.address, subject });
123
+ const res = await fetchImpl(`${relay}/relay/settle`, { method: "POST", headers: { "content-type": "application/json" },
124
+ body: JSON.stringify({ subject, reader: reader.address, ...proof }) });
125
+ const out = await res.json().catch(() => ({}));
126
+ if (!res.ok) throw new Error(`settle refused: ${out.error ?? `HTTP ${res.status}`}`);
127
+ await pub.waitForTransactionReceipt({ hash: out.tx });
227
128
  }
228
- try { return await fn(); } finally { fs.rmSync(lock, { force: true }); }
229
- }
230
129
 
231
- /** Buy many records at once. Pays from `vouchers` (spent ones are removed from the array) or
232
- * signs one exact payment with `account`/`privateKey`. Each key opens `ct[id]` (the row's
233
- * paid_ct), checked against `want[id]` (its paid_sha256). Returns the records and a manifest a
234
- * third party can check. */
235
- export async function buyBatch(paid, allIds, { ct = {}, want = {}, vouchers, account, privateKey, maxPrice, fetchImpl = fetch } = {}) {
236
- const url = paid.url.replace(/\/\{id\}$/, "");
237
- // Only ids whose sealed record is in hand are bought: anything else would pay for nothing.
238
- const ids = allIds.filter((id) => ct[id]), unsealed = allIds.filter((id) => !ct[id]);
239
- if (!ids.length) throw new Error(`none of these rows carries a sealed record (paid_ct): ${unsealed.join(", ")}`);
240
- const post = (headers = {}) => fetchImpl(url, { method: "POST", headers: { "content-type": "application/json", ...headers }, body: JSON.stringify({ ids }) });
241
- const first = await post();
242
- const body0 = await first.json();
243
- if (first.status !== 402) throw new Error(`expected a price, got HTTP ${first.status}: ${JSON.stringify(body0).slice(0, 200)}`);
244
- const offer = { ...body0, ...paymentRequired(first) };
245
- const req = choose(offer.accepts, { maxPrice });
246
- let payments;
247
- if (vouchers) {
248
- payments = pick(vouchers, req.amount);
249
- if (!payments) throw new Error(`the allowance cannot pay exactly ${req.amount} (base units); add funds or buy fewer records`);
250
- } else {
251
- account ??= privateKey ? (await import("viem/accounts")).privateKeyToAccount(privateKey) : null;
252
- if (!account) throw new Error(`${offer.found.length} record(s) cost ${req.amount} (base units); no wallet or allowance to pay`);
253
- payments = [await signPayment(req, account, { resource: offer.resource })];
130
+ const opened = {};
131
+ for (const [field, handle] of Object.entries(fields)) {
132
+ const bytes = await openField({ handle, deposit, key: terms.key, field, signer: reader, storage: { getFile: async (cid) => held.get(cid) ?? storage.getFile(cid) }, fetch: fetchImpl });
133
+ opened[field] = JSON.parse(new TextDecoder().decode(bytes));
254
134
  }
255
- const res = await post({ "PAYMENT-SIGNATURE": b64(JSON.stringify(payments)) });
256
- const body = await res.json();
257
- // Drop every voucher the seller says it settled (`paid`) or could not (`rejected`: spent, or
258
- // the wallet cannot cover it), on success, partial delivery and refusal alike, so a dead
259
- // voucher is never offered twice. A seller that names none: all of them on a 200.
260
- const nonce = (p) => p.payload.authorization.nonce;
261
- const paidNonces = new Set(body.paid ?? (res.status === 200 ? payments.map(nonce) : []));
262
- if (vouchers) for (const n of [...paidNonces, ...(body.rejected ?? [])]) {
263
- const k = vouchers.findIndex((v) => nonce(v) === n);
264
- if (k >= 0) vouchers.splice(k, 1);
265
- }
266
- if (res.status !== 200) throw new Error(`payment refused: ${body.error ?? JSON.stringify(body).slice(0, 200)}`);
267
- const spent = payments.filter((p) => paidNonces.has(nonce(p))).reduce((s, p) => s + BigInt(p.payload.authorization.value), 0n);
268
- const records = await openAll(body.keys, ct, want);
269
- // `undelivered` + `error`: a payment failed after others settled; these ids were not paid for.
270
- return { records, missing: [...unsealed, ...(body.missing ?? [])], undelivered: body.undelivered ?? [], error: body.error ?? null,
271
- manifest: { app: url, bought: new Date().toISOString(), price: String(spent),
272
- asset: req.asset, network: req.network, transactions: body.receipts.map((r) => r.transaction),
273
- records: records.map(({ id, sha256 }) => ({ id, sha256 })) } };
135
+ return { record, fields: opened, price: req.amount, tx };
274
136
  }
275
137
 
276
- // ── self-check: `node src/agent/x402.js` — a stand-in seller that checks the signature ──
138
+ // ── self-check: `node src/agent/x402.js` — a stand-in app, relay and key server; no chain ──
277
139
  if (typeof process !== "undefined" && import.meta.url === `file://${process.argv[1]}` && process.argv[1].endsWith("/x402.js")) {
278
- const { privateKeyToAccount, generatePrivateKey } = await import("viem/accounts");
279
- const { verifyTypedData } = await import("viem");
280
- const buyer = privateKeyToAccount(generatePrivateKey());
281
- // Base Sepolia's USDC, whose EIP-712 name is "USDC" (Base's is "USD Coin").
282
- const USDC = "0x036CbD53842c5426634e7929541eC2318f3dCF7e", SELLER = `0x${"5e".repeat(20)}`, CHAIN = 84532, DOMAIN = { name: "USDC", version: "2" };
283
- const req = { scheme: "exact", network: `eip155:${CHAIN}`, amount: "10000", payTo: SELLER, asset: USDC, maxTimeoutSeconds: 120, extra: DOMAIN };
284
- let settled = null;
285
- const seller = async (url, init = {}) => {
286
- const h = init.headers?.["PAYMENT-SIGNATURE"];
287
- if (!h) return new Response("{}", { status: 402, headers: { "PAYMENT-REQUIRED": b64(JSON.stringify({ x402Version: 2, resource: { url }, accepts: [req] })) } });
288
- const sent = JSON.parse(unb64(h)), p = sent.payload, a = p.authorization;
289
- if (sent.x402Version !== 2 || sent.resource?.url !== url || sent.accepted?.amount !== "10000") return new Response("{}", { status: 400 });
290
- const ok = await verifyTypedData({ address: a.from, signature: p.signature,
291
- domain: { ...DOMAIN, chainId: CHAIN, verifyingContract: USDC },
292
- types: { TransferWithAuthorization: [{ name: "from", type: "address" }, { name: "to", type: "address" }, { name: "value", type: "uint256" },
293
- { name: "validAfter", type: "uint256" }, { name: "validBefore", type: "uint256" }, { name: "nonce", type: "bytes32" }] },
294
- primaryType: "TransferWithAuthorization",
295
- message: { ...a, value: BigInt(a.value), validAfter: BigInt(a.validAfter), validBefore: BigInt(a.validBefore) } });
296
- if (!ok || a.to !== SELLER || a.value !== "10000") return new Response("{}", { status: 402, headers: { "PAYMENT-REQUIRED": b64(JSON.stringify({ x402Version: 2, error: "bad payment", accepts: [req] })) } });
297
- settled = a;
298
- return new Response('{"secret":42}', { status: 200, headers: { "PAYMENT-RESPONSE": b64(JSON.stringify({ success: true, transaction: "0xabc" })) } });
299
- };
300
- const r = await payAndFetch("https://x/paid/1", { account: buyer, maxPrice: 10000n, fetchImpl: seller });
301
- if (r.body !== '{"secret":42}' || r.receipt.transaction !== "0xabc" || settled.from !== buyer.address) throw new Error(`paid fetch: ${JSON.stringify(r)}`);
302
- let refused = false;
303
- try { await payAndFetch("https://x/paid/1", { account: buyer, maxPrice: 9999n, fetchImpl: seller }); } catch (e) { refused = /exceeds the cap/.test(e.message); }
304
- if (!refused) throw new Error("a price over the cap must be refused before signing");
305
-
306
- // Batches through the real worker (CONFIG as ship deploys it), a stand-in facilitator
307
- // that checks signatures and refuses a nonce settled twice, and vouchers signed ahead.
308
- const { mkdtempSync, writeFileSync } = await import("node:fs");
309
- const { tmpdir } = await import("node:os");
310
- const sale = { price: "100000", network: `eip155:${CHAIN}`, asset: USDC, payTo: SELLER, facilitator: "https://fac", ...DOMAIN, description: "" };
311
- // Not a literal: the page bundles this module, and a bundler would follow it into the Worker.
312
- const WORKER = "../../site/worker.js";
313
- const worker = (await import(/* @vite-ignore */ WORKER)).default;
314
- // The rows as a buyer reads them: each record sealed under its key, with the plaintext's sha256.
315
- const SECRET = "s3cret", ct = {}, want = {};
316
- for (const id of ["a", "b", "c"]) {
317
- const plain = JSON.stringify({ id, amount: 1 });
318
- ct[id] = await sealPaid(await paidKey(SECRET, id), plain);
319
- want[id] = await sha256(new TextEncoder().encode(plain));
320
- }
321
- const env = { PAID_SECRET: SECRET, CONFIG: { name: "Test", paid: sale } };
322
- const spent = new Set(), facilitated = [];
323
- const realFetch = globalThis.fetch;
324
- globalThis.fetch = async (u, init) => {
325
- const { x402Version, paymentPayload: p, paymentRequirements: q } = JSON.parse(init.body), a = p.payload.authorization;
326
- if (x402Version !== 2 || p.x402Version !== 2) throw new Error("the facilitator is spoken to in v2");
327
- facilitated.push(p);
328
- if (u.endsWith("/verify")) {
329
- const ok = await verifyTypedData({ address: a.from, signature: p.payload.signature,
330
- domain: { ...DOMAIN, chainId: CHAIN, verifyingContract: USDC },
331
- types: { TransferWithAuthorization: [{ name: "from", type: "address" }, { name: "to", type: "address" }, { name: "value", type: "uint256" },
332
- { name: "validAfter", type: "uint256" }, { name: "validBefore", type: "uint256" }, { name: "nonce", type: "bytes32" }] },
333
- primaryType: "TransferWithAuthorization",
334
- message: { ...a, value: BigInt(a.value), validAfter: BigInt(a.validAfter), validBefore: BigInt(a.validBefore) } });
335
- return new Response(JSON.stringify({ isValid: ok && a.value === q.amount, invalidReason: "bad" }));
140
+ const { x25519 } = await import("@noble/curves/ed25519");
141
+ const { generatePrivateKey } = await import("viem/accounts");
142
+ const { recoverTypedDataAddress } = await import("viem");
143
+ const sdk = await import("@fangorn-network/sdk");
144
+ const { testKeyServer } = await import("@fangorn-network/sdk/lib/test/key-server.js");
145
+ const eq = (a, b, m) => { if (JSON.stringify(a) !== JSON.stringify(b)) throw new Error(`${m}: ${JSON.stringify(a)} != ${JSON.stringify(b)}`); };
146
+
147
+ const publisher = privateKeyToAccount(generatePrivateKey()), buyer = privateKeyToAccount(generatePrivateKey());
148
+ const REGISTRY = `0x${"e0".repeat(20)}`, USDC = `0x${"aa".repeat(20)}`, REFERRER = `0x${"fe".repeat(20)}`;
149
+ const repo = sdk.repoId("notes"), key = sdk.recordKey("a");
150
+ const scope = { chainId: 84532, registry: REGISTRY, publisher: publisher.address, repo, generation: 0n };
151
+ const ksSecret = x25519.utils.randomSecretKey(), kRepo = sdk.newRepoKey();
152
+ const deposit = sdk.makeDeposit({ kRepo, keyServerPubkey: x25519.getPublicKey(ksSecret), keyServer: "https://keys.test", scope });
153
+ const files = new Map();
154
+ const storage = { putFile: async (b) => { const cid = `bafy${files.size}`; files.set(cid, b); return cid; }, getFile: async (cid) => files.get(cid) };
155
+ const body = await sdk.sealField({ plaintext: new TextEncoder().encode(JSON.stringify({ text: "the body" })), kRepo, scope, key, field: "body", storage });
156
+ const terms = { publisher: publisher.address, repo, key, price: "10000", shareBps: 500, generation: "0", deadline: "2000000000" };
157
+ const req = { scheme: "fangorn", network: "eip155:84532", amount: "10000", asset: USDC, payTo: REGISTRY, maxTimeoutSeconds: 300,
158
+ extra: { terms, signature: "0x12", referrer: REFERRER, name: "USDC", version: "2" } };
159
+
160
+ // The chain as the buyer sees it: settled once the relay has the proof.
161
+ let settled = false, registered = false, signed = null, proven = null;
162
+ const fangorn = { getConfig: () => ({ rpcUrl: "http://unused", fromBlock: 0n }), getStorage: () => storage,
163
+ getSettlementRegistry: () => ({ getAddress: () => REGISTRY, canRead: async () => settled, isRegistered: async () => registered }) };
164
+ const pub = { waitForTransactionReceipt: async () => ({}) };
165
+ const quoteUrl = `https://app.test/paid/${publisher.address}/notes/a`;
166
+ const fetchImpl = async (url, init = {}) => {
167
+ if (url === quoteUrl) {
168
+ const pay = init.headers?.["PAYMENT-SIGNATURE"];
169
+ if (!pay) return new Response(JSON.stringify({ record: { publisher: publisher.address, repo: "notes", id: "a" }, fields: { body }, deposit, relay: "https://relay.test" }),
170
+ { status: 402, headers: { "PAYMENT-REQUIRED": b64(JSON.stringify({ x402Version: 2, accepts: [req] })) } });
171
+ signed = JSON.parse(unb64(pay)).payload;
172
+ registered = true;
173
+ return new Response("{}", { headers: { "PAYMENT-RESPONSE": b64(JSON.stringify({ success: true, transaction: "0xabc" })) } });
336
174
  }
337
- if (spent.has(a.nonce)) return new Response(JSON.stringify({ success: false, errorReason: "nonce used" }));
338
- spent.add(a.nonce);
339
- return new Response(JSON.stringify({ success: true, transaction: `0x${spent.size}` }));
175
+ if (url === "https://relay.test/relay/settle") { proven = JSON.parse(init.body); settled = true; return new Response(JSON.stringify({ tx: "0xdef" })); }
176
+ if (url === "https://keys.test/access") return testKeyServer(JSON.parse(init.body), { secret: ksSecret, chainId: 84532, registry: REGISTRY,
177
+ canRead: async (r) => settled && r === proven.reader });
178
+ throw new Error(`unexpected fetch ${url}`);
340
179
  };
341
- const viaWorker = (u, init) => worker.fetch(new Request(u, init), env);
342
- const paidCard = { url: "https://x/paid/{id}", price: sale.price };
343
- try {
344
- // One record, one payment: the key opens the row's sealed record, which matches its hash.
345
- const single = await buyRecord(paidCard, "a", { ct: ct.a, want: want.a, account: buyer, maxPrice: 100000n, fetchImpl: viaWorker });
346
- if (single.verified !== true || single.record.id !== "a") throw new Error(`single key: ${JSON.stringify(single)}`);
347
- if (facilitated.some((p) => p.extensions)) throw new Error("GET /paid/<id> must not ask to be listed: its URL differs per record");
348
- // A probe with no body (x402scan, a Bazaar crawler) gets a 402 for one record, Bazaar extension and all.
349
- const probe = await viaWorker("https://x/paid", { method: "POST" });
350
- const asked = paymentRequired(probe);
351
- if (probe.status !== 402 || asked.accepts[0].amount !== sale.price || asked.extensions?.bazaar?.info?.input?.method !== "POST")
352
- throw new Error(`probe: ${probe.status} ${JSON.stringify(asked)}`);
353
- // One exact payment from a wallet: 2 of 3 ids are sealed in the rows, 2 × $0.10, the
354
- // third is never quoted (no ciphertext in hand, nothing to pay for).
355
- const one = await buyBatch(paidCard, ["a", "b", "zz"], { account: buyer, maxPrice: 200000n, ct, want, fetchImpl: viaWorker });
356
- if (one.records.length !== 2 || one.missing[0] !== "zz" || one.manifest.price !== "200000" || !one.records.every((r) => r.verified === true))
357
- throw new Error(`batch with a wallet: ${JSON.stringify(one)}`);
358
- // The facilitator lists what it settles with the Bazaar extension: every batch payment carries it.
359
- const batchSent = facilitated.slice(-2); // its one payment: verify, settle
360
- if (!batchSent.every((p) => p.extensions?.bazaar && p.resource.url === "https://x/paid" && p.accepted.amount === "200000"))
361
- throw new Error(`batch payloads: ${JSON.stringify(batchSent.map((p) => [p.resource?.url, p.accepted?.amount, !!p.extensions]))}`);
362
- // $2 of vouchers in $0.10 / $1, 30 days: any multiple of $0.10 can be paid exactly.
363
- const vs = await signVouchers({ ...paidCard, scheme: "exact", network: sale.network, payTo: SELLER, asset: USDC, extra: DOMAIN },
364
- buyer, { total: 2000000n, denominations: [100000n, 1000000n] });
365
- if (vs.length !== 11 || pick(vs, 1700000n).length !== 8 || pick(vs, 2100000n) !== null) throw new Error(`vouchers: ${vs.length}`);
366
- const before = [...vs];
367
- const three = await buyBatch(paidCard, ["a", "b", "c"], { vouchers: vs, ct, want, fetchImpl: viaWorker });
368
- if (three.records.length !== 3 || vs.length !== 8 || three.manifest.transactions.length !== 3) throw new Error(`batch with vouchers: ${JSON.stringify(three.manifest)}`);
369
- // A spent $0.10 voucher riding along with a fresh one: both verify, settle refuses, nothing sent.
370
- const usedOne = before.find((v) => !vs.includes(v) && v.payload.authorization.value === "100000");
371
- const fresh = vs.find((v) => v.payload.authorization.value === "100000");
372
- const replay = await viaWorker("https://x/paid", { method: "POST", body: JSON.stringify({ ids: ["a", "b"] }), headers: { "PAYMENT-SIGNATURE": b64(JSON.stringify([usedOne, fresh])) } });
373
- if (replay.status !== 402 || (await replay.json()).keys) throw new Error("a spent voucher must not buy anything");
374
- const short = await viaWorker("https://x/paid", { method: "POST", body: JSON.stringify({ ids: ["a", "b"] }), headers: { "PAYMENT-SIGNATURE": b64(JSON.stringify([fresh])) } });
375
- if (short.status !== 402) throw new Error("payments short of the price must be refused");
376
- // A spent voucher still in the stack, behind a fresh one (a crash before the file was
377
- // saved): the fresh one settles and buys one record, the spent one is refused, and both
378
- // leave the stack so neither is offered again.
379
- const fresh2 = vs.find((v) => v !== fresh && v.payload.authorization.value === "100000");
380
- const stack = [fresh2, usedOne];
381
- const half = await buyBatch(paidCard, ["a", "b"], { vouchers: stack, ct, want, fetchImpl: viaWorker });
382
- if (half.records.length !== 1 || half.undelivered[0] !== "b" || !half.error || half.manifest.price !== "100000" || stack.length !== 0)
383
- throw new Error(`partial settle: ${JSON.stringify({ ...half, stack: stack.length })}`);
384
- // The spent voucher first: nothing settles, nothing is sent, and it is dropped.
385
- const stack2 = [usedOne, fresh];
386
- let threw = false;
387
- try { await buyBatch(paidCard, ["a", "b"], { vouchers: stack2, ct, want, fetchImpl: viaWorker }); } catch { threw = true; }
388
- if (!threw || stack2.length !== 1 || stack2[0] !== fresh) throw new Error(`refused batch must drop only the spent voucher: ${stack2.length}`);
389
- // The allowance lock: two purchases at once take turns; a crashed one's lock is taken over.
390
- const vfile = `${mkdtempSync(`${tmpdir()}/wm-`)}/vouchers.json`, order = [];
391
- const turn = (name) => withLock(vfile, async () => { order.push(`${name}+`); await new Promise((ok) => setTimeout(ok, 50)); order.push(`${name}-`); });
392
- await Promise.all([turn("a"), turn("b")]);
393
- if (order.join() !== "a+,a-,b+,b-" && order.join() !== "b+,b-,a+,a-") throw new Error(`lock interleaved: ${order}`);
394
- writeFileSync(`${vfile}.lock`, "");
395
- const { utimesSync, existsSync } = await import("node:fs");
396
- utimesSync(`${vfile}.lock`, new Date(Date.now() - 300_000), new Date(Date.now() - 300_000));
397
- if (await withLock(vfile, async () => 7) !== 7 || existsSync(`${vfile}.lock`)) throw new Error("a stale lock must be taken over and released");
398
- } finally { globalThis.fetch = realFetch; }
399
- console.log("x402.js batch self-check ok — keys, not records: each opens the row's sealed record and matches its hash; one price for the ids sealed in the rows, paid by one signature or by vouchers signed ahead, spent vouchers removed and refused if replayed, short payment refused, a half-settled batch delivers what was paid and drops spent vouchers, the allowance file is locked per purchase");
400
- console.log("x402.js self-check ok — a signed EIP-3009 authorization the seller can verify, exactly the asked amount to the asked payee, the receipt read back, and a price over the cap refused before signing");
180
+ const deps = { pub, members: async () => [1n], prove: async ({ reader, subject }) => ({ merkleTreeDepth: "1", merkleTreeRoot: "1", nullifier: "1", points: Array(8).fill("0"), reader, subject }) };
181
+
182
+ await buyRecord(quoteUrl, { account: buyer, fangorn, fetchImpl, deps, maxPrice: 9999n }).then(() => { throw new Error("over the cap must refuse"); },
183
+ (e) => { if (!/exceeds the cap/.test(e.message)) throw e; });
184
+ eq(signed, null, "nothing is signed over the cap");
185
+ const gone = { ...fangorn, getStorage: () => ({ getFile: async () => { throw new Error("HTTP 404"); } }) };
186
+ await buyRecord(quoteUrl, { account: buyer, fangorn: gone, fetchImpl, deps }).then(() => { throw new Error("unreadable data must refuse"); },
187
+ (e) => { if (!/can't be read/.test(e.message)) throw e; });
188
+ eq(signed, null, "nothing is paid for data that can't be read");
189
+
190
+ const got = await buyRecord(quoteUrl, { account: buyer, fangorn, fetchImpl, deps, maxPrice: 10000n });
191
+ eq([got.fields, got.price, got.tx], [{ body: { text: "the body" } }, "10000", "0xabc"], "paid, settled, and the field opens as JSON");
192
+ const { identity, reader } = await identityOf(buyer);
193
+ eq([signed.commitment, proven.reader, proven.subject], [identity.commitment.toString(), reader.address, subjectOf(publisher.address, repo, key)], "the payment carries the identity; the proof names the reader");
194
+ if (reader.address === buyer.address) throw new Error("the reader must not be the paying wallet");
195
+ const p = signed.payment;
196
+ const from = await recoverTypedDataAddress({ domain: { name: "USDC", version: "2", chainId: 84532, verifyingContract: USDC },
197
+ types: { ReceiveWithAuthorization: [{ name: "from", type: "address" }, { name: "to", type: "address" }, { name: "value", type: "uint256" },
198
+ { name: "validAfter", type: "uint256" }, { name: "validBefore", type: "uint256" }, { name: "nonce", type: "bytes32" }] },
199
+ primaryType: "ReceiveWithAuthorization", message: { from: p.from, to: p.payTo ?? REGISTRY, value: BigInt(p.value), validAfter: 0n, validBefore: BigInt(p.validBefore), nonce: p.nonce },
200
+ signature: concat([p.r, p.s, toHex(p.v, { size: 1 })]) });
201
+ eq([from, p.value, p.nonce], [buyer.address, "10000", registerNonce(subjectOf(publisher.address, repo, key), identity.commitment, REFERRER, signed.salt)],
202
+ "an ERC-3009 authorization from the buyer for exactly the price, its nonce bound to this purchase and referrer");
203
+
204
+ signed = null;
205
+ const again = await buyRecord(quoteUrl, { account: buyer, fangorn, fetchImpl, deps });
206
+ eq([again.fields.body.text, signed, again.tx], ["the body", null, null], "a reader that can already read pays nothing again");
207
+
208
+ const other = { ...req, extra: { ...req.extra, terms: { ...terms, key: sdk.recordKey("b") } } };
209
+ const lying = async (url, init) => url === quoteUrl && !init?.headers ? new Response(JSON.stringify({ record: { publisher: publisher.address, repo: "notes", id: "a" }, fields: { body }, deposit, relay: "https://relay.test" }),
210
+ { status: 402, headers: { "PAYMENT-REQUIRED": b64(JSON.stringify({ x402Version: 2, accepts: [other] })) } }) : fetchImpl(url, init);
211
+ await buyRecord(quoteUrl, { account: privateKeyToAccount(generatePrivateKey()), fangorn, fetchImpl: lying, deps }).then(() => { throw new Error("must refuse"); },
212
+ (e) => { if (!/another record/.test(e.message)) throw e; });
213
+ console.log("x402.js self-check ok — the cap holds before signing, the payment is an ERC-3009 authorization bound to this purchase, the proof names a reader that isn't the wallet, a retry pays nothing, a quote for another record is refused, nothing is paid when the sealed data can't be read, and the field opens");
401
214
  }