@fangorn-network/westmarch 0.0.20-dev → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (386) hide show
  1. package/README.md +35 -5
  2. package/docs/app-to-agent.md +113 -620
  3. package/package.json +18 -18
  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-CkP1WhkV.js +8 -0
  7. package/site/dist/assets/roles-BDPDWrwo.js +1 -0
  8. package/site/dist/assets/rolldown-runtime-COiEfhsA.js +1 -0
  9. package/site/dist/assets/transformers.web-CUZUFjAK.js +49 -0
  10. package/site/dist/assets/transformers.web-CwR_IYQV.js +40 -0
  11. package/site/dist/index.html +4 -5
  12. package/site/feed.js +16 -1
  13. package/site/no-model.js +4 -0
  14. package/site/worker.js +382 -59
  15. package/src/agent/agent-card.js +51 -37
  16. package/src/agent/mcp.js +173 -500
  17. package/src/agent/tools.js +3 -2
  18. package/src/agent/ui.js +1 -1
  19. package/src/agent/view-tools.js +11 -30
  20. package/src/agent/x402.js +194 -85
  21. package/src/core/embed.js +56 -53
  22. package/src/core/embed.worker.js +1 -1
  23. package/src/core/rank.js +1 -1
  24. package/src/core/roles.js +7 -7
  25. package/src/core/shard.js +12 -8
  26. package/src/discover/apps.js +103 -67
  27. package/src/discover/corpora.js +1 -1
  28. package/src/discover/directory.js +3 -3
  29. package/src/market/cohort.js +1 -1
  30. package/src/market/demand.js +1 -1
  31. package/src/market/reactions.js +24 -18
  32. package/src/publish/devnet.js +146 -0
  33. package/src/publish/graph.js +1 -1
  34. package/src/publish/init.js +74 -0
  35. package/src/publish/lint.js +3 -2
  36. package/src/publish/ship.js +433 -288
  37. package/src/publish/view-cli.js +14 -10
  38. package/src/publish/view.js +485 -255
  39. package/src/taste/steam.js +1 -1
  40. package/src/taste/taste-doc.js +4 -3
  41. package/src/taste/taste.js +3 -3
  42. package/src/taste/vault.js +4 -2
  43. package/template/README.md +48 -0
  44. package/template/app.json +27 -0
  45. package/template/eval/goals.md +7 -0
  46. package/template/eval/golden.jsonl +0 -0
  47. package/template/gitignore +3 -0
  48. package/template/package.json +13 -0
  49. package/template/wrangler.json +12 -0
  50. package/plugin/scripts/fx.mjs +0 -268
  51. package/site/dist/assets/AccountNotFoundScreen-C4njXc69-BOfmiSH1.js +0 -1
  52. package/site/dist/assets/AddFundsSelectionScreen-RAoCn7ej-D_EsIeJu.js +0 -31
  53. package/site/dist/assets/Address-H8kD1d9O-C85sJ79B.js +0 -17
  54. package/site/dist/assets/AffirmativeConsentScreen-BG0MJxku-DdEPFOww.js +0 -6
  55. package/site/dist/assets/AllowlistRejectionScreen-Cae6e1Od-DhVy9iGN.js +0 -1
  56. package/site/dist/assets/ApiController-CYdiyu9B.js +0 -5
  57. package/site/dist/assets/ArrowsRightLeftIcon-cp60yobq.js +0 -1
  58. package/site/dist/assets/AuthenticateWithWalletScreen-CU6hIcFT-8rbYiYYE.js +0 -1
  59. package/site/dist/assets/AwaitingEvmToSolBridgingScreen-D0e_sZMS-2pkNfCno.js +0 -1
  60. package/site/dist/assets/AwaitingExternalEthereumTransferScreen-ur5HL4R7-7AQt25_k.js +0 -1
  61. package/site/dist/assets/AwaitingPasswordlessCodeScreen-DRHz76mM-BZY30TGs.js +0 -103
  62. package/site/dist/assets/AwaitingSolToEvmBridgingScreen-CdLRrGD2-BILOOLi_.js +0 -1
  63. package/site/dist/assets/BridgeNetworkSelectionView-CFGjB2nO-CvsbQcFv.js +0 -68
  64. package/site/dist/assets/CaptchaScreen-DbpvkIGx-D8eDcAkc.js +0 -1
  65. package/site/dist/assets/CheckCircleIcon-BM409mGI.js +0 -1
  66. package/site/dist/assets/CheckCircleIcon-NCOQc0Gs.js +0 -1
  67. package/site/dist/assets/Checkbox-D1EDeo41-rHlvsZzP.js +0 -52
  68. package/site/dist/assets/ChevronDownIcon-CW-4MSpE.js +0 -1
  69. package/site/dist/assets/Chip-CZKIKt9K-CutJG010.js +0 -26
  70. package/site/dist/assets/CoinbaseOnrampStatusScreen-BbyCJDXL-DLs9zX-Q.js +0 -16
  71. package/site/dist/assets/ConnectEmailForm-B-VRE0MN-N9J6T_YL.js +0 -3
  72. package/site/dist/assets/ConnectLedgerScreen-DWHABGot-h0uYIUi6.js +0 -10
  73. package/site/dist/assets/ConnectOnlyLandingScreen-CZYx9Bmp-LeuDMN1E.js +0 -1
  74. package/site/dist/assets/ConnectOnlyStatusScreen-DCRzjxHx-DaRa59tZ.js +0 -1
  75. package/site/dist/assets/ConnectOrCreateScreen-CUUYUO07-crHzh-TL.js +0 -1
  76. package/site/dist/assets/ConnectPhoneForm-Dek5xn4C-BEA4FmhI.js +0 -95
  77. package/site/dist/assets/ConnectWalletView-DqI-PTvX-CMQIad63.js +0 -74
  78. package/site/dist/assets/ConnectionStatusScreen-fUfROm9m-BeWLchcf.js +0 -7
  79. package/site/dist/assets/ConnectionStatusScreen-fUfROm9m-wu3a3kw4.js +0 -1
  80. package/site/dist/assets/ConnectorUtil-Cq4lWnfe.js +0 -21
  81. package/site/dist/assets/ConstantsUtil-DmLsRHAl.js +0 -198
  82. package/site/dist/assets/CopyToClipboard-i_OQSBJr-DujUHNzW.js +0 -39
  83. package/site/dist/assets/CopyableText-CQapvaMr-DMIdpDaU.js +0 -28
  84. package/site/dist/assets/CrossAppAuthScreen-CLw7ymZq-Csju14vF.js +0 -1
  85. package/site/dist/assets/CustomLandingScreenView-Cm_MBSyX-CBOrbNLo.js +0 -22
  86. package/site/dist/assets/DelegatedActionsConsentScreen-DYF2U4y5-BL5hzNUI.js +0 -1
  87. package/site/dist/assets/DelegatedActionsRevokeScreen-bPp6Uwyh-fcO51q5H.js +0 -1
  88. package/site/dist/assets/DepositAddressAddressStepView-4vdkb55L-B3vY888G.js +0 -187
  89. package/site/dist/assets/DepositAddressScreen-DBLRs7aI-DmdQaFI2.js +0 -8
  90. package/site/dist/assets/DepositAddressV3Screen-CDqIEMQw-BoupRyfV.js +0 -1
  91. package/site/dist/assets/EmailInputForm-Cqo1mda8-B2NG3K5V.js +0 -124
  92. package/site/dist/assets/EmbeddedWalletConnectingScreen-D4T7DUzs-D_AVgO4s.js +0 -1
  93. package/site/dist/assets/EmbeddedWalletCreatedScreen-D7WhW5ve-CLsJbPYQ.js +0 -1
  94. package/site/dist/assets/EmbeddedWalletKeyExportScreen-CqV4NYQ3-C9sAExzE.js +0 -23
  95. package/site/dist/assets/EmbeddedWalletOnAccountCreateScreen-BoKb3Q2S-DxaWAAKa.js +0 -1
  96. package/site/dist/assets/EmbeddedWalletPasswordUpdateScreen-D3rMlqS7-CxrfLr5q.js +0 -1
  97. package/site/dist/assets/EmbeddedWalletPasswordUpdateSplashScreen-BPFkWJeE-DHZRGoTN.js +0 -1
  98. package/site/dist/assets/EnrollTotp-Dibew4dp-SA2pvNEc.js +0 -66
  99. package/site/dist/assets/EnvelopeIcon-Dh2vSVj4.js +0 -1
  100. package/site/dist/assets/ErrorBanner-BcpGRt0h-BKOYTyCi.js +0 -21
  101. package/site/dist/assets/ErrorMessage-D8VaAP5m-CrZOnUyg.js +0 -8
  102. package/site/dist/assets/ErrorScreen-DipdTWzE-DcxVjMO4.js +0 -1
  103. package/site/dist/assets/ErrorScreen-DipdTWzE-c8g8En21.js +0 -4
  104. package/site/dist/assets/ExclamationCircleIcon-DUA2wMfO.js +0 -1
  105. package/site/dist/assets/ExclamationTriangleIcon-eZfaqcFq.js +0 -1
  106. package/site/dist/assets/FarcasterConnectStatusScreen-51tjUxqq-s_s5M2CZ.js +0 -75
  107. package/site/dist/assets/FarcasterSignerStatusScreen-6x-Uh95G-BHBR4k8H.js +0 -28
  108. package/site/dist/assets/FiatOnrampScreen-BAhYeiTI-BH_sU6EV.js +0 -491
  109. package/site/dist/assets/FingerPrintIcon-Dm6kxmg7.js +0 -1
  110. package/site/dist/assets/FundSolWalletWithExternalSolanaWallet-WtLwYaOl-B8TRV36j.js +0 -1
  111. package/site/dist/assets/FundWalletMethodHeader-BWNkokCT-BQdbJ5Bb.js +0 -1
  112. package/site/dist/assets/FundWithBankDepositScreen-DajkHxBD-9rPtyA3d.js +0 -58
  113. package/site/dist/assets/FundingEditAmountScreen-CpvpiO4M-GaXigGI9.js +0 -1
  114. package/site/dist/assets/FundingMethodSelectionScreen-Db7vhRlE-UsmtTqv4.js +0 -1
  115. package/site/dist/assets/GlobeAltIcon-vqrVV5ho.js +0 -1
  116. package/site/dist/assets/GooglePay-B53WnudL-BX5_WtCx.js +0 -1
  117. package/site/dist/assets/HCaptchaWrapper-CUi9LJsp-D-I8j7NO.js +0 -4
  118. package/site/dist/assets/InAppBrowserLoginNotPossible-BMR9br1T-BY3j50El.js +0 -1
  119. package/site/dist/assets/InfoBanner-Cb3p1z12-DLBl_Ywy.js +0 -22
  120. package/site/dist/assets/InjectedWalletIcon-DLcYOGDj-DLSV_NjP.js +0 -1
  121. package/site/dist/assets/InstallWalletScreen-DTA8dwLv-CxreyXoD.js +0 -1
  122. package/site/dist/assets/JsonTree-BHzNC-ic-DcNcbSoM.js +0 -34
  123. package/site/dist/assets/LabelXs-oqZNqbm_-iafrj25N.js +0 -6
  124. package/site/dist/assets/LandingScreen-CGWUoSUG-DK1EY-6v.js +0 -1
  125. package/site/dist/assets/Layouts-BMRfo5hw-CeCOyCLK.js +0 -77
  126. package/site/dist/assets/Link-BdDilT2T-BEDEdHuS.js +0 -40
  127. package/site/dist/assets/LinkConflictScreen-C3ZEPsgl-ClWoqvEL.js +0 -77
  128. package/site/dist/assets/LinkEmailScreen-dKo9frwc-C3v7glH0.js +0 -1
  129. package/site/dist/assets/LinkPasskeyScreen-zbrl_AI_-D72Gb0LW.js +0 -117
  130. package/site/dist/assets/LinkPasskeyScreen-zbrl_AI_-DYIl5Kr2.js +0 -1
  131. package/site/dist/assets/LinkPhoneScreen-oIUpfj0w-C0DuD14j.js +0 -1
  132. package/site/dist/assets/Loader-UAZqxsnw-Cy7hEE2f.js +0 -62
  133. package/site/dist/assets/LoadingSkeleton-BMsgO5PV-CiEyS5PZ.js +0 -17
  134. package/site/dist/assets/LockClosedIcon-BgY_5v_Z.js +0 -1
  135. package/site/dist/assets/LoginFailedScreen-CN0dJV6U-DhUMiJZJ.js +0 -1
  136. package/site/dist/assets/ManualTransferScreen-CHqcuqbs-tXiZDHmN.js +0 -1
  137. package/site/dist/assets/MfaAuthEnrollmentFlowScreen-Dj7gPTRC-CaW7xeCC.js +0 -1
  138. package/site/dist/assets/MfaAuthVerifyFlowScreen-DbFpyWMy-CeRqvx8k.js +0 -1
  139. package/site/dist/assets/MfaEnrollmentFlowScreen-DhLnGgnr-BkN1k4lh.js +0 -1
  140. package/site/dist/assets/MfaVerifyFlowScreen-C_XPjMRP-CXSqLVbE.js +0 -1
  141. package/site/dist/assets/ModalFooter-BCuEK_nz-c-khvG4Y.js +0 -403
  142. package/site/dist/assets/MoonpayStatusScreen-CzTUtokc-BRFN2eXr.js +0 -16
  143. package/site/dist/assets/OAuthStatusScreen-ufbOa-da-CeiGcSia.js +0 -1
  144. package/site/dist/assets/OpenLink-CUpJ1mOr-DfAvJRyK.js +0 -39
  145. package/site/dist/assets/PasskeySelectSignupOrLogin-xh2ScC_l-CIf5I7wU.js +0 -1
  146. package/site/dist/assets/PasskeyStatusScreen-Bla8MaOk-CLoL7QC-.js +0 -6
  147. package/site/dist/assets/PasswordRecoveryScreen-awGXZkQk-B9F67IYV.js +0 -20
  148. package/site/dist/assets/PhArrowCircleDown-D97G-BOA.js +0 -14
  149. package/site/dist/assets/PhArrowClockwise-DVgyFuez.js +0 -14
  150. package/site/dist/assets/PhArrowDown-87b1BizM.js +0 -14
  151. package/site/dist/assets/PhArrowLeft-Da0MfnbK.js +0 -14
  152. package/site/dist/assets/PhArrowRight-AdrIL4DW.js +0 -14
  153. package/site/dist/assets/PhArrowSquareOut-Bk2CMG3I.js +0 -14
  154. package/site/dist/assets/PhArrowUp-4sgyKPGx.js +0 -14
  155. package/site/dist/assets/PhArrowUpRight-BNghD2zq.js +0 -14
  156. package/site/dist/assets/PhArrowsClockwise-BXRn1GEl.js +0 -14
  157. package/site/dist/assets/PhArrowsDownUp-DM09Jo_3.js +0 -14
  158. package/site/dist/assets/PhArrowsLeftRight-MuKBLUoF.js +0 -14
  159. package/site/dist/assets/PhBank-fHbgCqqZ.js +0 -14
  160. package/site/dist/assets/PhBrowser-2-9m-AkM.js +0 -14
  161. package/site/dist/assets/PhCaretDown-BDNabFuL.js +0 -14
  162. package/site/dist/assets/PhCaretLeft-CI460R-A.js +0 -14
  163. package/site/dist/assets/PhCaretRight-D7ZS3Ls8.js +0 -14
  164. package/site/dist/assets/PhCaretUp-C-iIfyvO.js +0 -14
  165. package/site/dist/assets/PhCheck-DxQXn57r.js +0 -14
  166. package/site/dist/assets/PhCircleHalf-MJZeRBGF.js +0 -14
  167. package/site/dist/assets/PhClock-x3MS3Mwi.js +0 -14
  168. package/site/dist/assets/PhCompass-hYn3HhGN.js +0 -14
  169. package/site/dist/assets/PhCopy-CWNYcABI.js +0 -14
  170. package/site/dist/assets/PhCreditCard-Fy4pzwtt.js +0 -14
  171. package/site/dist/assets/PhCurrencyDollar-BgiaenCi.js +0 -14
  172. package/site/dist/assets/PhDesktop-CxrzCSv5.js +0 -14
  173. package/site/dist/assets/PhDeviceMobile-CsyA5Goi.js +0 -14
  174. package/site/dist/assets/PhDotsThree-D-k1BiYg.js +0 -14
  175. package/site/dist/assets/PhEnvelope-D8kzeNox.js +0 -14
  176. package/site/dist/assets/PhFunnelSimple-CfQs5DAW.js +0 -14
  177. package/site/dist/assets/PhGlobe-DvmSc9_A.js +0 -14
  178. package/site/dist/assets/PhIdentificationCard-CfkZm-5K.js +0 -14
  179. package/site/dist/assets/PhImage-Cf0J2ZfJ.js +0 -14
  180. package/site/dist/assets/PhInfo-Bwi51-2X.js +0 -14
  181. package/site/dist/assets/PhLightbulb-C4FZV7Es.js +0 -14
  182. package/site/dist/assets/PhMagnifyingGlass-C9u8jSCJ.js +0 -14
  183. package/site/dist/assets/PhPaperPlaneRight-zxQVvPLX.js +0 -14
  184. package/site/dist/assets/PhPlus-C4UoW-2n.js +0 -14
  185. package/site/dist/assets/PhPower-CFyWDmgD.js +0 -14
  186. package/site/dist/assets/PhPuzzlePiece-BjoNVfST.js +0 -14
  187. package/site/dist/assets/PhQrCode-CP3tFauY.js +0 -14
  188. package/site/dist/assets/PhQuestion-wOWJBd3G.js +0 -14
  189. package/site/dist/assets/PhQuestionMark-CV0k9C0Y.js +0 -14
  190. package/site/dist/assets/PhSealCheck-vIKzREuN.js +0 -14
  191. package/site/dist/assets/PhSignOut-CxELV-i0.js +0 -14
  192. package/site/dist/assets/PhSpinner-CGDmbzUd.js +0 -14
  193. package/site/dist/assets/PhTrash-C9PD0SAt.js +0 -14
  194. package/site/dist/assets/PhUser-0sWNXQeZ.js +0 -14
  195. package/site/dist/assets/PhVault-6JTskUcC.js +0 -14
  196. package/site/dist/assets/PhWarning-CoKs3gz6.js +0 -14
  197. package/site/dist/assets/PhWarningCircle-BXHw1fCt.js +0 -14
  198. package/site/dist/assets/PhX-DX0SFIqC.js +0 -14
  199. package/site/dist/assets/PhoneIcon-CvIiPwYQ.js +0 -1
  200. package/site/dist/assets/PinInput-DbZ0b1i1-CDXZBdQx.js +0 -247
  201. package/site/dist/assets/PrivyPluginContext-DA-yS5vO-CtQWBiV2.js +0 -1
  202. package/site/dist/assets/QrCode-DbnZwlkH-h6Bhoe5U.js +0 -22
  203. package/site/dist/assets/RecoveryOAuthStatusScreen-CsgtTI2x-D-A5P3PX.js +0 -1
  204. package/site/dist/assets/RecoveryPasswordCreateScreen-Be-qJ-LU-C8lTSeSr.js +0 -1
  205. package/site/dist/assets/RecoverySelectionScreen-D1iGtpmi-P5P3plkH.js +0 -7
  206. package/site/dist/assets/SIWXUtil-TUCxa7aA.js +0 -1
  207. package/site/dist/assets/Screen-D30meWsY-CCeVNuWs.js +0 -175
  208. package/site/dist/assets/ScreenHeader-CHmc4-Lu-DjyrH7IK.js +0 -31
  209. package/site/dist/assets/ScreenLayout-OwaQQcxH-CrQM6kvA.js +0 -1
  210. package/site/dist/assets/SelectSourceAsset-CWFog6gm-D7yTw87h.js +0 -140
  211. package/site/dist/assets/SetAutomaticRecoveryScreen-BO_OEqAZ-f0iDIGmw.js +0 -14
  212. package/site/dist/assets/SetWalletPasswordForm-DwCTVtAh-kq1zvA7d.js +0 -11
  213. package/site/dist/assets/ShieldCheckIcon-o6RHYev1.js +0 -1
  214. package/site/dist/assets/SignRequestScreen-BHKv32KR-DDpMYyJ1.js +0 -26
  215. package/site/dist/assets/SolanaWalletConnector-C7Lqk-Wz-DPwhuzcI.js +0 -1
  216. package/site/dist/assets/StackedContainer-B2vaEl56-eQCM2cmv.js +0 -24
  217. package/site/dist/assets/StandardSignAndSendTransactionScreen-CfhIXVtd-BJ-onCjv.js +0 -20
  218. package/site/dist/assets/Subtitle-CV-2yKE4-zxWw2m1Q.js +0 -13
  219. package/site/dist/assets/TelegramAuthScreen-UL9zLfO8-NWlMgNRX.js +0 -1
  220. package/site/dist/assets/Title-BnzYV3Is-B_JpTatT.js +0 -7
  221. package/site/dist/assets/TodoList-DnyULl18-C8Eyy28X.js +0 -36
  222. package/site/dist/assets/TransactionDetails-e9nesl30-C4MpIHR-.js +0 -109
  223. package/site/dist/assets/TransactionErrorView-BDBNqA_f-p1T9WVMx.js +0 -178
  224. package/site/dist/assets/TransferFromWalletScreen-Qvn1Yuno-88IQcRnY.js +0 -31
  225. package/site/dist/assets/TransferOrBridgeLoadingScreen-Dl1I_iIz-1fDAIxAf.js +0 -6
  226. package/site/dist/assets/TurnstileWrapper-B7sNddys-Jwy1tJtm.js +0 -1
  227. package/site/dist/assets/UpdateEmailScreen-DVOhPV0H-Jj2DQW8D.js +0 -1
  228. package/site/dist/assets/UpdatePhoneScreen-CXRswTNZ-DwYIUBqi.js +0 -1
  229. package/site/dist/assets/UserLimitReachedScreen-5we2DHn8-D7m70C4b.js +0 -1
  230. package/site/dist/assets/Value-DTgR824E-BRuIGiec.js +0 -27
  231. package/site/dist/assets/WalletCards-DH1rqayz-Bgp9Fsgs.js +0 -1
  232. package/site/dist/assets/WalletIcon-gKf0gTw6.js +0 -1
  233. package/site/dist/assets/WalletInfoCard-sq_2wgFg-DpNSAGte.js +0 -29
  234. package/site/dist/assets/WalletInterstitialScreen-Sh7LfsAh-DG-RuO-F.js +0 -1
  235. package/site/dist/assets/WalletLink-DK5LB5Ha-DHaL350n.js +0 -20
  236. package/site/dist/assets/WalletOverflowButton-bfCnp-9T-CUyIJFzt.js +0 -1
  237. package/site/dist/assets/WarningBanner-ZZqCEtZK-2eU4Qo74.js +0 -21
  238. package/site/dist/assets/__vite-browser-external-CitKSmYk.js +0 -1
  239. package/site/dist/assets/_esm-B26FZg5Y.js +0 -7
  240. package/site/dist/assets/_esm-BdeM6pxJ.js +0 -1
  241. package/site/dist/assets/accounts-D96UKWkC.js +0 -1
  242. package/site/dist/assets/accounts-DiZ-0VmN.js +0 -1
  243. package/site/dist/assets/analytics-mkkvFRju-BRBcadka.js +0 -1
  244. package/site/dist/assets/baseSepolia-DX-qGsik.js +0 -1
  245. package/site/dist/assets/basic-oIwwSyu4.js +0 -1575
  246. package/site/dist/assets/capitalizeFirstLetter-DmLYqXsO-DtOXx-2n.js +0 -1
  247. package/site/dist/assets/ccip-65sIfSPR.js +0 -1
  248. package/site/dist/assets/ccip-BZvRTL6g.js +0 -7
  249. package/site/dist/assets/ccip-DA_zG341.js +0 -1
  250. package/site/dist/assets/chain-DM3oH6Px.js +0 -8
  251. package/site/dist/assets/check-BPgcwEPP.js +0 -1
  252. package/site/dist/assets/chevron-down-BSsbXkK-.js +0 -1
  253. package/site/dist/assets/circle-alert-DWaeLZNj.js +0 -1
  254. package/site/dist/assets/circle-check-big-Chb1ivw9.js +0 -1
  255. package/site/dist/assets/circle-x-AV9Cet0R.js +0 -1
  256. package/site/dist/assets/client-CZG0ndth.js +0 -2
  257. package/site/dist/assets/client-Dsa7_3V3.js +0 -9
  258. package/site/dist/assets/coinbase-DCPC2L_R-D3aIgpyd.js +0 -3
  259. package/site/dist/assets/copy-Ckr0AkL6.js +0 -1
  260. package/site/dist/assets/core-snws5xwv.js +0 -12
  261. package/site/dist/assets/createLucideIcon-IPW50ckY.js +0 -1
  262. package/site/dist/assets/defineChain-CiuCXUZW.js +0 -1
  263. package/site/dist/assets/dijkstra-Drfl93V4.js +0 -1
  264. package/site/dist/assets/dist-Cm7pNAtL.js +0 -1
  265. package/site/dist/assets/dist-CoS6E-3x.js +0 -1
  266. package/site/dist/assets/dist-Dl07uGq1.js +0 -1
  267. package/site/dist/assets/dist-DnGANz51.js +0 -16
  268. package/site/dist/assets/embed.worker-ugGhEqCV.js +0 -1
  269. package/site/dist/assets/errors-BQ3N-RVt-BLqjTPNR.js +0 -1
  270. package/site/dist/assets/esm-B5cbIQaz.js +0 -37
  271. package/site/dist/assets/esm-ByALO6mA.js +0 -1
  272. package/site/dist/assets/esm-C3DslVJx.js +0 -1
  273. package/site/dist/assets/esm-Ci1ogKuK.js +0 -1
  274. package/site/dist/assets/esm-n2lWo7kz.js +0 -1
  275. package/site/dist/assets/ethers-zTjbodk5-wHhL_BQ1.js +0 -1
  276. package/site/dist/assets/eventemitter3-CL76jPsW.js +0 -1
  277. package/site/dist/assets/external-link-D-uehwM2.js +0 -1
  278. package/site/dist/assets/farcaster-DPlSjvF5-BqTvDXBR.js +0 -1
  279. package/site/dist/assets/features-DQ0UYy6K.js +0 -2
  280. package/site/dist/assets/fingerprint-pattern-D8ytA16Q.js +0 -1
  281. package/site/dist/assets/floating-ui.react-dom-DKWOL9UG.js +0 -1
  282. package/site/dist/assets/floating-ui.react-xRkl9O3u.js +0 -1
  283. package/site/dist/assets/formatErc20TokenAmount-BuPk9xcy-BVqOKQif.js +0 -1
  284. package/site/dist/assets/formatter-CVbp4SlJ.js +0 -1
  285. package/site/dist/assets/get-entropy-details-for-user-dHRH5Gpi-Dukf7_kJ.js +0 -1
  286. package/site/dist/assets/getChainName-DjpPdUSc-DUyKDrnp.js +0 -1
  287. package/site/dist/assets/getErc20Balance-jXNpFf0N-Bb9WM31M.js +0 -1
  288. package/site/dist/assets/getErc20TokenInfo-BNTrzLMP-KPrdn04K.js +0 -1
  289. package/site/dist/assets/getFormattedUsdFromLamports-De3U9GlO-Gx4_wxON.js +0 -1
  290. package/site/dist/assets/getUsdcMintAddress-BVgm9GRo-r2UnDeEy.js +0 -1
  291. package/site/dist/assets/guards-BIW-GiMJ-CHfXo8XI.js +0 -1
  292. package/site/dist/assets/hooks.module-BYZGfwDl.js +0 -1
  293. package/site/dist/assets/hourglass-CPG1HZmf.js +0 -1
  294. package/site/dist/assets/id-UqEwL2BI-BkKm533U.js +0 -1
  295. package/site/dist/assets/index-1HYFYA-W.js +0 -8
  296. package/site/dist/assets/index-BdEw5wyl-ChSJvPZk.js +0 -1
  297. package/site/dist/assets/index-CWARkn2w-Cezl_uBr.js +0 -61
  298. package/site/dist/assets/index-DvMddJcd-BatvgC-S.js +0 -12
  299. package/site/dist/assets/index-YiCw0Y_x-CX5i3Z87.js +0 -437
  300. package/site/dist/assets/index.browser-B-i-Lj3J.js +0 -1
  301. package/site/dist/assets/index.browser-BMmUcykv.js +0 -1
  302. package/site/dist/assets/index.es-BmqyoBEM.js +0 -2
  303. package/site/dist/assets/index.es-C1v1or--.js +0 -3
  304. package/site/dist/assets/isPaymentRequestAvailable-Bq1cemEn-By6r0dVB.js +0 -1
  305. package/site/dist/assets/jsx-runtime-BbDfbRii.js +0 -1
  306. package/site/dist/assets/landmark-CH6LV6F0.js +0 -1
  307. package/site/dist/assets/lib-BsS8z8Lj.js +0 -1
  308. package/site/dist/assets/localBatchGatewayRequest-3kLrX_c7.js +0 -32
  309. package/site/dist/assets/lock-DcskI4Bb.js +0 -1
  310. package/site/dist/assets/logger-CsXm3oIv-B0UYpNFI.js +0 -1
  311. package/site/dist/assets/mail-Bs0r5yOi.js +0 -1
  312. package/site/dist/assets/modal-context-CwmSeFcv-DruX9J1E.js +0 -1
  313. package/site/dist/assets/native-BKj27wfc.js +0 -1
  314. package/site/dist/assets/ofetch.CWycOUEr-CHp-crOQ.js +0 -1
  315. package/site/dist/assets/parseAbi-Csg-8t59.js +0 -2
  316. package/site/dist/assets/parseAbiParameters-BWbIMYe4.js +0 -1
  317. package/site/dist/assets/parseEther-VjhVo3dd.js +0 -1
  318. package/site/dist/assets/pkce-DXF0yKjP-ChQhE0st.js +0 -1
  319. package/site/dist/assets/preload-helper-uBIymjUX.js +0 -1
  320. package/site/dist/assets/privy-context-81KmsBw2-CjWGzyg7.js +0 -2
  321. package/site/dist/assets/property-DuZ9DLP4.js +0 -3
  322. package/site/dist/assets/react-DCXkXSrv.js +0 -1
  323. package/site/dist/assets/react-DQnMl2Xv.js +0 -1
  324. package/site/dist/assets/react-DrJ0MmMO.js +0 -1
  325. package/site/dist/assets/react-auth-sHnipzbS.js +0 -1
  326. package/site/dist/assets/react-dom-5dW7aKoP.js +0 -1
  327. package/site/dist/assets/reservoir-x-nGuZkT-BMX127i6.js +0 -1
  328. package/site/dist/assets/roles-32M1l9jI.js +0 -1
  329. package/site/dist/assets/rolldown-runtime-BcKkbAw3.js +0 -1
  330. package/site/dist/assets/safe-url-D7SRPu33-CkMedGro.js +0 -1
  331. package/site/dist/assets/secp256k1-C4nzcPo-.js +0 -1
  332. package/site/dist/assets/secp256k1-D92aGNnU.js +0 -1
  333. package/site/dist/assets/secp256k1-DmbwyFau.js +0 -1
  334. package/site/dist/assets/secp256k1-_MleGpqL.js +0 -1
  335. package/site/dist/assets/secp256k1-xl2og-wO.js +0 -1
  336. package/site/dist/assets/serializeAuthorizationList-CX8hGL56.js +0 -1
  337. package/site/dist/assets/serializeSignature-DfxkPnu-.js +0 -2
  338. package/site/dist/assets/sha2-BU_QyRTx.js +0 -1
  339. package/site/dist/assets/shared-DRZUmnL5-xqAUARWo.js +0 -193
  340. package/site/dist/assets/shared-FM0rljBt--Avg6q2R.js +0 -21
  341. package/site/dist/assets/shouldProceedtoEmbeddedWalletCreationFlow-B3vyavrh-BK_FBkaE.js +0 -1
  342. package/site/dist/assets/simulateTransaction-CfuOrae4-64lWYJAL.js +0 -1
  343. package/site/dist/assets/smartphone-CcEZ-XAy.js +0 -1
  344. package/site/dist/assets/src-BBjkfi-Q.js +0 -1
  345. package/site/dist/assets/storage-Ufl800Ne-D0bZBw-H.js +0 -9
  346. package/site/dist/assets/stringify-BlDm7tuc.js +0 -21
  347. package/site/dist/assets/stripe.esm-CZRDpc-N.js +0 -2
  348. package/site/dist/assets/styles-BsotlekN-CXEB4Ud9.js +0 -9
  349. package/site/dist/assets/styles-Cm9H63WO-Br4QtzUL.js +0 -92
  350. package/site/dist/assets/styles-DVyDvTdj-BTABn2K8.js +0 -74
  351. package/site/dist/assets/styles-hslXy6xG-D-49b1rk.js +0 -164
  352. package/site/dist/assets/telegram-B-JqnkqZ-RBoY3PLx.js +0 -1
  353. package/site/dist/assets/to-ui-error-DWe-qwS3-Cx2bk1kZ.js +0 -4
  354. package/site/dist/assets/toViemAccount-B5iDGq1b-Ba8L7zed.js +0 -2
  355. package/site/dist/assets/transaction-BNTP-bFm-M5tJx8vY.js +0 -1
  356. package/site/dist/assets/transfer-6YztDh-t-BVQkTvG-.js +0 -1
  357. package/site/dist/assets/transformers.web-CVP3pJOS.js +0 -40
  358. package/site/dist/assets/transformers.web-D8W1u_I2.js +0 -49
  359. package/site/dist/assets/triangle-alert-CWjYH75H.js +0 -1
  360. package/site/dist/assets/twitch-5IOe4sIQ-lDtgk0os.js +0 -1
  361. package/site/dist/assets/types-4CuOD23i.js +0 -1
  362. package/site/dist/assets/use-headless-crypto-deposit-Yn-QRLB3-LUIVFBSd.js +0 -1
  363. package/site/dist/assets/use-unlink-wallet-BKK4_5Ur-CC2pq27z.js +0 -1
  364. package/site/dist/assets/useActiveWallet-B-yZYGc6-D7Ik-LxJ.js +0 -123
  365. package/site/dist/assets/useGetSolPrice-x7gfUIHJ-D1hmQfpA.js +0 -1
  366. package/site/dist/assets/useGetTokenPrice-CaoUkb79-ZnbuJ-vP.js +0 -1
  367. package/site/dist/assets/useI18n-C-5n_noF-BcqC76yj.js +0 -1
  368. package/site/dist/assets/useWalletBalance-CZnP5f_f-Dw2GIJ4g.js +0 -1
  369. package/site/dist/assets/useWallets-X1ndvJ_U-DRjz7uYD.js +0 -1
  370. package/site/dist/assets/utils-BkR7EQjg.js +0 -1
  371. package/site/dist/assets/utils-DJhBNOZA.js +0 -1
  372. package/site/dist/assets/utils-DdckVfQr.js +0 -1
  373. package/site/dist/assets/w3m-modal-BeVrBOIf.js +0 -1174
  374. package/site/dist/assets/wallet-Bn85Sg2W.js +0 -1
  375. package/site/dist/assets/wallet-Bnzeiqed.js +0 -2
  376. package/site/dist/assets/wallet-connect-BvJtUsDt-Y03hhqFN.js +0 -1
  377. package/site/dist/assets/walletconnect-registry-xxG8XLCb-D1SSFvMp.js +0 -1
  378. package/site/dist/assets/walletconnect-v2-3frQFwbg-CrtLXofr.js +0 -12
  379. package/site/dist/assets/wui-ux-by-reown-D34ILNLS.js +0 -1147
  380. package/site/dist/assets/x-rlSp9U7W.js +0 -1
  381. package/src/discover/explore.js +0 -271
  382. package/src/market/settle.js +0 -165
  383. package/src/market/terms.js +0 -200
  384. package/src/publish/cli.js +0 -336
  385. package/src/publish/envelope.js +0 -185
  386. package/src/publish/manifest.js +0 -142
