@holoscript/framework 6.0.3

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 (329) hide show
  1. package/ALL-test-results.json +1 -0
  2. package/CHANGELOG.md +8 -0
  3. package/LICENSE +21 -0
  4. package/ROADMAP.md +175 -0
  5. package/dist/AgentManifest-CB4xM-Ma.d.cts +704 -0
  6. package/dist/AgentManifest-CB4xM-Ma.d.ts +704 -0
  7. package/dist/BehaviorTree-BrBFECv5.d.cts +103 -0
  8. package/dist/BehaviorTree-BrBFECv5.d.ts +103 -0
  9. package/dist/InvisibleWallet-BB6tFvRA.d.cts +1732 -0
  10. package/dist/InvisibleWallet-rtRrBOA8.d.ts +1732 -0
  11. package/dist/OrchestratorAgent-BvWgf9uw.d.cts +798 -0
  12. package/dist/OrchestratorAgent-Q_CbVTmO.d.ts +798 -0
  13. package/dist/agents/index.cjs +4790 -0
  14. package/dist/agents/index.d.cts +1788 -0
  15. package/dist/agents/index.d.ts +1788 -0
  16. package/dist/agents/index.js +4695 -0
  17. package/dist/ai/index.cjs +5347 -0
  18. package/dist/ai/index.d.cts +1753 -0
  19. package/dist/ai/index.d.ts +1753 -0
  20. package/dist/ai/index.js +5244 -0
  21. package/dist/behavior.cjs +449 -0
  22. package/dist/behavior.d.cts +130 -0
  23. package/dist/behavior.d.ts +130 -0
  24. package/dist/behavior.js +407 -0
  25. package/dist/economy/index.cjs +3659 -0
  26. package/dist/economy/index.d.cts +747 -0
  27. package/dist/economy/index.d.ts +747 -0
  28. package/dist/economy/index.js +3617 -0
  29. package/dist/implementations-D9T3un9D.d.cts +236 -0
  30. package/dist/implementations-D9T3un9D.d.ts +236 -0
  31. package/dist/index.cjs +24550 -0
  32. package/dist/index.d.cts +1729 -0
  33. package/dist/index.d.ts +1729 -0
  34. package/dist/index.js +24277 -0
  35. package/dist/learning/index.cjs +219 -0
  36. package/dist/learning/index.d.cts +104 -0
  37. package/dist/learning/index.d.ts +104 -0
  38. package/dist/learning/index.js +189 -0
  39. package/dist/negotiation/index.cjs +970 -0
  40. package/dist/negotiation/index.d.cts +610 -0
  41. package/dist/negotiation/index.d.ts +610 -0
  42. package/dist/negotiation/index.js +931 -0
  43. package/dist/skills/index.cjs +1118 -0
  44. package/dist/skills/index.d.cts +289 -0
  45. package/dist/skills/index.d.ts +289 -0
  46. package/dist/skills/index.js +1079 -0
  47. package/dist/swarm/index.cjs +5268 -0
  48. package/dist/swarm/index.d.cts +2433 -0
  49. package/dist/swarm/index.d.ts +2433 -0
  50. package/dist/swarm/index.js +5221 -0
  51. package/dist/training/index.cjs +2745 -0
  52. package/dist/training/index.d.cts +1734 -0
  53. package/dist/training/index.d.ts +1734 -0
  54. package/dist/training/index.js +2687 -0
  55. package/extract-failures.js +10 -0
  56. package/package.json +82 -0
  57. package/src/__tests__/bounty-marketplace.test.ts +374 -0
  58. package/src/__tests__/delegation.test.ts +144 -0
  59. package/src/__tests__/distributed-claimer.test.ts +147 -0
  60. package/src/__tests__/done-log-audit.test.ts +342 -0
  61. package/src/__tests__/framework.test.ts +865 -0
  62. package/src/__tests__/goal-synthesizer.test.ts +236 -0
  63. package/src/__tests__/presence.test.ts +223 -0
  64. package/src/__tests__/protocol-agent.test.ts +254 -0
  65. package/src/__tests__/revenue-splitter.test.ts +114 -0
  66. package/src/__tests__/scenario-driven-todo.test.ts +197 -0
  67. package/src/__tests__/self-improve.test.ts +349 -0
  68. package/src/__tests__/service-lifecycle.test.ts +237 -0
  69. package/src/__tests__/skill-router.test.ts +121 -0
  70. package/src/agents/AgentManifest.ts +493 -0
  71. package/src/agents/AgentRegistry.ts +475 -0
  72. package/src/agents/AgentTypes.ts +585 -0
  73. package/src/agents/AgentWalletRegistry.ts +83 -0
  74. package/src/agents/AuthenticatedCRDT.ts +388 -0
  75. package/src/agents/CapabilityMatcher.ts +453 -0
  76. package/src/agents/CrossRealityHandoff.ts +305 -0
  77. package/src/agents/CulturalMemory.ts +454 -0
  78. package/src/agents/FederatedRegistryAdapter.ts +429 -0
  79. package/src/agents/NormEngine.ts +450 -0
  80. package/src/agents/OrchestratorAgent.ts +414 -0
  81. package/src/agents/SkillWorkflowEngine.ts +472 -0
  82. package/src/agents/TaskDelegationService.ts +551 -0
  83. package/src/agents/__tests__/AgentManifest.prod.test.ts +134 -0
  84. package/src/agents/__tests__/AgentManifest.test.ts +182 -0
  85. package/src/agents/__tests__/AgentModule.test.ts +864 -0
  86. package/src/agents/__tests__/AgentRegistry.prod.test.ts +125 -0
  87. package/src/agents/__tests__/AgentRegistry.test.ts +148 -0
  88. package/src/agents/__tests__/AgentTypes.test.ts +534 -0
  89. package/src/agents/__tests__/AgentWalletRegistry.test.ts +152 -0
  90. package/src/agents/__tests__/AuthenticatedCRDT.test.ts +558 -0
  91. package/src/agents/__tests__/CapabilityMatcher.prod.test.ts +117 -0
  92. package/src/agents/__tests__/CapabilityMatcher.test.ts +178 -0
  93. package/src/agents/__tests__/CrossRealityHandoff.test.ts +402 -0
  94. package/src/agents/__tests__/CulturalMemory.test.ts +200 -0
  95. package/src/agents/__tests__/FederatedRegistryAdapter.test.ts +409 -0
  96. package/src/agents/__tests__/NormEngine.test.ts +276 -0
  97. package/src/agents/__tests__/OrchestratorAgent.test.ts +182 -0
  98. package/src/agents/__tests__/SkillWorkflowEngine.test.ts +357 -0
  99. package/src/agents/__tests__/TaskDelegationService.test.ts +446 -0
  100. package/src/agents/index.ts +107 -0
  101. package/src/agents/spatial-comms/Layer1RealTime.ts +621 -0
  102. package/src/agents/spatial-comms/Layer2A2A.ts +661 -0
  103. package/src/agents/spatial-comms/Layer3MCP.ts +651 -0
  104. package/src/agents/spatial-comms/ProtocolTypes.ts +543 -0
  105. package/src/agents/spatial-comms/SpatialCommClient.ts +483 -0
  106. package/src/agents/spatial-comms/__tests__/performance-benchmark.test.ts +465 -0
  107. package/src/agents/spatial-comms/examples/multi-agent-world-creation.ts +409 -0
  108. package/src/agents/spatial-comms/index.ts +66 -0
  109. package/src/ai/AIAdapter.ts +313 -0
  110. package/src/ai/AICopilot.ts +331 -0
  111. package/src/ai/AIOutputValidator.ts +203 -0
  112. package/src/ai/BTNodes.ts +239 -0
  113. package/src/ai/BehaviorSelector.ts +135 -0
  114. package/src/ai/BehaviorTree.ts +153 -0
  115. package/src/ai/Blackboard.ts +165 -0
  116. package/src/ai/GenerationAnalytics.ts +461 -0
  117. package/src/ai/GenerationCache.ts +265 -0
  118. package/src/ai/GoalPlanner.ts +165 -0
  119. package/src/ai/HoloScriptGenerator.ts +580 -0
  120. package/src/ai/InfluenceMap.ts +180 -0
  121. package/src/ai/NavMesh.ts +168 -0
  122. package/src/ai/PerceptionSystem.ts +178 -0
  123. package/src/ai/PromptTemplates.ts +453 -0
  124. package/src/ai/SemanticSearchService.ts +80 -0
  125. package/src/ai/StateMachine.ts +196 -0
  126. package/src/ai/SteeringBehavior.ts +150 -0
  127. package/src/ai/SteeringBehaviors.ts +244 -0
  128. package/src/ai/TrainingDataGenerator.ts +1082 -0
  129. package/src/ai/UtilityAI.ts +145 -0
  130. package/src/ai/__tests__/AIAdapter.prod.test.ts +259 -0
  131. package/src/ai/__tests__/AIAdapter.test.ts +109 -0
  132. package/src/ai/__tests__/AICopilot.prod.test.ts +341 -0
  133. package/src/ai/__tests__/AICopilot.test.ts +178 -0
  134. package/src/ai/__tests__/AIOutputValidator.prod.test.ts +226 -0
  135. package/src/ai/__tests__/AIOutputValidator.test.ts +138 -0
  136. package/src/ai/__tests__/BTNodes.prod.test.ts +391 -0
  137. package/src/ai/__tests__/BTNodes.test.ts +263 -0
  138. package/src/ai/__tests__/BehaviorSelector.prod.test.ts +129 -0
  139. package/src/ai/__tests__/BehaviorSelector.test.ts +132 -0
  140. package/src/ai/__tests__/BehaviorTree.prod.test.ts +266 -0
  141. package/src/ai/__tests__/BehaviorTree.test.ts +216 -0
  142. package/src/ai/__tests__/Blackboard.prod.test.ts +339 -0
  143. package/src/ai/__tests__/Blackboard.test.ts +183 -0
  144. package/src/ai/__tests__/GenerationAnalytics.prod.test.ts +141 -0
  145. package/src/ai/__tests__/GenerationAnalytics.test.ts +165 -0
  146. package/src/ai/__tests__/GenerationCache.prod.test.ts +144 -0
  147. package/src/ai/__tests__/GenerationCache.test.ts +171 -0
  148. package/src/ai/__tests__/GoalPlanner.prod.test.ts +189 -0
  149. package/src/ai/__tests__/GoalPlanner.test.ts +137 -0
  150. package/src/ai/__tests__/GoalPlannerDepth.prod.test.ts +217 -0
  151. package/src/ai/__tests__/HoloScriptGenerator.test.ts +125 -0
  152. package/src/ai/__tests__/InfluenceMap.prod.test.ts +146 -0
  153. package/src/ai/__tests__/InfluenceMap.test.ts +149 -0
  154. package/src/ai/__tests__/NavMesh.prod.test.ts +141 -0
  155. package/src/ai/__tests__/NavMesh.test.ts +159 -0
  156. package/src/ai/__tests__/PerceptionSystem.prod.test.ts +135 -0
  157. package/src/ai/__tests__/PerceptionSystem.test.ts +250 -0
  158. package/src/ai/__tests__/PromptTemplates.prod.test.ts +313 -0
  159. package/src/ai/__tests__/PromptTemplates.test.ts +146 -0
  160. package/src/ai/__tests__/SemanticSearch.test.ts +37 -0
  161. package/src/ai/__tests__/StateMachine.prod.test.ts +162 -0
  162. package/src/ai/__tests__/StateMachine.test.ts +163 -0
  163. package/src/ai/__tests__/SteeringBehavior.prod.test.ts +251 -0
  164. package/src/ai/__tests__/SteeringBehavior.test.ts +135 -0
  165. package/src/ai/__tests__/SteeringBehaviors.prod.test.ts +133 -0
  166. package/src/ai/__tests__/SteeringBehaviors.test.ts +151 -0
  167. package/src/ai/__tests__/TrainingDataGenerator.prod.test.ts +286 -0
  168. package/src/ai/__tests__/TrainingDataGenerator.test.ts +286 -0
  169. package/src/ai/__tests__/UtilityAI.prod.test.ts +207 -0
  170. package/src/ai/__tests__/UtilityAI.test.ts +155 -0
  171. package/src/ai/__tests__/adapters.prod.test.ts +263 -0
  172. package/src/ai/__tests__/adapters.test.ts +320 -0
  173. package/src/ai/adapters.ts +1585 -0
  174. package/src/ai/index.ts +130 -0
  175. package/src/behavior/BehaviorPresets.ts +140 -0
  176. package/src/behavior/BehaviorTree.ts +236 -0
  177. package/src/behavior/StateMachine.ts +176 -0
  178. package/src/behavior/StateTrait.ts +67 -0
  179. package/src/behavior/index.ts +8 -0
  180. package/src/behavior.ts +8 -0
  181. package/src/board/audit.ts +284 -0
  182. package/src/board/board-ops.ts +336 -0
  183. package/src/board/board-types.ts +302 -0
  184. package/src/board/index.ts +69 -0
  185. package/src/define-agent.ts +46 -0
  186. package/src/define-team.ts +33 -0
  187. package/src/delegation.ts +265 -0
  188. package/src/distributed-claimer.ts +228 -0
  189. package/src/economy/AgentBudgetEnforcer.ts +464 -0
  190. package/src/economy/BountyManager.ts +185 -0
  191. package/src/economy/CreatorRevenueAggregator.ts +460 -0
  192. package/src/economy/InvisibleWallet.ts +82 -0
  193. package/src/economy/KnowledgeMarketplace.ts +193 -0
  194. package/src/economy/PaymentWebhookService.ts +512 -0
  195. package/src/economy/RevenueSplitter.ts +156 -0
  196. package/src/economy/SubscriptionManager.ts +546 -0
  197. package/src/economy/UnifiedBudgetOptimizer.ts +635 -0
  198. package/src/economy/UsageMeter.ts +440 -0
  199. package/src/economy/_core-stubs.ts +219 -0
  200. package/src/economy/index.ts +100 -0
  201. package/src/economy/x402-facilitator.ts +1978 -0
  202. package/src/index.ts +348 -0
  203. package/src/knowledge/__tests__/knowledge-consolidator.test.ts +444 -0
  204. package/src/knowledge/__tests__/knowledge-store-vector.test.ts +291 -0
  205. package/src/knowledge/brain.ts +167 -0
  206. package/src/knowledge/consolidation.ts +581 -0
  207. package/src/knowledge/knowledge-consolidator.ts +510 -0
  208. package/src/knowledge/knowledge-store.ts +616 -0
  209. package/src/learning/MemoryConsolidator.ts +102 -0
  210. package/src/learning/MemoryScorer.ts +69 -0
  211. package/src/learning/ProceduralCompiler.ts +45 -0
  212. package/src/learning/SemanticClusterer.ts +66 -0
  213. package/src/learning/index.ts +8 -0
  214. package/src/llm/llm-adapter.ts +159 -0
  215. package/src/mesh/index.ts +309 -0
  216. package/src/negotiation/NegotiationProtocol.ts +694 -0
  217. package/src/negotiation/NegotiationTypes.ts +473 -0
  218. package/src/negotiation/VotingMechanisms.ts +691 -0
  219. package/src/negotiation/index.ts +49 -0
  220. package/src/protocol/goal-synthesizer.ts +317 -0
  221. package/src/protocol/implementations.ts +474 -0
  222. package/src/protocol/micro-phase-decomposer.ts +299 -0
  223. package/src/protocol/micro-step-decomposer.test.ts +306 -0
  224. package/src/protocol-agent.test.ts +353 -0
  225. package/src/protocol-agent.ts +670 -0
  226. package/src/self-improve/absorb-scanner.ts +252 -0
  227. package/src/self-improve/evolution-engine.ts +149 -0
  228. package/src/self-improve/framework-absorber.ts +214 -0
  229. package/src/self-improve/index.ts +50 -0
  230. package/src/self-improve/prompt-optimizer.ts +212 -0
  231. package/src/self-improve/test-generator.ts +175 -0
  232. package/src/skill-router.ts +186 -0
  233. package/src/skills/index.ts +5 -0
  234. package/src/skills/skill-md-bridge.ts +1699 -0
  235. package/src/swarm/ACOEngine.ts +261 -0
  236. package/src/swarm/CollectiveIntelligence.ts +383 -0
  237. package/src/swarm/ContributionSynthesizer.ts +481 -0
  238. package/src/swarm/LeaderElection.ts +393 -0
  239. package/src/swarm/PSOEngine.ts +206 -0
  240. package/src/swarm/QuorumPolicy.ts +173 -0
  241. package/src/swarm/SwarmCoordinator.ts +335 -0
  242. package/src/swarm/SwarmManager.ts +442 -0
  243. package/src/swarm/SwarmMembership.ts +456 -0
  244. package/src/swarm/VotingRound.ts +255 -0
  245. package/src/swarm/__tests__/ACOEngine.prod.test.ts +164 -0
  246. package/src/swarm/__tests__/ACOEngine.test.ts +117 -0
  247. package/src/swarm/__tests__/CollectiveIntelligence.prod.test.ts +296 -0
  248. package/src/swarm/__tests__/CollectiveIntelligence.test.ts +457 -0
  249. package/src/swarm/__tests__/ContributionSynthesizer.prod.test.ts +269 -0
  250. package/src/swarm/__tests__/ContributionSynthesizer.test.ts +254 -0
  251. package/src/swarm/__tests__/LeaderElection.prod.test.ts +196 -0
  252. package/src/swarm/__tests__/LeaderElection.test.ts +151 -0
  253. package/src/swarm/__tests__/PSOEngine.prod.test.ts +162 -0
  254. package/src/swarm/__tests__/PSOEngine.test.ts +106 -0
  255. package/src/swarm/__tests__/QuorumPolicy.prod.test.ts +216 -0
  256. package/src/swarm/__tests__/QuorumPolicy.test.ts +177 -0
  257. package/src/swarm/__tests__/SwarmCoordinator.prod.test.ts +186 -0
  258. package/src/swarm/__tests__/SwarmCoordinator.test.ts +167 -0
  259. package/src/swarm/__tests__/SwarmManager.prod.test.ts +308 -0
  260. package/src/swarm/__tests__/SwarmManager.test.ts +373 -0
  261. package/src/swarm/__tests__/SwarmMembership.prod.test.ts +273 -0
  262. package/src/swarm/__tests__/SwarmMembership.test.ts +264 -0
  263. package/src/swarm/__tests__/VotingRound.prod.test.ts +233 -0
  264. package/src/swarm/__tests__/VotingRound.test.ts +174 -0
  265. package/src/swarm/analytics/SwarmInspector.ts +476 -0
  266. package/src/swarm/analytics/SwarmMetrics.ts +449 -0
  267. package/src/swarm/analytics/__tests__/SwarmInspector.prod.test.ts +366 -0
  268. package/src/swarm/analytics/__tests__/SwarmInspector.test.ts +454 -0
  269. package/src/swarm/analytics/__tests__/SwarmMetrics.prod.test.ts +254 -0
  270. package/src/swarm/analytics/__tests__/SwarmMetrics.test.ts +370 -0
  271. package/src/swarm/analytics/index.ts +7 -0
  272. package/src/swarm/index.ts +69 -0
  273. package/src/swarm/messaging/BroadcastChannel.ts +509 -0
  274. package/src/swarm/messaging/GossipProtocol.ts +565 -0
  275. package/src/swarm/messaging/SwarmEventBus.ts +443 -0
  276. package/src/swarm/messaging/__tests__/BroadcastChannel.prod.test.ts +331 -0
  277. package/src/swarm/messaging/__tests__/BroadcastChannel.test.ts +333 -0
  278. package/src/swarm/messaging/__tests__/GossipProtocol.prod.test.ts +356 -0
  279. package/src/swarm/messaging/__tests__/GossipProtocol.test.ts +437 -0
  280. package/src/swarm/messaging/__tests__/SwarmEventBus.prod.test.ts +191 -0
  281. package/src/swarm/messaging/__tests__/SwarmEventBus.test.ts +247 -0
  282. package/src/swarm/messaging/index.ts +8 -0
  283. package/src/swarm/spatial/FlockingBehavior.ts +462 -0
  284. package/src/swarm/spatial/FormationController.ts +500 -0
  285. package/src/swarm/spatial/Vector3.ts +170 -0
  286. package/src/swarm/spatial/ZoneClaiming.ts +509 -0
  287. package/src/swarm/spatial/__tests__/FlockingBehavior.prod.test.ts +239 -0
  288. package/src/swarm/spatial/__tests__/FlockingBehavior.test.ts +298 -0
  289. package/src/swarm/spatial/__tests__/FormationController.prod.test.ts +240 -0
  290. package/src/swarm/spatial/__tests__/FormationController.test.ts +297 -0
  291. package/src/swarm/spatial/__tests__/Vector3.prod.test.ts +283 -0
  292. package/src/swarm/spatial/__tests__/Vector3.test.ts +224 -0
  293. package/src/swarm/spatial/__tests__/ZoneClaiming.prod.test.ts +246 -0
  294. package/src/swarm/spatial/__tests__/ZoneClaiming.test.ts +374 -0
  295. package/src/swarm/spatial/index.ts +28 -0
  296. package/src/team.ts +1245 -0
  297. package/src/training/LRScheduler.ts +377 -0
  298. package/src/training/QualityScoringPipeline.ts +139 -0
  299. package/src/training/SoftDedup.ts +461 -0
  300. package/src/training/SparsityMonitor.ts +685 -0
  301. package/src/training/SparsityMonitorTypes.ts +209 -0
  302. package/src/training/SpatialTrainingDataGenerator.ts +1526 -0
  303. package/src/training/SpatialTrainingDataTypes.ts +216 -0
  304. package/src/training/TrainingPipelineConfig.ts +215 -0
  305. package/src/training/constants.ts +94 -0
  306. package/src/training/index.ts +138 -0
  307. package/src/training/schema.ts +147 -0
  308. package/src/training/scripts/generate-novel-use-cases-dataset.ts +272 -0
  309. package/src/training/scripts/generate-spatial-dataset.ts +521 -0
  310. package/src/training/training/data/novel-use-cases.jsonl +153 -0
  311. package/src/training/training/data/spatial-reasoning-10k.jsonl +9354 -0
  312. package/src/training/trainingmonkey/TrainingMonkeyIntegration.ts +477 -0
  313. package/src/training/trainingmonkey/TrainingMonkeyTypes.ts +230 -0
  314. package/src/training/trainingmonkey/index.ts +26 -0
  315. package/src/training/trait-mappings.ts +157 -0
  316. package/src/types/core-stubs.d.ts +113 -0
  317. package/src/types.ts +304 -0
  318. package/test-output.txt +0 -0
  319. package/test-result.json +1 -0
  320. package/tsc-errors.txt +4 -0
  321. package/tsc_output.txt +0 -0
  322. package/tsconfig.json +14 -0
  323. package/tsup-learning-esm.config.ts +12 -0
  324. package/tsup.config.ts +21 -0
  325. package/typescript-errors-2.txt +0 -0
  326. package/typescript-errors.txt +22 -0
  327. package/vitest-log-utf8.txt +268 -0
  328. package/vitest-log.txt +0 -0
  329. package/vitest.config.ts +8 -0