@@ -29,7 +29,8 @@ const PLUMBING = new Set(["text", "vector", "norm", "id", "owner", "embed"]);
29
29
  export const brief = (r, roles, fields = []) => {
30
30
  const out = briefOf(r, roles, fields);
31
31
  const link = linkOf(r, roles);
32
- return link ? { ...out, url: link } : out;
32
+ // Whose it is, and its price when sold: what an agent needs to cite or buy a hit.
33
+ return { ...out, ...(link ? { url: link } : {}), ...(r.owner ? { publisher: r.owner } : {}), ...(r.sale ? { sale: r.sale } : {}) };
33
34
  };
34
35
 
35
36
  /**
@@ -275,7 +276,7 @@ export function getRow(rows, id, roles) {
275
276
  }
276
277
 
277
278
  // ── self-check: `node tools.js` — no browser, no network ────────────────────
278
- if (typeof process !== "undefined" && import.meta.url === `file://${process.argv[1]}` && process.argv[1].endsWith("/tools.js")) {
279
+ if (import.meta.url === `file://${globalThis.process?.argv?.[1]}` && process.argv[1].endsWith("/tools.js")) {
279
280
  const { rolesFrom } = await import("../core/roles.js");
280
281
  const row = (id, fields, vector) => ({
281
282
  id, owner: "0xa", ...fields,
package/src/agent/ui.js CHANGED
@@ -505,7 +505,7 @@ export function stage({ picks = [], heading = "", note = "" } = {}) {
505
505
  }
506
506
 
507
507
  // ── self-check: `node src/agent/ui.js` ────────────────────────────────────────
508
- if (typeof process !== "undefined" && import.meta.url === `file://${process.argv[1]}` && process.argv[1].endsWith("/ui.js")) {
508
+ if (import.meta.url === `file://${globalThis.process?.argv?.[1]}` && process.argv[1].endsWith("/ui.js")) {
509
509
  const roles = { measures: ["rating", "year", "runtime", "votes"] };
510
510
  const html = rankedList({
511
511
  heading: 'search "frozen"', note: "2 corpora",
@@ -11,7 +11,7 @@
11
11
  // - a search goes to the 3 views whose coverage sketch best matches the query,
12
12
  // not to all of them (Kingsfoil: ~3 of 23 views, ~92% of the true top 10 with
13
13
  // a 32-vector sketch). The result names what was searched and what was not.
14
- // - `warm()` starts the embedder the moment an app is opened, and a search
14
+ // - `warm()` starts the embedder before the first query, and a search
15
15
  // never waits on a first-time model download.
16
16
  //
17
17
  // `get`/`similar`/`count` answer over what is loaded, and say so.
@@ -69,9 +69,10 @@ const WHERE = { type: "object", description: 'Equality filters, e.g. {"phase":"P
69
69
  * The tools for one app, or null when its card names no views.
70
70
  * `app` is `{ name, desc, views }` from a verified card. `fetchCatalog(url)` may
71
71
  * answer from a stale copy: the survey only describes and routes, and loading a
72
- * view re-reads its catalog through shard.js either way.
72
+ * view re-reads its catalog through shard.js either way. `queryVector(q)` embeds a
73
+ * query (null to rank by words); a Worker passes its own encoder.
73
74
  */
74
- export function viewTools(app, { fetchCatalog = fetch } = {}) {
75
+ export function viewTools(app, { fetchCatalog = fetch, queryVector: embedQ = queryVector } = {}) {
75
76
  if (!app.views?.length) return null;
76
77
  const names = new Map(); // view url → its name in every result
77
78
  const label = (url) => names.get(url) ?? url.split("/").pop();
@@ -85,6 +86,7 @@ export function viewTools(app, { fetchCatalog = fetch } = {}) {
85
86
  view, name: label(view),
86
87
  description: domains.map((d) => d.description).filter(Boolean).join(" "),
87
88
  count: domains.reduce((s, d) => s + (d.count ?? 0), 0),
89
+ repos: domains.filter((d) => d.publisher).map((d) => ({ publisher: d.publisher, repo: d.repo, rows: d.count ?? 0, description: d.description ?? "" })),
88
90
  coverage: { vectors: domains.flatMap((d) => d.coverage?.vectors ?? []) },
89
91
  };
90
92
  })).catch((e) => { surveyed = null; throw e; }));
@@ -111,11 +113,12 @@ export function viewTools(app, { fetchCatalog = fetch } = {}) {
111
113
  const run = {
112
114
  describe: async () => ({
113
115
  app: app.name, description: app.desc,
114
- views: (await survey()).map((v) => ({ view: v.name, rows: v.count, description: v.description, loaded: loaded.has(v.view) })),
116
+ views: (await survey()).map((v) => ({ view: v.name, rows: v.count, description: v.description, loaded: loaded.has(v.view),
117
+ ...(v.repos.length ? { repos: v.repos } : {}) })),
115
118
  }),
116
119
  search: async ({ query, limit = 10, views, where: w }) => {
117
120
  if (!query) throw new Error("query is required");
118
- const qv = await queryVector(query);
121
+ const qv = await embedQ(query);
119
122
  const ranked = views?.length ? await pick(views) : (await route(query, qv)).slice(0, ROUTE_TOP);
120
123
  const others = views?.length ? [] : (await route(query, qv)).slice(ROUTE_TOP, ROUTE_TOP + 5).map((v) => v.name);
121
124
  const per = await Promise.all(ranked.map(async (v) =>
@@ -125,7 +128,9 @@ export function viewTools(app, { fetchCatalog = fetch } = {}) {
125
128
  next_views: others, hits };
126
129
  },
127
130
  get: async ({ id, view }) => {
128
- if (view) for (const v of await pick([view])) await load(v);
131
+ // Nothing loaded (a fresh Worker isolate: each request may land on a new one): load every view.
132
+ // ponytail: downloads every view for an app with many; route by id when that shows.
133
+ for (const v of view ? await pick([view]) : loaded.size ? [] : await survey()) await load(v);
129
134
  for (const [v, rows] of loaded) { const r = getRow(rows, id, rolesOf(v)); if (r) return r; }
130
135
  return { error: `${id} is not in a loaded view. Search for it, or pass its view.`, loaded: [...loaded.keys()].map(label) };
131
136
  },
@@ -173,30 +178,6 @@ export function viewTools(app, { fetchCatalog = fetch } = {}) {
173
178
  { name: "threads", description: `One thing followed across records of a ${n} view (a matter from committee to council): steps in date order, the latest-moving first. Views that declare no thread say so.`,
174
179
  inputSchema: { type: "object", properties: { view: str("View name from describe"), id: str("One thread, by a step's thread field"), limit: num("default 20"), offset: num("default 0"), where: WHERE }, required: ["view"] } },
175
180
  ];
176
- // Records the app sells over x402 (its card's `paid`). Paid from this server's wallet,
177
- // never above FANGORN_MCP_MAX_PRICE, and checked against the sha256 the app published
178
- // for that record, so what was paid for is what was committed.
179
- if (app.paid) {
180
- const p = app.paid, dec = p.decimals ?? 6;
181
- const price = `${(Number(p.price) / 10 ** dec).toFixed(dec).replace(/0+$/, "").replace(/\.$/, "")} ${p.symbol ?? "USDC"}`;
182
- const cap = BigInt(Math.round(Number(process.env.FANGORN_MCP_MAX_PRICE ?? "0.10") * 10 ** dec));
183
- run.buy = async ({ id }) => {
184
- if (!id) throw new Error("id is required");
185
- const hit = loadedRows().find(({ r, view }) => r.id === id || r[rolesOf(view).identity] === id);
186
- const key = hit ? hit.r[rolesOf(hit.view).identity] ?? id : id;
187
- const { buyRecord } = await import("./x402.js");
188
- const want = hit?.r.paid_sha256;
189
- const r = await buyRecord(p, key, { want, privateKey: process.env.FANGORN_MCP_WALLET_KEY, maxPrice: cap });
190
- if (r.error) return r;
191
- return { id: key, paid: r.price ? price : "free", transaction: r.receipt?.transaction ?? null, network: r.receipt?.network ?? p.network,
192
- verified: want ? (r.verified ? "matches the published sha256" : `MISMATCH: published ${want}, got ${r.digest}`) : "not checked (search for the record first)",
193
- record: r.record };
194
- };
195
- tools.push({ name: "buy", description: `Buy one ${n} record's paid detail for ${price} (x402 on ${p.network}): ${p.description || "the structured record"}. ` +
196
- "Pass an id from search. Paid from this server's wallet (FANGORN_MCP_WALLET_KEY), never above FANGORN_MCP_MAX_PRICE.",
197
- inputSchema: { type: "object", properties: { id: str("A record id from search or get") }, required: ["id"] } });
198
- }
199
-
200
181
  return { tools, call: async (name, args = {}) => {
201
182
  if (!run[name]) throw new Error(`no tool ${name}`);
202
183
  return text(await run[name](args));
package/src/agent/x402.js CHANGED
@@ -1,105 +1,214 @@
1
- // Paying for a resource over x402 (v1, "exact" scheme): 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 `accepts`: what it costs, in which token, on which chain, to whom.
6
- // 2. Sign an EIP-3009 transferWithAuthorization for exactly that (EIP-712, the token's own
7
- // domain). Nothing is sent on chain by the buyer: the seller's facilitator submits it.
8
- // 3. GET url again with X-PAYMENT: the seller verifies, settles, and returns the resource,
9
- // with the settlement (the tx hash) in X-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.
10
19
  //
11
- // `maxPrice` (token base units) is checked BEFORE signing: a seller cannot ask for more
12
- // 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";
13
25
 
14
- const CHAIN_IDS = { "arbitrum-sepolia": 421614, arbitrum: 42161, "base-sepolia": 84532, base: 8453 };
15
26
  const b64 = (s) => (typeof btoa === "function" ? btoa(s) : Buffer.from(s, "utf8").toString("base64"));
16
27
  const unb64 = (s) => (typeof atob === "function" ? atob(s) : Buffer.from(s, "base64").toString("utf8"));
28
+ const semaphore = () => import("@semaphore-protocol/core").catch(() => {
29
+ throw new Error("buying needs @semaphore-protocol/core: npm i @semaphore-protocol/core");
30
+ });
17
31
 
18
- /** The one requirement we can pay: exact scheme, a chain we know, within `maxPrice`. */
19
- export function choose(accepts = [], { maxPrice, networks = Object.keys(CHAIN_IDS) } = {}) {
20
- const ok = accepts.filter((a) => a.scheme === "exact" && networks.includes(a.network));
21
- if (!ok.length) throw new Error(`no payable option: ${JSON.stringify(accepts.map((a) => `${a.scheme}/${a.network}`))}`);
22
- const cheapest = ok.sort((a, b) => (BigInt(a.maxAmountRequired) < BigInt(b.maxAmountRequired) ? -1 : 1))[0];
23
- if (maxPrice != null && BigInt(cheapest.maxAmountRequired) > BigInt(maxPrice))
24
- throw new Error(`price ${cheapest.maxAmountRequired} exceeds the cap ${maxPrice} (token base units)`);
25
- return cheapest;
32
+ /** A 402's PaymentRequired, from its PAYMENT-REQUIRED header. */
33
+ export function paymentRequired(res) {
34
+ const h = res.headers.get("PAYMENT-REQUIRED");
35
+ if (!h) throw new Error(`HTTP ${res.status} has no PAYMENT-REQUIRED header (not x402 v2)`);
36
+ return JSON.parse(unb64(h));
26
37
  }
27
38
 
28
- /** The X-PAYMENT header value for requirement `req`, signed by `account` (a viem account). */
29
- export async function paymentHeader(req, account, { now = Math.floor(Date.now() / 1000) } = {}) {
30
- const chainId = CHAIN_IDS[req.network];
31
- if (!chainId) throw new Error(`unknown network ${req.network}`);
32
- const nonce = `0x${[...crypto.getRandomValues(new Uint8Array(32))].map((b) => b.toString(16).padStart(2, "0")).join("")}`;
33
- const authorization = {
34
- from: account.address, to: req.payTo, value: String(req.maxAmountRequired),
35
- validAfter: String(now - 600), validBefore: String(now + (req.maxTimeoutSeconds ?? 120)), nonce,
36
- };
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;
45
+ }
46
+
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) };
37
58
  const signature = await account.signTypedData({
38
- domain: { name: req.extra?.name ?? "USD Coin", version: req.extra?.version ?? "2", chainId, verifyingContract: req.asset },
39
- types: { TransferWithAuthorization: [
40
- { 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" },
41
61
  { name: "validAfter", type: "uint256" }, { name: "validBefore", type: "uint256" }, { name: "nonce", type: "bytes32" }] },
42
- primaryType: "TransferWithAuthorization",
43
- message: { ...authorization, value: BigInt(authorization.value), validAfter: BigInt(authorization.validAfter), validBefore: BigInt(authorization.validBefore) },
62
+ primaryType: "ReceiveWithAuthorization", message,
44
63
  });
45
- return b64(JSON.stringify({ x402Version: 1, scheme: "exact", network: req.network, payload: { signature, authorization } }));
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 };
46
67
  }
47
68
 
48
- /** GET `url`, paying once if it answers 402. Returns { status, body, price, receipt }. */
49
- export async function payAndFetch(url, { privateKey, account, maxPrice, fetchImpl = fetch } = {}) {
50
- const first = await fetchImpl(url);
51
- if (first.status !== 402) return { status: first.status, body: await first.text(), price: null, receipt: null };
52
- const offer = await first.json();
53
- const req = choose(offer.accepts, { maxPrice });
54
- if (!account) {
55
- if (!privateKey) throw new Error(`${url} costs ${req.maxAmountRequired} (base units of ${req.asset} on ${req.network}); no wallet configured to pay`);
56
- account = (await import("viem/accounts")).privateKeyToAccount(privateKey);
57
- }
58
- const paid = await fetchImpl(url, { headers: { "X-PAYMENT": await paymentHeader(req, account) } });
59
- const receiptHeader = paid.headers.get("X-PAYMENT-RESPONSE");
60
- const body = await paid.text();
61
- if (paid.status === 402) throw new Error(`payment refused: ${JSON.parse(body).error ?? body}`);
62
- return { status: paid.status, body, price: req.maxAmountRequired, receipt: receiptHeader ? JSON.parse(unb64(receiptHeader)) : null };
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) };
63
74
  }
64
75
 
65
- /** Buy one record an app sells (its agent card's `paid`) and check it against the sha256 the
66
- * app published for it (`want`, the row's `paid_sha256`): what was paid for is what was
67
- * committed. Shared by fangorn-mcp's `buy` and the stock page. */
68
- export async function buyRecord(paid, key, { want, account, privateKey, maxPrice = paid.price, fetchImpl } = {}) {
69
- const r = await payAndFetch(paid.url.replace("{id}", encodeURIComponent(key)), { account, privateKey, maxPrice, fetchImpl });
70
- if (r.status !== 200) return { id: key, error: `HTTP ${r.status}: ${r.body.slice(0, 200)}` };
71
- const digest = [...new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(r.body)))]
72
- .map((b) => b.toString(16).padStart(2, "0")).join("");
73
- return { id: key, price: r.price, receipt: r.receipt, digest, verified: want ? want === digest : null, record: JSON.parse(r.body) };
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}`); }
95
+ }
96
+
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 });
112
+ }
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);
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 });
128
+ }
129
+
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));
134
+ }
135
+ return { record, fields: opened, price: req.amount, tx };
74
136
  }
75
137
 
76
- // ── self-check: `node src/agent/x402.js` — a stand-in seller that checks the signature ──
77
- if (typeof process !== "undefined" && import.meta.url === `file://${process.argv[1]}` && process.argv[1].endsWith("/x402.js")) {
78
- const { privateKeyToAccount, generatePrivateKey } = await import("viem/accounts");
79
- const { verifyTypedData } = await import("viem");
80
- const buyer = privateKeyToAccount(generatePrivateKey());
81
- const USDC = "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d", SELLER = `0x${"5e".repeat(20)}`;
82
- const req = { scheme: "exact", network: "arbitrum-sepolia", maxAmountRequired: "10000", resource: "https://x/paid/1",
83
- payTo: SELLER, asset: USDC, maxTimeoutSeconds: 120, extra: { name: "USD Coin", version: "2" } };
84
- let settled = null;
85
- const seller = async (url, init = {}) => {
86
- const h = init.headers?.["X-PAYMENT"];
87
- if (!h) return new Response(JSON.stringify({ x402Version: 1, accepts: [req] }), { status: 402 });
88
- const p = JSON.parse(unb64(h)).payload, a = p.authorization;
89
- const ok = await verifyTypedData({ address: a.from, signature: p.signature,
90
- domain: { name: "USD Coin", version: "2", chainId: 421614, verifyingContract: USDC },
91
- types: { TransferWithAuthorization: [{ name: "from", type: "address" }, { name: "to", type: "address" }, { name: "value", type: "uint256" },
92
- { name: "validAfter", type: "uint256" }, { name: "validBefore", type: "uint256" }, { name: "nonce", type: "bytes32" }] },
93
- primaryType: "TransferWithAuthorization",
94
- message: { ...a, value: BigInt(a.value), validAfter: BigInt(a.validAfter), validBefore: BigInt(a.validBefore) } });
95
- if (!ok || a.to !== SELLER || a.value !== "10000") return new Response(JSON.stringify({ error: "bad payment" }), { status: 402 });
96
- settled = a;
97
- return new Response('{"secret":42}', { status: 200, headers: { "X-PAYMENT-RESPONSE": b64(JSON.stringify({ success: true, transaction: "0xabc" })) } });
138
+ // ── self-check: `node src/agent/x402.js` — a stand-in app, relay and key server; no chain ──
139
+ if (import.meta.url === `file://${globalThis.process?.argv?.[1]}` && process.argv[1].endsWith("/x402.js")) {
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" })) } });
174
+ }
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}`);
98
179
  };
99
- const r = await payAndFetch("https://x/paid/1", { account: buyer, maxPrice: 10000n, fetchImpl: seller });
100
- if (r.body !== '{"secret":42}' || r.receipt.transaction !== "0xabc" || settled.from !== buyer.address) throw new Error(`paid fetch: ${JSON.stringify(r)}`);
101
- let refused = false;
102
- try { await payAndFetch("https://x/paid/1", { account: buyer, maxPrice: 9999n, fetchImpl: seller }); } catch (e) { refused = /exceeds the cap/.test(e.message); }
103
- if (!refused) throw new Error("a price over the cap must be refused before signing");
104
- 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");
105
214
  }
package/src/core/embed.js CHANGED
@@ -1,55 +1,58 @@
1
- // In-browser query embedder. Lifted from embeddings/examples/src/lib/embed.ts —
2
- // the query vector MUST come out of the same model and post-processing as the
1
+ // The network's one embedding model, on both sides of every search. The query
2
+ // vector MUST come out of the same model, prompt and post-processing as the
3
3
  // document vectors in the shard, or cosine is meaningless.
4
4
  //
5
- // Document side (quickbeam/embeddings.py): fastembed nomic-embed-text-v1.5 on
6
- // "search_document: " + text, then matryoshka(vec, 256).
7
- // Query side (here): same model on "search_query: " + text (nomic is asymmetric),
8
- // then the identical matryoshka.
5
+ // EmbeddingGemma-300m, asymmetric by prompt (QUERY / DOCUMENT below), truncated
6
+ // to 256 dims (it is Matryoshka-trained) and L2-normalized. Two runtimes run it:
7
+ // - a Fangorn app's Worker (Workers AI `@cf/google/embeddinggemma-300m`), the
8
+ // free tier's indexer (site/worker.js), on the document side;
9
+ // - transformers.js here, in a reader's tab or a Node publisher, on either side.
10
+ // They agree closely but not bit for bit, so a view names its `embedder` and is only
11
+ // ever appended to by the runtime that made it (view.js).
9
12
  //
10
- // If the embedding pipeline switches model or dim, change MODEL/DIM here to match.
11
- // transformers.js is dynamically imported so its ~MB runtime stays out of the main
13
+ // transformers.js is dynamically imported so its runtime stays out of the main
12
14
  // bundle and a load failure degrades to lexical search instead of breaking the app.
13
15
 
14
- const MODEL = "nomic-ai/nomic-embed-text-v1.5";
16
+ const MODEL = "google/embeddinggemma-300m";
17
+ const ONNX = "onnx-community/embeddinggemma-300m-ONNX";
15
18
  const DIM = 256;
16
- // The document side's precision. q8 everywhere by default (the browser's queries are q8).
17
- // WESTMARCH_EMBED_DTYPE=fp16 is a different encoder (~0.96 cosine to q8): a view records it
18
- // and refuses to mix the two (view.js), so switching an app is one --rebake.
19
+ /** The model card's retrieval prompts. Exported so every runtime uses the same text. */
20
+ export const QUERY = "task: search result | query: ";
21
+ export const DOCUMENT = "title: none | text: ";
22
+ // The document side's precision in transformers.js. q8 by default (the browser's queries are q8).
23
+ // EmbeddingGemma's activations do not survive fp16; WESTMARCH_EMBED_DTYPE=fp32 is the slow exact one.
19
24
  const DTYPE = globalThis.process?.env?.WESTMARCH_EMBED_DTYPE ?? "q8";
20
25
  export const EMBED_DTYPE = DTYPE;
21
26
 
22
- // Stamped onto every vector a publisher commits. Vectors from different models
23
- // (or dims) are not comparable — cosine between them is noise, and silently so.
24
- // The shard bake drops any vector whose stamp doesn't match rather than blending
25
- // corpora, which is the one thing that can quietly ruin a shared index.
27
+ // Stamped onto every view. Vectors from different models (or dims) are not
28
+ // comparable — cosine between them is noise, and silently so.
26
29
  export const EMBED_MODEL = MODEL;
27
30
  export const EMBED_DIM = DIM;
31
+ /** A short name for the space a taste or a reaction lives in. */
32
+ export const SPACE = `embeddinggemma-${DIM}`;
28
33
 
29
- let _extractor = null;
30
- const extractor = () => (_extractor ??= (async () => {
31
- const { pipeline } = await import("@huggingface/transformers");
32
- // WESTMARCH_EMBED_DEVICE=cuda puts the publisher's document side on a GPU (Node only; needs
33
- // onnxruntime-node's CUDA binaries and the matching CUDA runtime, cuBLAS, cuRAND and cuDNN on
34
- // LD_LIBRARY_PATH). q8's integer ops do not run on CUDA, so q8 there is slower than on CPU:
35
- // pair it with WESTMARCH_EMBED_DTYPE=fp16, which is ~4x faster than CPU q8, batched.
34
+ let _model = null, _progress = null;
35
+ /** Hear the model download: transformers.js's own events ({status, file, loaded, total}), on
36
+ * the thread that loads it (the page's own worker, when it calls the *Direct functions). */
37
+ export const onModelProgress = (f) => { _progress = f; };
38
+ const model = () => (_model ??= (async () => {
39
+ const { AutoModel, AutoTokenizer } = await import("@huggingface/transformers");
40
+ // WESTMARCH_EMBED_DEVICE=cuda puts a Node publisher's document side on a GPU.
36
41
  const device = globalThis.process?.env?.WESTMARCH_EMBED_DEVICE;
37
- return pipeline("feature-extraction", MODEL, { dtype: DTYPE, ...(device && { device }) });
42
+ const progress_callback = (p) => _progress?.(p);
43
+ const [tokenizer, m] = await Promise.all([AutoTokenizer.from_pretrained(ONNX, { progress_callback }),
44
+ AutoModel.from_pretrained(ONNX, { dtype: DTYPE, progress_callback, ...(device && { device }) })]);
45
+ // `sentence_embedding`, not mean pooling: the model's dense head runs after the pool.
46
+ return async (texts) => {
47
+ const { sentence_embedding: e } = await m(await tokenizer(texts, { padding: true, truncation: true }));
48
+ const d = e.dims.at(-1);
49
+ return texts.map((_, j) => matryoshka(e.data.slice(j * d, (j + 1) * d)));
50
+ };
38
51
  })());
39
52
 
40
- /** Standardize over the full vector, slice to `dim`, L2-normalize the slice. */
41
- function matryoshka(vec, dim = DIM) {
42
- const n = vec.length;
43
- let mean = 0;
44
- for (let i = 0; i < n; i++) mean += vec[i];
45
- mean /= n;
46
- let varsum = 0;
47
- for (let i = 0; i < n; i++) varsum += (vec[i] - mean) ** 2;
48
- const std = Math.sqrt(varsum / n + 1e-5);
49
-
50
- const out = new Array(Math.min(dim, n));
51
- for (let i = 0; i < out.length; i++) out[i] = (vec[i] - mean) / std;
52
-
53
+ /** Truncate to `dim` and L2-normalize: EmbeddingGemma's Matryoshka truncation. */
54
+ export function matryoshka(vec, dim = DIM) {
55
+ const out = Array.from(vec.slice(0, dim), Number);
53
56
  let norm = 0;
54
57
  for (const x of out) norm += x * x;
55
58
  norm = Math.sqrt(norm);
@@ -60,9 +63,8 @@ function matryoshka(vec, dim = DIM) {
60
63
  /** Free text → 256-d L2-normalized vector. Throws if the model can't load —
61
64
  * callers fall back to lexical. First call downloads the model (slow). */
62
65
  export async function embedQueryDirect(text) {
63
- const ex = await extractor();
64
- const output = await ex(`search_query: ${text}`, { pooling: "mean", normalize: false });
65
- return matryoshka(output.data);
66
+ const [v] = await (await model())([QUERY + text]);
67
+ return v;
66
68
  }
67
69
 
68
70
  /** One document, in-process. Calls made together are embedded as one batch (a GPU's
@@ -79,20 +81,22 @@ async function flushDocuments() {
79
81
  const all = pending;
80
82
  pending = [];
81
83
  try {
82
- const ex = await extractor();
83
- for (let i = 0; i < all.length; i += BATCH) {
84
- const part = all.slice(i, i + BATCH);
85
- const output = await ex(part.map((p) => `search_document: ${p.text}`), { pooling: "mean", normalize: false });
86
- const d = output.dims.at(-1);
87
- part.forEach((p, j) => p.resolve(matryoshka(output.data.slice(j * d, (j + 1) * d))));
88
- }
84
+ const vs = await embedRaw(all.map((p) => DOCUMENT + p.text));
85
+ all.forEach((p, j) => p.resolve(vs[j]));
89
86
  } catch (e) {
90
87
  for (const p of all) p.reject(e);
91
88
  }
92
89
  }
93
90
 
91
+ /** Texts that already carry their prompt → vectors, in-process: a stand-in for the Worker's AI binding (devnet.js). */
92
+ export async function embedRaw(texts) {
93
+ const run = await model(), out = [];
94
+ for (let i = 0; i < texts.length; i += BATCH) out.push(...await run(texts.slice(i, i + BATCH)));
95
+ return out;
96
+ }
97
+
94
98
  /** Load the model in-process, and settle when it is actually ready. */
95
- export const warmDirect = () => extractor().then(() => {});
99
+ export const warmDirect = () => model().then(() => {});
96
100
 
97
101
  // ── off the main thread ──────────────────────────────────────────────────────
98
102
  //
@@ -136,9 +140,8 @@ const ask = (kind, text) => new Promise((resolve, reject) => {
136
140
  export const embedQuery = (text) => (offThread() ? ask("query", text) : embedQueryDirect(text));
137
141
 
138
142
  /**
139
- * Document side. Same model, the OTHER nomic prefix — it's an asymmetric model,
140
- * so documents embedded with "search_query: " would rank against real queries
141
- * about as well as random.
143
+ * Document side. Same model, the OTHER prompt — it's asymmetric, so documents
144
+ * embedded with the query prompt rank against real queries worse.
142
145
  *
143
146
  * Runs one at a time on purpose: this executes in a publisher's browser during
144
147
  * publish, and a batch large enough to matter is a batch large enough to lock
@@ -235,7 +238,7 @@ export function unpackBits(b64) {
235
238
  * focus — the pipeline is a memoized singleton on whichever thread owns it.
236
239
  *
237
240
  * Call this on INTENT (the search box taking focus), never on page load: it is a
238
- * 131MB download, and until someone types there is nothing for it to embed. The
241
+ * ~300MB download, and until someone types there is nothing for it to embed. The
239
242
  * whole first-paint path — browse, shelves, channels, the field, the compass —
240
243
  * runs on the document vectors already in the shard.
241
244
  */
@@ -245,7 +248,7 @@ export const warmEmbedder = () => {
245
248
  };
246
249
 
247
250
  // ── self-check: `node src/llm/embed.js` — quantization only, no model download ────
248
- if (typeof process !== "undefined" && import.meta.url === `file://${process.argv[1]}` && process.argv[1].endsWith("/embed.js")) {
251
+ if (import.meta.url === `file://${globalThis.process?.argv?.[1]}` && process.argv[1].endsWith("/embed.js")) {
249
252
  const v = matryoshka(Array.from({ length: 768 }, (_, i) => Math.sin(i)));
250
253
  let norm = 0;
251
254
  for (const x of v) norm += x * x;