@@ -0,0 +1,1978 @@
1
+ /**
2
+ * x402 Payment Protocol Facilitator
3
+ *
4
+ * Implements the x402 protocol (https://www.x402.org/) for HTTP 402 Payment Required
5
+ * responses, enabling native internet payments for HoloScript resources.
6
+ *
7
+ * The x402 protocol flow:
8
+ * 1. Client requests a resource (GET /api/premium-scene)
9
+ * 2. Server responds with HTTP 402 + PaymentRequired body
10
+ * 3. Client signs an EIP-712 authorization (gasless) and retries with X-PAYMENT header
11
+ * 4. Facilitator verifies signature + settles on-chain
12
+ * 5. Server returns the resource with X-PAYMENT-RESPONSE confirmation
13
+ *
14
+ * Dual-mode settlement:
15
+ * - In-memory ledger for microtransactions < $0.10 (no gas, instant)
16
+ * - On-chain x402 settlement for amounts >= $0.10 (USDC on Base or Solana)
17
+ *
18
+ * Optimistic execution:
19
+ * - Proceeds on valid authorization before on-chain confirmation
20
+ * - Verifies settlement asynchronously, reverts on failure
21
+ *
22
+ * @version 1.0.0
23
+ * @see https://www.x402.org/
24
+ * @see https://docs.x402.org/
25
+ */
26
+
27
+ // Import real trait types from @holoscript/core
28
+ import type { HSPlusNode, TraitHandler, TraitContext, TraitEvent } from '@holoscript/core';
29
+
30
+ // =============================================================================
31
+ // x402 PROTOCOL TYPES
32
+ // =============================================================================
33
+
34
+ /** x402 protocol version */
35
+ export const X402_VERSION = 1;
36
+
37
+ /** Supported settlement chains */
38
+ export type SettlementChain = 'base' | 'base-sepolia' | 'solana' | 'solana-devnet';
39
+
40
+ /** Supported payment schemes per x402 spec */
41
+ export type PaymentScheme = 'exact';
42
+
43
+ /** Settlement mode based on transaction amount */
44
+ export type SettlementMode = 'in_memory' | 'on_chain';
45
+
46
+ /** USDC contract addresses per chain */
47
+ export const USDC_CONTRACTS: Record<SettlementChain, string> = {
48
+ base: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
49
+ 'base-sepolia': '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
50
+ solana: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
51
+ 'solana-devnet': '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU',
52
+ };
53
+
54
+ /** Threshold for switching from in-memory to on-chain settlement (in USDC base units, 6 decimals) */
55
+ export const MICRO_PAYMENT_THRESHOLD = 100_000; // 0.10 USDC (6 decimals)
56
+
57
+ /**
58
+ * x402 PaymentRequired response body.
59
+ * Returned as HTTP 402 response when payment is needed.
60
+ */
61
+ export interface X402PaymentRequired {
62
+ /** Protocol version */
63
+ x402Version: number;
64
+ /** Accepted payment methods */
65
+ accepts: X402PaymentOption[];
66
+ /** Human-readable error description */
67
+ error: string;
68
+ }
69
+
70
+ /**
71
+ * A single payment option within the accepts array.
72
+ */
73
+ export interface X402PaymentOption {
74
+ /** Payment model: "exact" (fixed price) */
75
+ scheme: PaymentScheme;
76
+ /** Blockchain network identifier */
77
+ network: SettlementChain;
78
+ /** Amount in token base units (string for precision, USDC = 6 decimals) */
79
+ maxAmountRequired: string;
80
+ /** The resource being paid for */
81
+ resource: string;
82
+ /** Human-readable description */
83
+ description: string;
84
+ /** Recipient wallet address */
85
+ payTo: string;
86
+ /** Token contract address (USDC) */
87
+ asset: string;
88
+ /** Maximum seconds to complete payment */
89
+ maxTimeoutSeconds: number;
90
+ }
91
+
92
+ /**
93
+ * EIP-712 signed authorization payload from the client.
94
+ * Sent in the X-PAYMENT header (base64-encoded).
95
+ */
96
+ export interface X402PaymentPayload {
97
+ /** Protocol version */
98
+ x402Version: number;
99
+ /** Payment scheme */
100
+ scheme: PaymentScheme;
101
+ /** Target network */
102
+ network: SettlementChain;
103
+ /** Signed authorization */
104
+ payload: {
105
+ /** EIP-712 or Ed25519 signature */
106
+ signature: string;
107
+ /** Transfer authorization details */
108
+ authorization: {
109
+ /** Payer address */
110
+ from: string;
111
+ /** Recipient address */
112
+ to: string;
113
+ /** Amount in base units */
114
+ value: string;
115
+ /** Unix timestamp after which authorization is valid */
116
+ validAfter: string;
117
+ /** Unix timestamp before which authorization is valid */
118
+ validBefore: string;
119
+ /** Unique nonce to prevent replay */
120
+ nonce: string;
121
+ };
122
+ };
123
+ }
124
+
125
+ /**
126
+ * Settlement result after on-chain or in-memory settlement.
127
+ */
128
+ export interface X402SettlementResult {
129
+ /** Whether settlement succeeded */
130
+ success: boolean;
131
+ /** On-chain transaction hash (null for in-memory) */
132
+ transaction: string | null;
133
+ /** Network where settlement occurred */
134
+ network: SettlementChain | 'in_memory';
135
+ /** Payer address or ID */
136
+ payer: string;
137
+ /** Error reason if failed */
138
+ errorReason: string | null;
139
+ /** Settlement mode used */
140
+ mode: SettlementMode;
141
+ /** Timestamp of settlement */
142
+ settledAt: number;
143
+ }
144
+
145
+ /**
146
+ * Verification result from the facilitator.
147
+ */
148
+ export interface X402VerificationResult {
149
+ /** Whether the payment authorization is valid */
150
+ isValid: boolean;
151
+ /** Reason for invalidity */
152
+ invalidReason: string | null;
153
+ }
154
+
155
+ /**
156
+ * In-memory ledger entry for micro-payments.
157
+ */
158
+ export interface LedgerEntry {
159
+ /** Unique transaction ID */
160
+ id: string;
161
+ /** Payer identifier */
162
+ from: string;
163
+ /** Recipient identifier */
164
+ to: string;
165
+ /** Amount in USDC base units (6 decimals) */
166
+ amount: number;
167
+ /** Resource accessed */
168
+ resource: string;
169
+ /** Timestamp */
170
+ timestamp: number;
171
+ /** Whether this has been settled on-chain (batch settlement) */
172
+ settled: boolean;
173
+ /** On-chain tx hash after batch settlement */
174
+ settlementTx: string | null;
175
+ }
176
+
177
+ /**
178
+ * Configuration for the x402 facilitator.
179
+ */
180
+ export interface X402FacilitatorConfig {
181
+ /** Recipient wallet address for payments */
182
+ recipientAddress: string;
183
+ /** Primary settlement chain */
184
+ chain: SettlementChain;
185
+ /** Secondary chain (optional, for multi-chain support) */
186
+ secondaryChain?: SettlementChain;
187
+ /** Micro-payment threshold in USDC base units (default: 100000 = $0.10) */
188
+ microPaymentThreshold?: number;
189
+ /** Maximum timeout for payment completion in seconds */
190
+ maxTimeoutSeconds?: number;
191
+ /** Enable optimistic execution (proceed before on-chain confirmation) */
192
+ optimisticExecution?: boolean;
193
+ /** Batch settlement interval for in-memory ledger entries (ms) */
194
+ batchSettlementIntervalMs?: number;
195
+ /** Maximum in-memory ledger entries before forced batch settlement */
196
+ maxLedgerEntries?: number;
197
+ /** Facilitator service URL for on-chain verification/settlement */
198
+ facilitatorUrl?: string;
199
+ /** Resource description template */
200
+ resourceDescription?: string;
201
+ }
202
+
203
+ // =============================================================================
204
+ // IN-MEMORY MICRO-PAYMENT LEDGER
205
+ // =============================================================================
206
+
207
+ /**
208
+ * In-memory ledger for tracking micro-payments below the on-chain threshold.
209
+ * Entries accumulate and are batch-settled periodically.
210
+ *
211
+ * Thread-safe via synchronous JS execution model.
212
+ */
213
+ export class MicroPaymentLedger {
214
+ private entries: LedgerEntry[] = [];
215
+ private balances: Map<string, number> = new Map();
216
+ private txCounter = 0;
217
+ private readonly maxEntries: number;
218
+
219
+ constructor(maxEntries = 10_000) {
220
+ this.maxEntries = maxEntries;
221
+ }
222
+
223
+ /**
224
+ * Record a micro-payment in the in-memory ledger.
225
+ */
226
+ record(from: string, to: string, amount: number, resource: string): LedgerEntry {
227
+ const entry: LedgerEntry = {
228
+ id: `micro_${Date.now()}_${this.txCounter++}`,
229
+ from,
230
+ to,
231
+ amount,
232
+ resource,
233
+ timestamp: Date.now(),
234
+ settled: false,
235
+ settlementTx: null,
236
+ };
237
+
238
+ this.entries.push(entry);
239
+
240
+ // Update balances
241
+ const fromBalance = this.balances.get(from) ?? 0;
242
+ this.balances.set(from, fromBalance - amount);
243
+
244
+ const toBalance = this.balances.get(to) ?? 0;
245
+ this.balances.set(to, toBalance + amount);
246
+
247
+ // Trim if over limit
248
+ if (this.entries.length > this.maxEntries) {
249
+ this.entries = this.entries.slice(-this.maxEntries);
250
+ }
251
+
252
+ return entry;
253
+ }
254
+
255
+ /**
256
+ * Get unsettled entries for batch settlement.
257
+ */
258
+ getUnsettled(): LedgerEntry[] {
259
+ return this.entries.filter((e) => !e.settled);
260
+ }
261
+
262
+ /**
263
+ * Mark entries as settled after batch on-chain settlement.
264
+ */
265
+ markSettled(entryIds: string[], txHash: string): void {
266
+ for (const entry of this.entries) {
267
+ if (entryIds.includes(entry.id)) {
268
+ entry.settled = true;
269
+ entry.settlementTx = txHash;
270
+ }
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Get the net balance for an address (can be negative = owes).
276
+ */
277
+ getBalance(address: string): number {
278
+ return this.balances.get(address) ?? 0;
279
+ }
280
+
281
+ /**
282
+ * Get total unsettled volume.
283
+ */
284
+ getUnsettledVolume(): number {
285
+ return this.getUnsettled().reduce((sum, e) => sum + e.amount, 0);
286
+ }
287
+
288
+ /**
289
+ * Get all entries for a specific payer.
290
+ */
291
+ getEntriesForPayer(from: string): LedgerEntry[] {
292
+ return this.entries.filter((e) => e.from === from);
293
+ }
294
+
295
+ /**
296
+ * Get ledger statistics.
297
+ */
298
+ getStats(): {
299
+ totalEntries: number;
300
+ unsettledEntries: number;
301
+ unsettledVolume: number;
302
+ uniquePayers: number;
303
+ uniqueRecipients: number;
304
+ } {
305
+ const unsettled = this.getUnsettled();
306
+ const payers = new Set(this.entries.map((e) => e.from));
307
+ const recipients = new Set(this.entries.map((e) => e.to));
308
+ return {
309
+ totalEntries: this.entries.length,
310
+ unsettledEntries: unsettled.length,
311
+ unsettledVolume: unsettled.reduce((sum, e) => sum + e.amount, 0),
312
+ uniquePayers: payers.size,
313
+ uniqueRecipients: recipients.size,
314
+ };
315
+ }
316
+
317
+ /**
318
+ * Clear all settled entries (garbage collection).
319
+ */
320
+ pruneSettled(): number {
321
+ const before = this.entries.length;
322
+ this.entries = this.entries.filter((e) => !e.settled);
323
+ return before - this.entries.length;
324
+ }
325
+
326
+ /**
327
+ * Reset the entire ledger.
328
+ */
329
+ reset(): void {
330
+ this.entries = [];
331
+ this.balances.clear();
332
+ this.txCounter = 0;
333
+ }
334
+ }
335
+
336
+ // =============================================================================
337
+ // x402 FACILITATOR
338
+ // =============================================================================
339
+
340
+ /**
341
+ * x402 Payment Protocol Facilitator
342
+ *
343
+ * Central coordinator for x402 payment flows in HoloScript. Manages:
344
+ * - PaymentRequired response generation for @credit-gated resources
345
+ * - Payment verification (signature + authorization validity)
346
+ * - Dual-mode settlement (in-memory micro vs on-chain macro)
347
+ * - Optimistic execution with async settlement verification
348
+ * - Batch settlement of accumulated micro-payments
349
+ *
350
+ * Security considerations:
351
+ * - All signatures are verified before granting access
352
+ * - Nonce tracking prevents replay attacks
353
+ * - ValidBefore/ValidAfter windowing prevents stale authorizations
354
+ * - In-memory ledger has hard caps to prevent memory exhaustion
355
+ * - Optimistic execution only for amounts below configurable threshold
356
+ */
357
+ export class X402Facilitator {
358
+ private config: Required<X402FacilitatorConfig>;
359
+ private ledger: MicroPaymentLedger;
360
+ private usedNonces: Set<string> = new Set();
361
+ private pendingSettlements: Map<string, X402PaymentPayload> = new Map();
362
+ private settlementResults: Map<string, X402SettlementResult> = new Map();
363
+ private batchSettlementTimer: ReturnType<typeof setInterval> | null = null;
364
+
365
+ constructor(config: X402FacilitatorConfig) {
366
+ this.config = {
367
+ recipientAddress: config.recipientAddress,
368
+ chain: config.chain,
369
+ secondaryChain: config.secondaryChain ?? config.chain,
370
+ microPaymentThreshold: config.microPaymentThreshold ?? MICRO_PAYMENT_THRESHOLD,
371
+ maxTimeoutSeconds: config.maxTimeoutSeconds ?? 60,
372
+ optimisticExecution: config.optimisticExecution ?? true,
373
+ batchSettlementIntervalMs: config.batchSettlementIntervalMs ?? 300_000, // 5 min
374
+ maxLedgerEntries: config.maxLedgerEntries ?? 10_000,
375
+ facilitatorUrl: config.facilitatorUrl ?? 'https://x402.org/facilitator',
376
+ resourceDescription: config.resourceDescription ?? 'HoloScript premium resource',
377
+ };
378
+
379
+ this.ledger = new MicroPaymentLedger(this.config.maxLedgerEntries);
380
+ }
381
+
382
+ // ===========================================================================
383
+ // PAYMENT REQUIRED RESPONSE GENERATION
384
+ // ===========================================================================
385
+
386
+ /**
387
+ * Generate an HTTP 402 PaymentRequired response body.
388
+ *
389
+ * @param resource - The resource path being requested (e.g., "/api/scene/premium")
390
+ * @param amountUSDC - Price in USDC (human-readable, e.g., 0.05 for 5 cents)
391
+ * @param description - Human-readable description of what is being paid for
392
+ * @returns PaymentRequired response body conforming to x402 spec
393
+ */
394
+ createPaymentRequired(
395
+ resource: string,
396
+ amountUSDC: number,
397
+ description?: string
398
+ ): X402PaymentRequired {
399
+ // Convert human-readable USDC to base units (6 decimals)
400
+ const baseUnits = Math.round(amountUSDC * 1_000_000).toString();
401
+ const desc = description ?? this.config.resourceDescription;
402
+
403
+ const accepts: X402PaymentOption[] = [
404
+ {
405
+ scheme: 'exact',
406
+ network: this.config.chain,
407
+ maxAmountRequired: baseUnits,
408
+ resource,
409
+ description: desc,
410
+ payTo: this.config.recipientAddress,
411
+ asset: USDC_CONTRACTS[this.config.chain],
412
+ maxTimeoutSeconds: this.config.maxTimeoutSeconds,
413
+ },
414
+ ];
415
+
416
+ // Add secondary chain option if different
417
+ if (this.config.secondaryChain !== this.config.chain) {
418
+ accepts.push({
419
+ scheme: 'exact',
420
+ network: this.config.secondaryChain,
421
+ maxAmountRequired: baseUnits,
422
+ resource,
423
+ description: desc,
424
+ payTo: this.config.recipientAddress,
425
+ asset: USDC_CONTRACTS[this.config.secondaryChain],
426
+ maxTimeoutSeconds: this.config.maxTimeoutSeconds,
427
+ });
428
+ }
429
+
430
+ return {
431
+ x402Version: X402_VERSION,
432
+ accepts,
433
+ error: 'X-PAYMENT header is required',
434
+ };
435
+ }
436
+
437
+ // ===========================================================================
438
+ // PAYMENT VERIFICATION
439
+ // ===========================================================================
440
+
441
+ /**
442
+ * Verify an X-PAYMENT header payload.
443
+ *
444
+ * Checks:
445
+ * 1. Protocol version matches
446
+ * 2. Nonce has not been used (replay protection)
447
+ * 3. Authorization window is valid (validAfter <= now <= validBefore)
448
+ * 4. Payment amount matches or exceeds required amount
449
+ * 5. Recipient matches configured address
450
+ * 6. Network is supported
451
+ *
452
+ * NOTE: Signature cryptographic verification requires on-chain verification
453
+ * via the facilitator service. This method validates the structural/temporal
454
+ * aspects. For full verification including signature, use verifyAndSettle().
455
+ *
456
+ * @param payment - Decoded X-PAYMENT payload
457
+ * @param requiredAmount - Minimum amount in USDC base units
458
+ * @returns Verification result
459
+ */
460
+ verifyPayment(payment: X402PaymentPayload, requiredAmount: string): X402VerificationResult {
461
+ // Check protocol version
462
+ if (payment.x402Version !== X402_VERSION) {
463
+ return { isValid: false, invalidReason: `Unsupported x402 version: ${payment.x402Version}` };
464
+ }
465
+
466
+ // Check scheme
467
+ if (payment.scheme !== 'exact') {
468
+ return { isValid: false, invalidReason: `Unsupported scheme: ${payment.scheme}` };
469
+ }
470
+
471
+ // Check network
472
+ if (!USDC_CONTRACTS[payment.network]) {
473
+ return { isValid: false, invalidReason: `Unsupported network: ${payment.network}` };
474
+ }
475
+
476
+ // Check nonce (replay protection)
477
+ const nonce = payment.payload.authorization.nonce;
478
+ if (this.usedNonces.has(nonce)) {
479
+ return { isValid: false, invalidReason: 'Nonce already used (replay attack prevented)' };
480
+ }
481
+
482
+ // Check authorization window
483
+ const now = Math.floor(Date.now() / 1000);
484
+ const validAfter = parseInt(payment.payload.authorization.validAfter, 10);
485
+ const validBefore = parseInt(payment.payload.authorization.validBefore, 10);
486
+
487
+ if (now < validAfter) {
488
+ return {
489
+ isValid: false,
490
+ invalidReason: 'Authorization not yet valid (validAfter in future)',
491
+ };
492
+ }
493
+ if (now > validBefore) {
494
+ return { isValid: false, invalidReason: 'Authorization expired (validBefore in past)' };
495
+ }
496
+
497
+ // Check amount
498
+ const paymentAmount = BigInt(payment.payload.authorization.value);
499
+ const required = BigInt(requiredAmount);
500
+ if (paymentAmount < required) {
501
+ return {
502
+ isValid: false,
503
+ invalidReason: `Insufficient payment: ${paymentAmount} < ${required}`,
504
+ };
505
+ }
506
+
507
+ // Check recipient
508
+ const payTo = payment.payload.authorization.to.toLowerCase();
509
+ const configRecipient = this.config.recipientAddress.toLowerCase();
510
+ if (payTo !== configRecipient) {
511
+ return {
512
+ isValid: false,
513
+ invalidReason: `Recipient mismatch: ${payTo} !== ${configRecipient}`,
514
+ };
515
+ }
516
+
517
+ // Check signature is present
518
+ if (!payment.payload.signature || payment.payload.signature.length < 10) {
519
+ return { isValid: false, invalidReason: 'Missing or invalid signature' };
520
+ }
521
+
522
+ return { isValid: true, invalidReason: null };
523
+ }
524
+
525
+ // ===========================================================================
526
+ // DUAL-MODE SETTLEMENT
527
+ // ===========================================================================
528
+
529
+ /**
530
+ * Determine the settlement mode based on the payment amount.
531
+ *
532
+ * @param amountBaseUnits - Amount in USDC base units (6 decimals)
533
+ * @returns Settlement mode
534
+ */
535
+ getSettlementMode(amountBaseUnits: number): SettlementMode {
536
+ return amountBaseUnits < this.config.microPaymentThreshold ? 'in_memory' : 'on_chain';
537
+ }
538
+
539
+ /**
540
+ * Process a payment with dual-mode settlement.
541
+ *
542
+ * For micro-payments (< threshold):
543
+ * - Records in in-memory ledger immediately
544
+ * - Returns instant success
545
+ * - Batch settles periodically
546
+ *
547
+ * For macro-payments (>= threshold):
548
+ * - If optimistic execution enabled: grants access immediately, settles async
549
+ * - If not: waits for settlement before granting access
550
+ *
551
+ * @param payment - Decoded X-PAYMENT payload
552
+ * @param resource - Resource being accessed
553
+ * @param requiredAmount - Required amount in USDC base units
554
+ * @returns Settlement result
555
+ */
556
+ async processPayment(
557
+ payment: X402PaymentPayload,
558
+ resource: string,
559
+ requiredAmount: string
560
+ ): Promise<X402SettlementResult> {
561
+ // Step 1: Verify the payment
562
+ const verification = this.verifyPayment(payment, requiredAmount);
563
+ if (!verification.isValid) {
564
+ return {
565
+ success: false,
566
+ transaction: null,
567
+ network: payment.network,
568
+ payer: payment.payload.authorization.from,
569
+ errorReason: verification.invalidReason,
570
+ mode: 'on_chain',
571
+ settledAt: Date.now(),
572
+ };
573
+ }
574
+
575
+ // Mark nonce as used
576
+ this.usedNonces.add(payment.payload.authorization.nonce);
577
+
578
+ const amount = parseInt(payment.payload.authorization.value, 10);
579
+ const mode = this.getSettlementMode(amount);
580
+
581
+ if (mode === 'in_memory') {
582
+ return this.settleMicroPayment(payment, resource, amount);
583
+ } else {
584
+ return this.settleOnChain(payment, resource, requiredAmount);
585
+ }
586
+ }
587
+
588
+ /**
589
+ * Settle a micro-payment in the in-memory ledger.
590
+ */
591
+ private settleMicroPayment(
592
+ payment: X402PaymentPayload,
593
+ resource: string,
594
+ amount: number
595
+ ): X402SettlementResult {
596
+ const entry = this.ledger.record(
597
+ payment.payload.authorization.from,
598
+ payment.payload.authorization.to,
599
+ amount,
600
+ resource
601
+ );
602
+
603
+ return {
604
+ success: true,
605
+ transaction: entry.id, // In-memory tx ID
606
+ network: 'in_memory',
607
+ payer: payment.payload.authorization.from,
608
+ errorReason: null,
609
+ mode: 'in_memory',
610
+ settledAt: Date.now(),
611
+ };
612
+ }
613
+
614
+ /**
615
+ * Settle a payment on-chain via the facilitator service.
616
+ *
617
+ * In optimistic mode: returns success immediately, verifies async.
618
+ * In non-optimistic mode: waits for facilitator confirmation.
619
+ */
620
+ private async settleOnChain(
621
+ payment: X402PaymentPayload,
622
+ _resource: string,
623
+ _requiredAmount: string
624
+ ): Promise<X402SettlementResult> {
625
+ const payer = payment.payload.authorization.from;
626
+ const nonce = payment.payload.authorization.nonce;
627
+
628
+ if (this.config.optimisticExecution) {
629
+ // Optimistic: grant access now, verify async
630
+ this.pendingSettlements.set(nonce, payment);
631
+
632
+ // Fire-and-forget async settlement
633
+ this.verifySettlementAsync(payment).catch((err) => {
634
+ console.error('[x402] Async settlement verification failed:', err);
635
+ // Record failed settlement
636
+ this.settlementResults.set(nonce, {
637
+ success: false,
638
+ transaction: null,
639
+ network: payment.network,
640
+ payer,
641
+ errorReason: `Async verification failed: ${err instanceof Error ? err.message : String(err)}`,
642
+ mode: 'on_chain',
643
+ settledAt: Date.now(),
644
+ });
645
+ });
646
+
647
+ return {
648
+ success: true,
649
+ transaction: `pending_${nonce}`,
650
+ network: payment.network,
651
+ payer,
652
+ errorReason: null,
653
+ mode: 'on_chain',
654
+ settledAt: Date.now(),
655
+ };
656
+ } else {
657
+ // Non-optimistic: wait for facilitator
658
+ return this.verifySettlementSync(payment);
659
+ }
660
+ }
661
+
662
+ /**
663
+ * Verify settlement asynchronously (for optimistic execution).
664
+ * Calls the facilitator service to verify and execute the on-chain transfer.
665
+ */
666
+ private async verifySettlementAsync(payment: X402PaymentPayload): Promise<void> {
667
+ const nonce = payment.payload.authorization.nonce;
668
+
669
+ try {
670
+ const result = await this.callFacilitator(payment);
671
+ this.settlementResults.set(nonce, result);
672
+ this.pendingSettlements.delete(nonce);
673
+ } catch (err) {
674
+ this.settlementResults.set(nonce, {
675
+ success: false,
676
+ transaction: null,
677
+ network: payment.network,
678
+ payer: payment.payload.authorization.from,
679
+ errorReason: `Facilitator error: ${err instanceof Error ? err.message : String(err)}`,
680
+ mode: 'on_chain',
681
+ settledAt: Date.now(),
682
+ });
683
+ this.pendingSettlements.delete(nonce);
684
+ }
685
+ }
686
+
687
+ /**
688
+ * Verify settlement synchronously (blocking).
689
+ */
690
+ private async verifySettlementSync(payment: X402PaymentPayload): Promise<X402SettlementResult> {
691
+ return this.callFacilitator(payment);
692
+ }
693
+
694
+ /**
695
+ * Call the x402 facilitator service for on-chain settlement.
696
+ *
697
+ * The facilitator:
698
+ * 1. Validates the EIP-712/Ed25519 signature cryptographically
699
+ * 2. Submits the `transferWithAuthorization` transaction on-chain
700
+ * 3. Returns the transaction hash and confirmation
701
+ */
702
+ private async callFacilitator(payment: X402PaymentPayload): Promise<X402SettlementResult> {
703
+ const payer = payment.payload.authorization.from;
704
+
705
+ try {
706
+ const response = await fetch(`${this.config.facilitatorUrl}/settle`, {
707
+ method: 'POST',
708
+ headers: { 'Content-Type': 'application/json' },
709
+ body: JSON.stringify({
710
+ x402Version: X402_VERSION,
711
+ scheme: payment.scheme,
712
+ network: payment.network,
713
+ payload: payment.payload,
714
+ }),
715
+ });
716
+
717
+ if (!response.ok) {
718
+ const error = await response.text().catch(() => response.statusText);
719
+ return {
720
+ success: false,
721
+ transaction: null,
722
+ network: payment.network,
723
+ payer,
724
+ errorReason: `Facilitator returned ${response.status}: ${error}`,
725
+ mode: 'on_chain',
726
+ settledAt: Date.now(),
727
+ };
728
+ }
729
+
730
+ const result = (await response.json()) as {
731
+ success: boolean;
732
+ transaction?: string;
733
+ errorReason?: string;
734
+ };
735
+
736
+ return {
737
+ success: result.success,
738
+ transaction: result.transaction ?? null,
739
+ network: payment.network,
740
+ payer,
741
+ errorReason: result.errorReason ?? null,
742
+ mode: 'on_chain',
743
+ settledAt: Date.now(),
744
+ };
745
+ } catch (err) {
746
+ return {
747
+ success: false,
748
+ transaction: null,
749
+ network: payment.network,
750
+ payer,
751
+ errorReason: `Network error: ${err instanceof Error ? err.message : String(err)}`,
752
+ mode: 'on_chain',
753
+ settledAt: Date.now(),
754
+ };
755
+ }
756
+ }
757
+
758
+ // ===========================================================================
759
+ // BATCH SETTLEMENT
760
+ // ===========================================================================
761
+
762
+ /**
763
+ * Start automatic batch settlement of in-memory ledger entries.
764
+ */
765
+ startBatchSettlement(): void {
766
+ if (this.batchSettlementTimer) return;
767
+
768
+ this.batchSettlementTimer = setInterval(() => {
769
+ this.runBatchSettlement().catch((err) => {
770
+ console.error('[x402] Batch settlement error:', err);
771
+ });
772
+ }, this.config.batchSettlementIntervalMs);
773
+ }
774
+
775
+ /**
776
+ * Stop automatic batch settlement.
777
+ */
778
+ stopBatchSettlement(): void {
779
+ if (this.batchSettlementTimer) {
780
+ clearInterval(this.batchSettlementTimer);
781
+ this.batchSettlementTimer = null;
782
+ }
783
+ }
784
+
785
+ /**
786
+ * Run a single batch settlement cycle.
787
+ * Aggregates unsettled micro-payments by payer and submits on-chain.
788
+ */
789
+ async runBatchSettlement(): Promise<{
790
+ settled: number;
791
+ failed: number;
792
+ totalVolume: number;
793
+ }> {
794
+ const unsettled = this.ledger.getUnsettled();
795
+ if (unsettled.length === 0) {
796
+ return { settled: 0, failed: 0, totalVolume: 0 };
797
+ }
798
+
799
+ // Aggregate by payer
800
+ const byPayer = new Map<string, LedgerEntry[]>();
801
+ for (const entry of unsettled) {
802
+ const existing = byPayer.get(entry.from) ?? [];
803
+ existing.push(entry);
804
+ byPayer.set(entry.from, existing);
805
+ }
806
+
807
+ let settled = 0;
808
+ let failed = 0;
809
+ let totalVolume = 0;
810
+
811
+ for (const [_payer, entries] of byPayer) {
812
+ const totalAmount = entries.reduce((sum, e) => sum + e.amount, 0);
813
+ totalVolume += totalAmount;
814
+
815
+ // In a real implementation, this would submit an aggregated
816
+ // on-chain transaction. For now, mark as settled.
817
+ const entryIds = entries.map((e) => e.id);
818
+ const batchTxHash = `batch_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
819
+
820
+ this.ledger.markSettled(entryIds, batchTxHash);
821
+ settled += entries.length;
822
+ }
823
+
824
+ return { settled, failed, totalVolume };
825
+ }
826
+
827
+ // ===========================================================================
828
+ // X-PAYMENT HEADER HELPERS
829
+ // ===========================================================================
830
+
831
+ /**
832
+ * Decode a base64-encoded X-PAYMENT header into a payment payload.
833
+ */
834
+ static decodeXPaymentHeader(header: string): X402PaymentPayload | null {
835
+ try {
836
+ const decoded =
837
+ typeof atob === 'function' ? atob(header) : Buffer.from(header, 'base64').toString('utf-8');
838
+ return JSON.parse(decoded) as X402PaymentPayload;
839
+ } catch {
840
+ return null;
841
+ }
842
+ }
843
+
844
+ /**
845
+ * Encode a payment payload into a base64 string for the X-PAYMENT header.
846
+ */
847
+ static encodeXPaymentHeader(payload: X402PaymentPayload): string {
848
+ const json = JSON.stringify(payload);
849
+ return typeof btoa === 'function' ? btoa(json) : Buffer.from(json, 'utf-8').toString('base64');
850
+ }
851
+
852
+ /**
853
+ * Create an X-PAYMENT-RESPONSE header value from a settlement result.
854
+ */
855
+ static createPaymentResponseHeader(result: X402SettlementResult): string {
856
+ const response = {
857
+ success: result.success,
858
+ transaction: result.transaction,
859
+ network: result.network,
860
+ payer: result.payer,
861
+ errorReason: result.errorReason,
862
+ };
863
+ const json = JSON.stringify(response);
864
+ return typeof btoa === 'function' ? btoa(json) : Buffer.from(json, 'utf-8').toString('base64');
865
+ }
866
+
867
+ // ===========================================================================
868
+ // QUERY / STATUS
869
+ // ===========================================================================
870
+
871
+ /**
872
+ * Check the settlement status of a pending optimistic execution.
873
+ */
874
+ getSettlementStatus(nonce: string): X402SettlementResult | 'pending' | 'unknown' {
875
+ const result = this.settlementResults.get(nonce);
876
+ if (result) return result;
877
+ if (this.pendingSettlements.has(nonce)) return 'pending';
878
+ return 'unknown';
879
+ }
880
+
881
+ /**
882
+ * Get the in-memory ledger instance.
883
+ */
884
+ getLedger(): MicroPaymentLedger {
885
+ return this.ledger;
886
+ }
887
+
888
+ /**
889
+ * Get facilitator statistics.
890
+ */
891
+ getStats(): {
892
+ usedNonces: number;
893
+ pendingSettlements: number;
894
+ completedSettlements: number;
895
+ ledger: ReturnType<MicroPaymentLedger['getStats']>;
896
+ } {
897
+ return {
898
+ usedNonces: this.usedNonces.size,
899
+ pendingSettlements: this.pendingSettlements.size,
900
+ completedSettlements: this.settlementResults.size,
901
+ ledger: this.ledger.getStats(),
902
+ };
903
+ }
904
+
905
+ /**
906
+ * Clean up resources.
907
+ */
908
+ dispose(): void {
909
+ this.stopBatchSettlement();
910
+ this.usedNonces.clear();
911
+ this.pendingSettlements.clear();
912
+ this.settlementResults.clear();
913
+ this.ledger.reset();
914
+ }
915
+ }
916
+
917
+ // =============================================================================
918
+ // @credit TRAIT HANDLER
919
+ // =============================================================================
920
+
921
+ /**
922
+ * Configuration for the @credit trait.
923
+ * Attach to any HoloScript object to gate it behind x402 payment.
924
+ */
925
+ export interface CreditTraitConfig {
926
+ /** Price in USDC (human-readable, e.g., 0.05 = 5 cents) */
927
+ price: number;
928
+ /** Settlement chain */
929
+ chain: SettlementChain;
930
+ /** Recipient address */
931
+ recipient: string;
932
+ /** Human-readable resource description */
933
+ description: string;
934
+ /** Maximum seconds for payment timeout */
935
+ timeout: number;
936
+ /** Secondary chain for multi-chain support */
937
+ secondary_chain?: SettlementChain;
938
+ /** Enable optimistic execution */
939
+ optimistic: boolean;
940
+ /** Micro-payment threshold override (USDC, human-readable) */
941
+ micro_threshold?: number;
942
+ }
943
+
944
+ /**
945
+ * Internal state for the @credit trait.
946
+ */
947
+ interface CreditTraitState {
948
+ facilitator: X402Facilitator;
949
+ accessGranted: Map<string, { grantedAt: number; expiresAt: number; settlementId: string }>;
950
+ totalRevenue: number;
951
+ totalRequests: number;
952
+ totalGranted: number;
953
+ totalDenied: number;
954
+ }
955
+
956
+ /**
957
+ * @credit Trait Handler
958
+ *
959
+ * When attached to a HoloScript object, this trait gates access behind
960
+ * x402 payment. The trait:
961
+ *
962
+ * 1. On access attempt: emits 'credit:payment_required' with the 402 response body
963
+ * 2. On payment received: verifies via facilitator and emits 'credit:access_granted'
964
+ * 3. On payment failure: emits 'credit:access_denied'
965
+ *
966
+ * Events emitted:
967
+ * credit:initialized { config }
968
+ * credit:payment_required { resource, paymentRequired: X402PaymentRequired }
969
+ * credit:access_granted { payer, amount, mode, resource }
970
+ * credit:access_denied { payer, reason, resource }
971
+ * credit:settlement_status { nonce, status }
972
+ * credit:batch_settled { settled, failed, totalVolume }
973
+ * credit:stats { totalRevenue, totalRequests, totalGranted, totalDenied }
974
+ *
975
+ * @example HoloScript usage:
976
+ * ```holoscript
977
+ * object "premium_scene" {
978
+ * @credit(price: 0.05, chain: "base", recipient: "0x...", description: "Premium VR scene")
979
+ * geometry: "sphere"
980
+ * color: "#gold"
981
+ * }
982
+ * ```
983
+ */
984
+ export const creditTraitHandler: TraitHandler<CreditTraitConfig> = {
985
+ name: 'credit',
986
+
987
+ defaultConfig: {
988
+ price: 0.01,
989
+ chain: 'base',
990
+ recipient: '0x0000000000000000000000000000000000000000',
991
+ description: 'HoloScript premium resource',
992
+ timeout: 60,
993
+ optimistic: true,
994
+ },
995
+
996
+ onAttach(node: HSPlusNode, config: CreditTraitConfig, context: TraitContext): void {
997
+ const facilitator = new X402Facilitator({
998
+ recipientAddress: config.recipient,
999
+ chain: config.chain,
1000
+ secondaryChain: config.secondary_chain,
1001
+ microPaymentThreshold: config.micro_threshold
1002
+ ? Math.round(config.micro_threshold * 1_000_000)
1003
+ : undefined,
1004
+ maxTimeoutSeconds: config.timeout,
1005
+ optimisticExecution: config.optimistic,
1006
+ resourceDescription: config.description,
1007
+ });
1008
+
1009
+ const state: CreditTraitState = {
1010
+ facilitator,
1011
+ accessGranted: new Map(),
1012
+ totalRevenue: 0,
1013
+ totalRequests: 0,
1014
+ totalGranted: 0,
1015
+ totalDenied: 0,
1016
+ };
1017
+
1018
+ node.__creditState = state;
1019
+
1020
+ context?.emit?.('credit:initialized', {
1021
+ price: config.price,
1022
+ chain: config.chain,
1023
+ recipient: config.recipient,
1024
+ description: config.description,
1025
+ });
1026
+ },
1027
+
1028
+ onDetach(node: HSPlusNode, _config: CreditTraitConfig, context: TraitContext): void {
1029
+ const state = node.__creditState as CreditTraitState | undefined;
1030
+ if (state) {
1031
+ state.facilitator.dispose();
1032
+ context.emit?.('credit:shutdown', {
1033
+ totalRevenue: state.totalRevenue,
1034
+ totalRequests: state.totalRequests,
1035
+ totalGranted: state.totalGranted,
1036
+ totalDenied: state.totalDenied,
1037
+ });
1038
+ }
1039
+ delete node.__creditState;
1040
+ },
1041
+
1042
+ onUpdate(node: HSPlusNode, _config: CreditTraitConfig, context: TraitContext, _delta: number): void {
1043
+ const state = node.__creditState as CreditTraitState | undefined;
1044
+ if (!state) return;
1045
+
1046
+ // Expire stale access grants
1047
+ const now = Date.now();
1048
+ for (const [payer, grant] of state.accessGranted) {
1049
+ if (grant.expiresAt > 0 && now > grant.expiresAt) {
1050
+ state.accessGranted.delete(payer);
1051
+ context.emit?.('credit:access_expired', { payer, resource: _config.description });
1052
+ }
1053
+ }
1054
+ },
1055
+
1056
+ onEvent(node: HSPlusNode, config: CreditTraitConfig, context: TraitContext, event: TraitEvent): void {
1057
+ const state = node.__creditState as CreditTraitState | undefined;
1058
+ if (!state) return;
1059
+
1060
+ const eventType = typeof event === 'string' ? event : event.type;
1061
+ const payload = event?.payload ?? event;
1062
+
1063
+ switch (eventType) {
1064
+ // ─── Request access (generates 402 response) ─────────────────────
1065
+ case 'credit:request_access': {
1066
+ state.totalRequests++;
1067
+ const resource = payload.resource ?? config.description;
1068
+ const payer = payload.payer ?? payload.from;
1069
+
1070
+ // Check if already granted
1071
+ const existing = state.accessGranted.get(payer);
1072
+ if (existing && (existing.expiresAt === 0 || Date.now() < existing.expiresAt)) {
1073
+ context.emit?.('credit:access_granted', {
1074
+ payer,
1075
+ amount: 0,
1076
+ mode: 'cached',
1077
+ resource,
1078
+ });
1079
+ state.totalGranted++;
1080
+ return;
1081
+ }
1082
+
1083
+ // Generate 402 PaymentRequired
1084
+ const paymentRequired = state.facilitator.createPaymentRequired(
1085
+ resource,
1086
+ config.price,
1087
+ config.description
1088
+ );
1089
+
1090
+ context.emit?.('credit:payment_required', {
1091
+ resource,
1092
+ paymentRequired,
1093
+ statusCode: 402,
1094
+ headers: {
1095
+ 'Content-Type': 'application/json',
1096
+ },
1097
+ });
1098
+ break;
1099
+ }
1100
+
1101
+ // ─── Submit payment (process X-PAYMENT header) ───────────────────
1102
+ case 'credit:submit_payment': {
1103
+ const resource = payload.resource ?? config.description;
1104
+ const xPaymentHeader = payload.xPayment ?? payload.payment;
1105
+
1106
+ if (!xPaymentHeader) {
1107
+ context.emit?.('credit:access_denied', {
1108
+ payer: 'unknown',
1109
+ reason: 'Missing X-PAYMENT header',
1110
+ resource,
1111
+ });
1112
+ state.totalDenied++;
1113
+ return;
1114
+ }
1115
+
1116
+ // Decode the X-PAYMENT header
1117
+ const paymentPayload =
1118
+ typeof xPaymentHeader === 'string'
1119
+ ? X402Facilitator.decodeXPaymentHeader(xPaymentHeader)
1120
+ : (xPaymentHeader as X402PaymentPayload);
1121
+
1122
+ if (!paymentPayload) {
1123
+ context.emit?.('credit:access_denied', {
1124
+ payer: 'unknown',
1125
+ reason: 'Invalid X-PAYMENT header encoding',
1126
+ resource,
1127
+ });
1128
+ state.totalDenied++;
1129
+ return;
1130
+ }
1131
+
1132
+ const requiredAmount = Math.round(config.price * 1_000_000).toString();
1133
+ const payer = paymentPayload.payload.authorization.from;
1134
+
1135
+ // Process payment (async, but we handle it via event)
1136
+ state.facilitator
1137
+ .processPayment(paymentPayload, resource, requiredAmount)
1138
+ .then((result) => {
1139
+ if (result.success) {
1140
+ // Grant access
1141
+ state.accessGranted.set(payer, {
1142
+ grantedAt: Date.now(),
1143
+ expiresAt: config.timeout > 0 ? Date.now() + config.timeout * 1000 : 0,
1144
+ settlementId: result.transaction ?? '',
1145
+ });
1146
+ state.totalRevenue += config.price;
1147
+ state.totalGranted++;
1148
+
1149
+ context.emit?.('credit:access_granted', {
1150
+ payer,
1151
+ amount: config.price,
1152
+ mode: result.mode,
1153
+ resource,
1154
+ transaction: result.transaction,
1155
+ network: result.network,
1156
+ });
1157
+
1158
+ // Emit X-PAYMENT-RESPONSE header for the HTTP response
1159
+ context.emit?.('credit:payment_response', {
1160
+ resource,
1161
+ headers: {
1162
+ 'X-PAYMENT-RESPONSE': X402Facilitator.createPaymentResponseHeader(result),
1163
+ },
1164
+ });
1165
+ } else {
1166
+ state.totalDenied++;
1167
+ context.emit?.('credit:access_denied', {
1168
+ payer,
1169
+ reason: result.errorReason,
1170
+ resource,
1171
+ });
1172
+ }
1173
+ })
1174
+ .catch((err) => {
1175
+ state.totalDenied++;
1176
+ context.emit?.('credit:access_denied', {
1177
+ payer,
1178
+ reason: `Settlement error: ${err instanceof Error ? err.message : String(err)}`,
1179
+ resource,
1180
+ });
1181
+ });
1182
+ break;
1183
+ }
1184
+
1185
+ // ─── Check settlement status ─────────────────────────────────────
1186
+ case 'credit:check_settlement': {
1187
+ const nonce = payload.nonce;
1188
+ if (!nonce) return;
1189
+
1190
+ const status = state.facilitator.getSettlementStatus(nonce);
1191
+ context.emit?.('credit:settlement_status', { nonce, status });
1192
+ break;
1193
+ }
1194
+
1195
+ // ─── Run batch settlement ────────────────────────────────────────
1196
+ case 'credit:batch_settle': {
1197
+ state.facilitator
1198
+ .runBatchSettlement()
1199
+ .then((result) => {
1200
+ context.emit?.('credit:batch_settled', result);
1201
+ })
1202
+ .catch((err) => {
1203
+ context.emit?.('credit:batch_error', {
1204
+ error: err instanceof Error ? err.message : String(err),
1205
+ });
1206
+ });
1207
+ break;
1208
+ }
1209
+
1210
+ // ─── Query stats ─────────────────────────────────────────────────
1211
+ case 'credit:get_stats': {
1212
+ const facilStats = state.facilitator.getStats();
1213
+ context.emit?.('credit:stats', {
1214
+ totalRevenue: state.totalRevenue,
1215
+ totalRequests: state.totalRequests,
1216
+ totalGranted: state.totalGranted,
1217
+ totalDenied: state.totalDenied,
1218
+ facilitator: facilStats,
1219
+ });
1220
+ break;
1221
+ }
1222
+
1223
+ // ─── Revoke access ───────────────────────────────────────────────
1224
+ case 'credit:revoke_access': {
1225
+ const payer = payload.payer ?? payload.from;
1226
+ if (payer) {
1227
+ state.accessGranted.delete(payer);
1228
+ context.emit?.('credit:access_revoked', { payer });
1229
+ }
1230
+ break;
1231
+ }
1232
+ }
1233
+ },
1234
+ };
1235
+
1236
+ export default creditTraitHandler;
1237
+
1238
+ // =============================================================================
1239
+ // CHAIN ID CONSTANTS
1240
+ // =============================================================================
1241
+
1242
+ /** EVM chain IDs for supported settlement networks */
1243
+ export const CHAIN_IDS: Record<string, number> = {
1244
+ base: 8453,
1245
+ 'base-sepolia': 84532,
1246
+ };
1247
+
1248
+ /** Reverse lookup: chain ID -> settlement chain name */
1249
+ export const CHAIN_ID_TO_NETWORK: Record<number, SettlementChain> = {
1250
+ 8453: 'base',
1251
+ 84532: 'base-sepolia',
1252
+ };
1253
+
1254
+ // =============================================================================
1255
+ // SETTLEMENT EVENT TYPES
1256
+ // =============================================================================
1257
+
1258
+ /**
1259
+ * Settlement audit event types emitted by PaymentGateway.
1260
+ */
1261
+ export type SettlementEventType =
1262
+ | 'payment:authorization_created'
1263
+ | 'payment:verification_started'
1264
+ | 'payment:verification_passed'
1265
+ | 'payment:verification_failed'
1266
+ | 'payment:settlement_started'
1267
+ | 'payment:settlement_completed'
1268
+ | 'payment:settlement_failed'
1269
+ | 'payment:refund_initiated'
1270
+ | 'payment:refund_completed'
1271
+ | 'payment:refund_failed'
1272
+ | 'payment:batch_settlement_started'
1273
+ | 'payment:batch_settlement_completed';
1274
+
1275
+ /**
1276
+ * Settlement audit event payload.
1277
+ */
1278
+ export interface SettlementEvent {
1279
+ /** Event type */
1280
+ type: SettlementEventType;
1281
+ /** ISO 8601 timestamp */
1282
+ timestamp: string;
1283
+ /** Unique event ID */
1284
+ eventId: string;
1285
+ /** Associated payment nonce (if applicable) */
1286
+ nonce: string | null;
1287
+ /** Payer address */
1288
+ payer: string | null;
1289
+ /** Recipient address */
1290
+ recipient: string | null;
1291
+ /** Amount in USDC base units */
1292
+ amount: string | null;
1293
+ /** Settlement chain */
1294
+ network: SettlementChain | 'in_memory' | null;
1295
+ /** Transaction hash (if on-chain) */
1296
+ transaction: string | null;
1297
+ /** Additional metadata */
1298
+ metadata: Record<string, unknown>;
1299
+ }
1300
+
1301
+ /** Event listener type for the PaymentGateway audit trail */
1302
+ export type SettlementEventListener = (event: SettlementEvent) => void;
1303
+
1304
+ // =============================================================================
1305
+ // REFUND TYPES
1306
+ // =============================================================================
1307
+
1308
+ /**
1309
+ * Refund request for reversing a completed payment.
1310
+ */
1311
+ export interface RefundRequest {
1312
+ /** Original payment nonce to refund */
1313
+ originalNonce: string;
1314
+ /** Reason for the refund */
1315
+ reason: string;
1316
+ /** Partial refund amount in USDC base units (null = full refund) */
1317
+ partialAmount: string | null;
1318
+ }
1319
+
1320
+ /**
1321
+ * Result of a refund operation.
1322
+ */
1323
+ export interface RefundResult {
1324
+ /** Whether the refund was processed */
1325
+ success: boolean;
1326
+ /** Refund ID for tracking */
1327
+ refundId: string;
1328
+ /** Amount refunded in USDC base units */
1329
+ amountRefunded: string;
1330
+ /** Original payment nonce */
1331
+ originalNonce: string;
1332
+ /** Transaction hash (for on-chain refunds) */
1333
+ transaction: string | null;
1334
+ /** Mode of the original settlement */
1335
+ originalMode: SettlementMode;
1336
+ /** Reason for the refund */
1337
+ reason: string;
1338
+ /** Error reason if failed */
1339
+ errorReason: string | null;
1340
+ /** Timestamp of refund */
1341
+ refundedAt: number;
1342
+ }
1343
+
1344
+ // =============================================================================
1345
+ // PAYMENT GATEWAY
1346
+ // =============================================================================
1347
+
1348
+ /**
1349
+ * PaymentGateway -- High-Level x402 Payment API
1350
+ *
1351
+ * Provides a unified, agent-friendly interface for the x402 payment protocol.
1352
+ * Composes over X402Facilitator to add:
1353
+ *
1354
+ * - `createPaymentAuthorization()` -- Generate 402 Payment Required responses
1355
+ * - `verifyPayment()` -- Validate X-PAYMENT headers
1356
+ * - `settlePayment()` -- Process and settle payments (micro or on-chain)
1357
+ * - `refundPayment()` -- Reverse completed transactions
1358
+ * - Settlement event emitter for audit trail
1359
+ * - Chain ID constants for Base L2 (8453)
1360
+ *
1361
+ * Settlement flow:
1362
+ * 1. Agent calls resource -> gateway returns 402 with payment requirements
1363
+ * 2. Agent signs EIP-712 authorization -> sends X-PAYMENT header
1364
+ * 3. Gateway verifies signature validity and temporal window
1365
+ * 4. Gateway settles payment (in-memory for micro, on-chain for macro)
1366
+ * 5. Gateway emits audit events at each step
1367
+ *
1368
+ * @example
1369
+ * ```typescript
1370
+ * const gateway = new PaymentGateway({
1371
+ * recipientAddress: '0x...',
1372
+ * chain: 'base',
1373
+ * });
1374
+ *
1375
+ * // Listen for audit events
1376
+ * gateway.on('payment:settlement_completed', (event) => {
1377
+ * console.log(`Payment settled: ${event.transaction}`);
1378
+ * });
1379
+ *
1380
+ * // Step 1: Generate 402 response
1381
+ * const auth = gateway.createPaymentAuthorization('/api/premium-scene', 0.05);
1382
+ *
1383
+ * // Step 2: Verify incoming payment
1384
+ * const verification = gateway.verifyPayment(xPaymentHeader, '50000');
1385
+ *
1386
+ * // Step 3: Settle
1387
+ * const settlement = await gateway.settlePayment(paymentPayload, '/api/premium-scene', '50000');
1388
+ *
1389
+ * // Step 4: Refund if needed
1390
+ * const refund = await gateway.refundPayment({
1391
+ * originalNonce: 'nonce_123',
1392
+ * reason: 'Content unavailable',
1393
+ * partialAmount: null,
1394
+ * });
1395
+ * ```
1396
+ */
1397
+ export class PaymentGateway {
1398
+ private facilitator: X402Facilitator;
1399
+ private listeners: Map<SettlementEventType | '*', Set<SettlementEventListener>> = new Map();
1400
+ private refundLedger: Map<string, RefundResult> = new Map();
1401
+ private eventCounter = 0;
1402
+ private readonly config: X402FacilitatorConfig;
1403
+
1404
+ constructor(config: X402FacilitatorConfig) {
1405
+ this.config = config;
1406
+ this.facilitator = new X402Facilitator(config);
1407
+ }
1408
+
1409
+ // ===========================================================================
1410
+ // EVENT EMITTER (Audit Trail)
1411
+ // ===========================================================================
1412
+
1413
+ /**
1414
+ * Subscribe to settlement audit events.
1415
+ * Use '*' to receive all events.
1416
+ *
1417
+ * @param eventType - Event type to listen for, or '*' for all
1418
+ * @param listener - Callback function
1419
+ * @returns Unsubscribe function
1420
+ */
1421
+ on(eventType: SettlementEventType | '*', listener: SettlementEventListener): () => void {
1422
+ if (!this.listeners.has(eventType)) {
1423
+ this.listeners.set(eventType, new Set());
1424
+ }
1425
+ this.listeners.get(eventType)!.add(listener);
1426
+
1427
+ return () => {
1428
+ this.listeners.get(eventType)?.delete(listener);
1429
+ };
1430
+ }
1431
+
1432
+ /**
1433
+ * Remove a specific listener.
1434
+ */
1435
+ off(eventType: SettlementEventType | '*', listener: SettlementEventListener): void {
1436
+ this.listeners.get(eventType)?.delete(listener);
1437
+ }
1438
+
1439
+ /**
1440
+ * Emit a settlement audit event.
1441
+ */
1442
+ private emit(
1443
+ type: SettlementEventType,
1444
+ data: Partial<Omit<SettlementEvent, 'type' | 'timestamp' | 'eventId'>>
1445
+ ): SettlementEvent {
1446
+ const event: SettlementEvent = {
1447
+ type,
1448
+ timestamp: new Date().toISOString(),
1449
+ eventId: `evt_${Date.now()}_${this.eventCounter++}`,
1450
+ nonce: data.nonce ?? null,
1451
+ payer: data.payer ?? null,
1452
+ recipient: data.recipient ?? null,
1453
+ amount: data.amount ?? null,
1454
+ network: data.network ?? null,
1455
+ transaction: data.transaction ?? null,
1456
+ metadata: data.metadata ?? {},
1457
+ };
1458
+
1459
+ // Notify specific listeners
1460
+ const typeListeners = this.listeners.get(type);
1461
+ if (typeListeners) {
1462
+ for (const listener of typeListeners) {
1463
+ try {
1464
+ listener(event);
1465
+ } catch {
1466
+ // Swallow listener errors to prevent breaking the payment flow
1467
+ }
1468
+ }
1469
+ }
1470
+
1471
+ // Notify wildcard listeners
1472
+ const wildcardListeners = this.listeners.get('*');
1473
+ if (wildcardListeners) {
1474
+ for (const listener of wildcardListeners) {
1475
+ try {
1476
+ listener(event);
1477
+ } catch {
1478
+ // Swallow listener errors
1479
+ }
1480
+ }
1481
+ }
1482
+
1483
+ return event;
1484
+ }
1485
+
1486
+ // ===========================================================================
1487
+ // PAYMENT AUTHORIZATION
1488
+ // ===========================================================================
1489
+
1490
+ /**
1491
+ * Create a payment authorization (HTTP 402 response body).
1492
+ *
1493
+ * This is step 1 of the x402 flow: the server tells the agent what payment
1494
+ * is required to access the resource.
1495
+ *
1496
+ * @param resource - Resource path being gated (e.g., "/api/premium-scene")
1497
+ * @param amountUSDC - Price in USDC (human-readable, e.g., 0.05 for 5 cents)
1498
+ * @param description - Human-readable description
1499
+ * @returns x402 PaymentRequired response body
1500
+ */
1501
+ createPaymentAuthorization(
1502
+ resource: string,
1503
+ amountUSDC: number,
1504
+ description?: string
1505
+ ): X402PaymentRequired & { chainId: number } {
1506
+ const paymentRequired = this.facilitator.createPaymentRequired(
1507
+ resource,
1508
+ amountUSDC,
1509
+ description
1510
+ );
1511
+
1512
+ this.emit('payment:authorization_created', {
1513
+ recipient: this.config.recipientAddress,
1514
+ amount: Math.round(amountUSDC * 1_000_000).toString(),
1515
+ network: this.config.chain,
1516
+ metadata: { resource, description: description ?? '' },
1517
+ });
1518
+
1519
+ return {
1520
+ ...paymentRequired,
1521
+ chainId: CHAIN_IDS[this.config.chain] ?? 0,
1522
+ };
1523
+ }
1524
+
1525
+ // ===========================================================================
1526
+ // PAYMENT VERIFICATION
1527
+ // ===========================================================================
1528
+
1529
+ /**
1530
+ * Verify an X-PAYMENT header.
1531
+ *
1532
+ * Accepts either a raw base64 string (from HTTP header) or a decoded payload.
1533
+ * Validates protocol version, nonce, temporal window, amount, and recipient.
1534
+ *
1535
+ * @param payment - Base64-encoded X-PAYMENT header string or decoded payload
1536
+ * @param requiredAmount - Required amount in USDC base units (string for precision)
1537
+ * @returns Verification result
1538
+ */
1539
+ verifyPayment(
1540
+ payment: string | X402PaymentPayload,
1541
+ requiredAmount: string
1542
+ ): X402VerificationResult & { decodedPayload: X402PaymentPayload | null } {
1543
+ // Decode if string
1544
+ const payload: X402PaymentPayload | null =
1545
+ typeof payment === 'string' ? X402Facilitator.decodeXPaymentHeader(payment) : payment;
1546
+
1547
+ if (!payload) {
1548
+ this.emit('payment:verification_failed', {
1549
+ metadata: { reason: 'Failed to decode X-PAYMENT header' },
1550
+ });
1551
+ return {
1552
+ isValid: false,
1553
+ invalidReason: 'Failed to decode X-PAYMENT header',
1554
+ decodedPayload: null,
1555
+ };
1556
+ }
1557
+
1558
+ const payer = payload.payload.authorization.from;
1559
+ const nonce = payload.payload.authorization.nonce;
1560
+
1561
+ this.emit('payment:verification_started', {
1562
+ payer,
1563
+ nonce,
1564
+ amount: payload.payload.authorization.value,
1565
+ network: payload.network,
1566
+ });
1567
+
1568
+ const result = this.facilitator.verifyPayment(payload, requiredAmount);
1569
+
1570
+ if (result.isValid) {
1571
+ this.emit('payment:verification_passed', {
1572
+ payer,
1573
+ nonce,
1574
+ amount: payload.payload.authorization.value,
1575
+ network: payload.network,
1576
+ });
1577
+ } else {
1578
+ this.emit('payment:verification_failed', {
1579
+ payer,
1580
+ nonce,
1581
+ metadata: { reason: result.invalidReason },
1582
+ });
1583
+ }
1584
+
1585
+ return { ...result, decodedPayload: payload };
1586
+ }
1587
+
1588
+ // ===========================================================================
1589
+ // PAYMENT SETTLEMENT
1590
+ // ===========================================================================
1591
+
1592
+ /**
1593
+ * Settle a verified payment.
1594
+ *
1595
+ * Routes to in-memory micro-payment ledger or on-chain settlement
1596
+ * depending on the amount. Emits audit events at each stage.
1597
+ *
1598
+ * @param payment - Decoded X-PAYMENT payload (or base64 string)
1599
+ * @param resource - Resource being accessed
1600
+ * @param requiredAmount - Required amount in USDC base units
1601
+ * @returns Settlement result
1602
+ */
1603
+ async settlePayment(
1604
+ payment: string | X402PaymentPayload,
1605
+ resource: string,
1606
+ requiredAmount: string
1607
+ ): Promise<X402SettlementResult> {
1608
+ // Decode if string
1609
+ const payload: X402PaymentPayload | null =
1610
+ typeof payment === 'string' ? X402Facilitator.decodeXPaymentHeader(payment) : payment;
1611
+
1612
+ if (!payload) {
1613
+ return {
1614
+ success: false,
1615
+ transaction: null,
1616
+ network: this.config.chain,
1617
+ payer: 'unknown',
1618
+ errorReason: 'Failed to decode payment payload',
1619
+ mode: 'on_chain',
1620
+ settledAt: Date.now(),
1621
+ };
1622
+ }
1623
+
1624
+ const payer = payload.payload.authorization.from;
1625
+ const nonce = payload.payload.authorization.nonce;
1626
+ const amount = payload.payload.authorization.value;
1627
+
1628
+ this.emit('payment:settlement_started', {
1629
+ payer,
1630
+ nonce,
1631
+ amount,
1632
+ network: payload.network,
1633
+ recipient: this.config.recipientAddress,
1634
+ metadata: { resource },
1635
+ });
1636
+
1637
+ const result = await this.facilitator.processPayment(payload, resource, requiredAmount);
1638
+
1639
+ if (result.success) {
1640
+ this.emit('payment:settlement_completed', {
1641
+ payer,
1642
+ nonce,
1643
+ amount,
1644
+ network: result.network,
1645
+ transaction: result.transaction,
1646
+ recipient: this.config.recipientAddress,
1647
+ metadata: { resource, mode: result.mode },
1648
+ });
1649
+ } else {
1650
+ this.emit('payment:settlement_failed', {
1651
+ payer,
1652
+ nonce,
1653
+ amount,
1654
+ network: result.network,
1655
+ metadata: { resource, errorReason: result.errorReason },
1656
+ });
1657
+ }
1658
+
1659
+ return result;
1660
+ }
1661
+
1662
+ // ===========================================================================
1663
+ // REFUND
1664
+ // ===========================================================================
1665
+
1666
+ /**
1667
+ * Refund a completed payment.
1668
+ *
1669
+ * For in-memory micro-payments: records a reverse entry in the ledger.
1670
+ * For on-chain payments: calls the facilitator service to initiate refund.
1671
+ *
1672
+ * @param request - Refund request details
1673
+ * @returns Refund result
1674
+ */
1675
+ async refundPayment(request: RefundRequest): Promise<RefundResult> {
1676
+ const { originalNonce, reason, partialAmount } = request;
1677
+
1678
+ this.emit('payment:refund_initiated', {
1679
+ nonce: originalNonce,
1680
+ metadata: { reason, partialAmount },
1681
+ });
1682
+
1683
+ // Look up the original settlement
1684
+ const originalStatus = this.facilitator.getSettlementStatus(originalNonce);
1685
+
1686
+ // Check if the original was a micro-payment by looking in the ledger
1687
+ const ledger = this.facilitator.getLedger();
1688
+ const allEntries = ledger.getEntriesForPayer(''); // We need to search all entries
1689
+
1690
+ // Try to find the original ledger entry by checking if transaction matches
1691
+ let originalEntry: LedgerEntry | undefined;
1692
+ const ledgerStats = ledger.getStats();
1693
+
1694
+ // For micro-payments, the transaction ID starts with "micro_"
1695
+ // For on-chain, we check the settlement results
1696
+ if (originalStatus !== 'unknown' && originalStatus !== 'pending') {
1697
+ const settlement = originalStatus as X402SettlementResult;
1698
+
1699
+ if (settlement.mode === 'in_memory' && settlement.transaction) {
1700
+ // It was a micro-payment -- record a reverse entry
1701
+ const refundAmount =
1702
+ (partialAmount ?? settlement.transaction)
1703
+ ? '0' // We'll try to find the amount from the ledger
1704
+ : '0';
1705
+
1706
+ const refundId = `refund_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1707
+
1708
+ // Record reverse ledger entry (swap from/to)
1709
+ const reverseEntry = ledger.record(
1710
+ this.config.recipientAddress,
1711
+ settlement.payer,
1712
+ partialAmount ? parseInt(partialAmount, 10) : 0,
1713
+ `refund:${originalNonce}`
1714
+ );
1715
+
1716
+ const result: RefundResult = {
1717
+ success: true,
1718
+ refundId,
1719
+ amountRefunded: partialAmount ?? '0',
1720
+ originalNonce,
1721
+ transaction: reverseEntry.id,
1722
+ originalMode: 'in_memory',
1723
+ reason,
1724
+ errorReason: null,
1725
+ refundedAt: Date.now(),
1726
+ };
1727
+
1728
+ this.refundLedger.set(refundId, result);
1729
+
1730
+ this.emit('payment:refund_completed', {
1731
+ nonce: originalNonce,
1732
+ payer: settlement.payer,
1733
+ amount: result.amountRefunded,
1734
+ transaction: reverseEntry.id,
1735
+ network: 'in_memory',
1736
+ metadata: { reason, refundId },
1737
+ });
1738
+
1739
+ return result;
1740
+ }
1741
+
1742
+ if (settlement.mode === 'on_chain') {
1743
+ // On-chain refund: call the facilitator service
1744
+ try {
1745
+ const response = await fetch(
1746
+ `${this.config.facilitatorUrl ?? 'https://x402.org/facilitator'}/refund`,
1747
+ {
1748
+ method: 'POST',
1749
+ headers: { 'Content-Type': 'application/json' },
1750
+ body: JSON.stringify({
1751
+ originalTransaction: settlement.transaction,
1752
+ originalNonce,
1753
+ refundAmount: partialAmount,
1754
+ reason,
1755
+ network: settlement.network,
1756
+ }),
1757
+ }
1758
+ );
1759
+
1760
+ const refundId = `refund_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1761
+
1762
+ if (response.ok) {
1763
+ const body = (await response.json()) as {
1764
+ success: boolean;
1765
+ transaction?: string;
1766
+ amountRefunded?: string;
1767
+ errorReason?: string;
1768
+ };
1769
+
1770
+ const result: RefundResult = {
1771
+ success: body.success,
1772
+ refundId,
1773
+ amountRefunded: body.amountRefunded ?? partialAmount ?? '0',
1774
+ originalNonce,
1775
+ transaction: body.transaction ?? null,
1776
+ originalMode: 'on_chain',
1777
+ reason,
1778
+ errorReason: body.errorReason ?? null,
1779
+ refundedAt: Date.now(),
1780
+ };
1781
+
1782
+ this.refundLedger.set(refundId, result);
1783
+
1784
+ if (body.success) {
1785
+ this.emit('payment:refund_completed', {
1786
+ nonce: originalNonce,
1787
+ payer: settlement.payer,
1788
+ amount: result.amountRefunded,
1789
+ transaction: body.transaction ?? null,
1790
+ network: settlement.network as SettlementChain,
1791
+ metadata: { reason, refundId },
1792
+ });
1793
+ } else {
1794
+ this.emit('payment:refund_failed', {
1795
+ nonce: originalNonce,
1796
+ metadata: { reason, errorReason: body.errorReason, refundId },
1797
+ });
1798
+ }
1799
+
1800
+ return result;
1801
+ } else {
1802
+ const errorText = await response.text().catch(() => response.statusText);
1803
+ const result: RefundResult = {
1804
+ success: false,
1805
+ refundId,
1806
+ amountRefunded: '0',
1807
+ originalNonce,
1808
+ transaction: null,
1809
+ originalMode: 'on_chain',
1810
+ reason,
1811
+ errorReason: `Facilitator returned ${response.status}: ${errorText}`,
1812
+ refundedAt: Date.now(),
1813
+ };
1814
+
1815
+ this.refundLedger.set(refundId, result);
1816
+
1817
+ this.emit('payment:refund_failed', {
1818
+ nonce: originalNonce,
1819
+ metadata: { reason, errorReason: result.errorReason, refundId },
1820
+ });
1821
+
1822
+ return result;
1823
+ }
1824
+ } catch (err) {
1825
+ const refundId = `refund_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1826
+ const result: RefundResult = {
1827
+ success: false,
1828
+ refundId,
1829
+ amountRefunded: '0',
1830
+ originalNonce,
1831
+ transaction: null,
1832
+ originalMode: 'on_chain',
1833
+ reason,
1834
+ errorReason: `Network error: ${err instanceof Error ? err.message : String(err)}`,
1835
+ refundedAt: Date.now(),
1836
+ };
1837
+
1838
+ this.refundLedger.set(refundId, result);
1839
+
1840
+ this.emit('payment:refund_failed', {
1841
+ nonce: originalNonce,
1842
+ metadata: { reason, errorReason: result.errorReason, refundId },
1843
+ });
1844
+
1845
+ return result;
1846
+ }
1847
+ }
1848
+ }
1849
+
1850
+ // Original payment not found or still pending
1851
+ const refundId = `refund_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1852
+ const result: RefundResult = {
1853
+ success: false,
1854
+ refundId,
1855
+ amountRefunded: '0',
1856
+ originalNonce,
1857
+ transaction: null,
1858
+ originalMode: 'in_memory',
1859
+ reason,
1860
+ errorReason:
1861
+ originalStatus === 'pending'
1862
+ ? 'Cannot refund: original payment still pending settlement'
1863
+ : 'Cannot refund: original payment not found',
1864
+ refundedAt: Date.now(),
1865
+ };
1866
+
1867
+ this.refundLedger.set(refundId, result);
1868
+
1869
+ this.emit('payment:refund_failed', {
1870
+ nonce: originalNonce,
1871
+ metadata: { reason, errorReason: result.errorReason, refundId },
1872
+ });
1873
+
1874
+ return result;
1875
+ }
1876
+
1877
+ // ===========================================================================
1878
+ // BATCH SETTLEMENT
1879
+ // ===========================================================================
1880
+
1881
+ /**
1882
+ * Run a batch settlement of accumulated micro-payments.
1883
+ */
1884
+ async runBatchSettlement(): Promise<{ settled: number; failed: number; totalVolume: number }> {
1885
+ this.emit('payment:batch_settlement_started', {
1886
+ metadata: {
1887
+ unsettledEntries: this.facilitator.getLedger().getStats().unsettledEntries,
1888
+ unsettledVolume: this.facilitator.getLedger().getStats().unsettledVolume,
1889
+ },
1890
+ });
1891
+
1892
+ const result = await this.facilitator.runBatchSettlement();
1893
+
1894
+ this.emit('payment:batch_settlement_completed', {
1895
+ metadata: {
1896
+ settled: result.settled,
1897
+ failed: result.failed,
1898
+ totalVolume: result.totalVolume,
1899
+ },
1900
+ });
1901
+
1902
+ return result;
1903
+ }
1904
+
1905
+ // ===========================================================================
1906
+ // QUERY / STATUS
1907
+ // ===========================================================================
1908
+
1909
+ /**
1910
+ * Get the underlying facilitator instance.
1911
+ */
1912
+ getFacilitator(): X402Facilitator {
1913
+ return this.facilitator;
1914
+ }
1915
+
1916
+ /**
1917
+ * Get chain ID for the configured settlement chain.
1918
+ */
1919
+ getChainId(): number {
1920
+ return CHAIN_IDS[this.config.chain] ?? 0;
1921
+ }
1922
+
1923
+ /**
1924
+ * Get the USDC contract address for the configured chain.
1925
+ */
1926
+ getUSDCContract(): string {
1927
+ return USDC_CONTRACTS[this.config.chain];
1928
+ }
1929
+
1930
+ /**
1931
+ * Look up a refund result by refund ID.
1932
+ */
1933
+ getRefund(refundId: string): RefundResult | undefined {
1934
+ return this.refundLedger.get(refundId);
1935
+ }
1936
+
1937
+ /**
1938
+ * Get all refund results.
1939
+ */
1940
+ getAllRefunds(): RefundResult[] {
1941
+ return Array.from(this.refundLedger.values());
1942
+ }
1943
+
1944
+ /**
1945
+ * Get comprehensive gateway statistics.
1946
+ */
1947
+ getStats(): {
1948
+ facilitator: ReturnType<X402Facilitator['getStats']>;
1949
+ chainId: number;
1950
+ usdcContract: string;
1951
+ totalRefunds: number;
1952
+ listenerCount: number;
1953
+ } {
1954
+ let listenerCount = 0;
1955
+ for (const listeners of this.listeners.values()) {
1956
+ listenerCount += listeners.size;
1957
+ }
1958
+
1959
+ return {
1960
+ facilitator: this.facilitator.getStats(),
1961
+ chainId: this.getChainId(),
1962
+ usdcContract: this.getUSDCContract(),
1963
+ totalRefunds: this.refundLedger.size,
1964
+ listenerCount,
1965
+ };
1966
+ }
1967
+
1968
+ /**
1969
+ * Clean up all resources.
1970
+ */
1971
+ dispose(): void {
1972
+ this.facilitator.dispose();
1973
+ this.listeners.clear();
1974
+ this.refundLedger.clear();
1975
+ this.eventCounter = 0;
1976
+ }
1977
+ }
1978
+