@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.
- package/ALL-test-results.json +1 -0
- package/CHANGELOG.md +8 -0
- package/LICENSE +21 -0
- package/ROADMAP.md +175 -0
- package/dist/AgentManifest-CB4xM-Ma.d.cts +704 -0
- package/dist/AgentManifest-CB4xM-Ma.d.ts +704 -0
- package/dist/BehaviorTree-BrBFECv5.d.cts +103 -0
- package/dist/BehaviorTree-BrBFECv5.d.ts +103 -0
- package/dist/InvisibleWallet-BB6tFvRA.d.cts +1732 -0
- package/dist/InvisibleWallet-rtRrBOA8.d.ts +1732 -0
- package/dist/OrchestratorAgent-BvWgf9uw.d.cts +798 -0
- package/dist/OrchestratorAgent-Q_CbVTmO.d.ts +798 -0
- package/dist/agents/index.cjs +4790 -0
- package/dist/agents/index.d.cts +1788 -0
- package/dist/agents/index.d.ts +1788 -0
- package/dist/agents/index.js +4695 -0
- package/dist/ai/index.cjs +5347 -0
- package/dist/ai/index.d.cts +1753 -0
- package/dist/ai/index.d.ts +1753 -0
- package/dist/ai/index.js +5244 -0
- package/dist/behavior.cjs +449 -0
- package/dist/behavior.d.cts +130 -0
- package/dist/behavior.d.ts +130 -0
- package/dist/behavior.js +407 -0
- package/dist/economy/index.cjs +3659 -0
- package/dist/economy/index.d.cts +747 -0
- package/dist/economy/index.d.ts +747 -0
- package/dist/economy/index.js +3617 -0
- package/dist/implementations-D9T3un9D.d.cts +236 -0
- package/dist/implementations-D9T3un9D.d.ts +236 -0
- package/dist/index.cjs +24550 -0
- package/dist/index.d.cts +1729 -0
- package/dist/index.d.ts +1729 -0
- package/dist/index.js +24277 -0
- package/dist/learning/index.cjs +219 -0
- package/dist/learning/index.d.cts +104 -0
- package/dist/learning/index.d.ts +104 -0
- package/dist/learning/index.js +189 -0
- package/dist/negotiation/index.cjs +970 -0
- package/dist/negotiation/index.d.cts +610 -0
- package/dist/negotiation/index.d.ts +610 -0
- package/dist/negotiation/index.js +931 -0
- package/dist/skills/index.cjs +1118 -0
- package/dist/skills/index.d.cts +289 -0
- package/dist/skills/index.d.ts +289 -0
- package/dist/skills/index.js +1079 -0
- package/dist/swarm/index.cjs +5268 -0
- package/dist/swarm/index.d.cts +2433 -0
- package/dist/swarm/index.d.ts +2433 -0
- package/dist/swarm/index.js +5221 -0
- package/dist/training/index.cjs +2745 -0
- package/dist/training/index.d.cts +1734 -0
- package/dist/training/index.d.ts +1734 -0
- package/dist/training/index.js +2687 -0
- package/extract-failures.js +10 -0
- package/package.json +82 -0
- package/src/__tests__/bounty-marketplace.test.ts +374 -0
- package/src/__tests__/delegation.test.ts +144 -0
- package/src/__tests__/distributed-claimer.test.ts +147 -0
- package/src/__tests__/done-log-audit.test.ts +342 -0
- package/src/__tests__/framework.test.ts +865 -0
- package/src/__tests__/goal-synthesizer.test.ts +236 -0
- package/src/__tests__/presence.test.ts +223 -0
- package/src/__tests__/protocol-agent.test.ts +254 -0
- package/src/__tests__/revenue-splitter.test.ts +114 -0
- package/src/__tests__/scenario-driven-todo.test.ts +197 -0
- package/src/__tests__/self-improve.test.ts +349 -0
- package/src/__tests__/service-lifecycle.test.ts +237 -0
- package/src/__tests__/skill-router.test.ts +121 -0
- package/src/agents/AgentManifest.ts +493 -0
- package/src/agents/AgentRegistry.ts +475 -0
- package/src/agents/AgentTypes.ts +585 -0
- package/src/agents/AgentWalletRegistry.ts +83 -0
- package/src/agents/AuthenticatedCRDT.ts +388 -0
- package/src/agents/CapabilityMatcher.ts +453 -0
- package/src/agents/CrossRealityHandoff.ts +305 -0
- package/src/agents/CulturalMemory.ts +454 -0
- package/src/agents/FederatedRegistryAdapter.ts +429 -0
- package/src/agents/NormEngine.ts +450 -0
- package/src/agents/OrchestratorAgent.ts +414 -0
- package/src/agents/SkillWorkflowEngine.ts +472 -0
- package/src/agents/TaskDelegationService.ts +551 -0
- package/src/agents/__tests__/AgentManifest.prod.test.ts +134 -0
- package/src/agents/__tests__/AgentManifest.test.ts +182 -0
- package/src/agents/__tests__/AgentModule.test.ts +864 -0
- package/src/agents/__tests__/AgentRegistry.prod.test.ts +125 -0
- package/src/agents/__tests__/AgentRegistry.test.ts +148 -0
- package/src/agents/__tests__/AgentTypes.test.ts +534 -0
- package/src/agents/__tests__/AgentWalletRegistry.test.ts +152 -0
- package/src/agents/__tests__/AuthenticatedCRDT.test.ts +558 -0
- package/src/agents/__tests__/CapabilityMatcher.prod.test.ts +117 -0
- package/src/agents/__tests__/CapabilityMatcher.test.ts +178 -0
- package/src/agents/__tests__/CrossRealityHandoff.test.ts +402 -0
- package/src/agents/__tests__/CulturalMemory.test.ts +200 -0
- package/src/agents/__tests__/FederatedRegistryAdapter.test.ts +409 -0
- package/src/agents/__tests__/NormEngine.test.ts +276 -0
- package/src/agents/__tests__/OrchestratorAgent.test.ts +182 -0
- package/src/agents/__tests__/SkillWorkflowEngine.test.ts +357 -0
- package/src/agents/__tests__/TaskDelegationService.test.ts +446 -0
- package/src/agents/index.ts +107 -0
- package/src/agents/spatial-comms/Layer1RealTime.ts +621 -0
- package/src/agents/spatial-comms/Layer2A2A.ts +661 -0
- package/src/agents/spatial-comms/Layer3MCP.ts +651 -0
- package/src/agents/spatial-comms/ProtocolTypes.ts +543 -0
- package/src/agents/spatial-comms/SpatialCommClient.ts +483 -0
- package/src/agents/spatial-comms/__tests__/performance-benchmark.test.ts +465 -0
- package/src/agents/spatial-comms/examples/multi-agent-world-creation.ts +409 -0
- package/src/agents/spatial-comms/index.ts +66 -0
- package/src/ai/AIAdapter.ts +313 -0
- package/src/ai/AICopilot.ts +331 -0
- package/src/ai/AIOutputValidator.ts +203 -0
- package/src/ai/BTNodes.ts +239 -0
- package/src/ai/BehaviorSelector.ts +135 -0
- package/src/ai/BehaviorTree.ts +153 -0
- package/src/ai/Blackboard.ts +165 -0
- package/src/ai/GenerationAnalytics.ts +461 -0
- package/src/ai/GenerationCache.ts +265 -0
- package/src/ai/GoalPlanner.ts +165 -0
- package/src/ai/HoloScriptGenerator.ts +580 -0
- package/src/ai/InfluenceMap.ts +180 -0
- package/src/ai/NavMesh.ts +168 -0
- package/src/ai/PerceptionSystem.ts +178 -0
- package/src/ai/PromptTemplates.ts +453 -0
- package/src/ai/SemanticSearchService.ts +80 -0
- package/src/ai/StateMachine.ts +196 -0
- package/src/ai/SteeringBehavior.ts +150 -0
- package/src/ai/SteeringBehaviors.ts +244 -0
- package/src/ai/TrainingDataGenerator.ts +1082 -0
- package/src/ai/UtilityAI.ts +145 -0
- package/src/ai/__tests__/AIAdapter.prod.test.ts +259 -0
- package/src/ai/__tests__/AIAdapter.test.ts +109 -0
- package/src/ai/__tests__/AICopilot.prod.test.ts +341 -0
- package/src/ai/__tests__/AICopilot.test.ts +178 -0
- package/src/ai/__tests__/AIOutputValidator.prod.test.ts +226 -0
- package/src/ai/__tests__/AIOutputValidator.test.ts +138 -0
- package/src/ai/__tests__/BTNodes.prod.test.ts +391 -0
- package/src/ai/__tests__/BTNodes.test.ts +263 -0
- package/src/ai/__tests__/BehaviorSelector.prod.test.ts +129 -0
- package/src/ai/__tests__/BehaviorSelector.test.ts +132 -0
- package/src/ai/__tests__/BehaviorTree.prod.test.ts +266 -0
- package/src/ai/__tests__/BehaviorTree.test.ts +216 -0
- package/src/ai/__tests__/Blackboard.prod.test.ts +339 -0
- package/src/ai/__tests__/Blackboard.test.ts +183 -0
- package/src/ai/__tests__/GenerationAnalytics.prod.test.ts +141 -0
- package/src/ai/__tests__/GenerationAnalytics.test.ts +165 -0
- package/src/ai/__tests__/GenerationCache.prod.test.ts +144 -0
- package/src/ai/__tests__/GenerationCache.test.ts +171 -0
- package/src/ai/__tests__/GoalPlanner.prod.test.ts +189 -0
- package/src/ai/__tests__/GoalPlanner.test.ts +137 -0
- package/src/ai/__tests__/GoalPlannerDepth.prod.test.ts +217 -0
- package/src/ai/__tests__/HoloScriptGenerator.test.ts +125 -0
- package/src/ai/__tests__/InfluenceMap.prod.test.ts +146 -0
- package/src/ai/__tests__/InfluenceMap.test.ts +149 -0
- package/src/ai/__tests__/NavMesh.prod.test.ts +141 -0
- package/src/ai/__tests__/NavMesh.test.ts +159 -0
- package/src/ai/__tests__/PerceptionSystem.prod.test.ts +135 -0
- package/src/ai/__tests__/PerceptionSystem.test.ts +250 -0
- package/src/ai/__tests__/PromptTemplates.prod.test.ts +313 -0
- package/src/ai/__tests__/PromptTemplates.test.ts +146 -0
- package/src/ai/__tests__/SemanticSearch.test.ts +37 -0
- package/src/ai/__tests__/StateMachine.prod.test.ts +162 -0
- package/src/ai/__tests__/StateMachine.test.ts +163 -0
- package/src/ai/__tests__/SteeringBehavior.prod.test.ts +251 -0
- package/src/ai/__tests__/SteeringBehavior.test.ts +135 -0
- package/src/ai/__tests__/SteeringBehaviors.prod.test.ts +133 -0
- package/src/ai/__tests__/SteeringBehaviors.test.ts +151 -0
- package/src/ai/__tests__/TrainingDataGenerator.prod.test.ts +286 -0
- package/src/ai/__tests__/TrainingDataGenerator.test.ts +286 -0
- package/src/ai/__tests__/UtilityAI.prod.test.ts +207 -0
- package/src/ai/__tests__/UtilityAI.test.ts +155 -0
- package/src/ai/__tests__/adapters.prod.test.ts +263 -0
- package/src/ai/__tests__/adapters.test.ts +320 -0
- package/src/ai/adapters.ts +1585 -0
- package/src/ai/index.ts +130 -0
- package/src/behavior/BehaviorPresets.ts +140 -0
- package/src/behavior/BehaviorTree.ts +236 -0
- package/src/behavior/StateMachine.ts +176 -0
- package/src/behavior/StateTrait.ts +67 -0
- package/src/behavior/index.ts +8 -0
- package/src/behavior.ts +8 -0
- package/src/board/audit.ts +284 -0
- package/src/board/board-ops.ts +336 -0
- package/src/board/board-types.ts +302 -0
- package/src/board/index.ts +69 -0
- package/src/define-agent.ts +46 -0
- package/src/define-team.ts +33 -0
- package/src/delegation.ts +265 -0
- package/src/distributed-claimer.ts +228 -0
- package/src/economy/AgentBudgetEnforcer.ts +464 -0
- package/src/economy/BountyManager.ts +185 -0
- package/src/economy/CreatorRevenueAggregator.ts +460 -0
- package/src/economy/InvisibleWallet.ts +82 -0
- package/src/economy/KnowledgeMarketplace.ts +193 -0
- package/src/economy/PaymentWebhookService.ts +512 -0
- package/src/economy/RevenueSplitter.ts +156 -0
- package/src/economy/SubscriptionManager.ts +546 -0
- package/src/economy/UnifiedBudgetOptimizer.ts +635 -0
- package/src/economy/UsageMeter.ts +440 -0
- package/src/economy/_core-stubs.ts +219 -0
- package/src/economy/index.ts +100 -0
- package/src/economy/x402-facilitator.ts +1978 -0
- package/src/index.ts +348 -0
- package/src/knowledge/__tests__/knowledge-consolidator.test.ts +444 -0
- package/src/knowledge/__tests__/knowledge-store-vector.test.ts +291 -0
- package/src/knowledge/brain.ts +167 -0
- package/src/knowledge/consolidation.ts +581 -0
- package/src/knowledge/knowledge-consolidator.ts +510 -0
- package/src/knowledge/knowledge-store.ts +616 -0
- package/src/learning/MemoryConsolidator.ts +102 -0
- package/src/learning/MemoryScorer.ts +69 -0
- package/src/learning/ProceduralCompiler.ts +45 -0
- package/src/learning/SemanticClusterer.ts +66 -0
- package/src/learning/index.ts +8 -0
- package/src/llm/llm-adapter.ts +159 -0
- package/src/mesh/index.ts +309 -0
- package/src/negotiation/NegotiationProtocol.ts +694 -0
- package/src/negotiation/NegotiationTypes.ts +473 -0
- package/src/negotiation/VotingMechanisms.ts +691 -0
- package/src/negotiation/index.ts +49 -0
- package/src/protocol/goal-synthesizer.ts +317 -0
- package/src/protocol/implementations.ts +474 -0
- package/src/protocol/micro-phase-decomposer.ts +299 -0
- package/src/protocol/micro-step-decomposer.test.ts +306 -0
- package/src/protocol-agent.test.ts +353 -0
- package/src/protocol-agent.ts +670 -0
- package/src/self-improve/absorb-scanner.ts +252 -0
- package/src/self-improve/evolution-engine.ts +149 -0
- package/src/self-improve/framework-absorber.ts +214 -0
- package/src/self-improve/index.ts +50 -0
- package/src/self-improve/prompt-optimizer.ts +212 -0
- package/src/self-improve/test-generator.ts +175 -0
- package/src/skill-router.ts +186 -0
- package/src/skills/index.ts +5 -0
- package/src/skills/skill-md-bridge.ts +1699 -0
- package/src/swarm/ACOEngine.ts +261 -0
- package/src/swarm/CollectiveIntelligence.ts +383 -0
- package/src/swarm/ContributionSynthesizer.ts +481 -0
- package/src/swarm/LeaderElection.ts +393 -0
- package/src/swarm/PSOEngine.ts +206 -0
- package/src/swarm/QuorumPolicy.ts +173 -0
- package/src/swarm/SwarmCoordinator.ts +335 -0
- package/src/swarm/SwarmManager.ts +442 -0
- package/src/swarm/SwarmMembership.ts +456 -0
- package/src/swarm/VotingRound.ts +255 -0
- package/src/swarm/__tests__/ACOEngine.prod.test.ts +164 -0
- package/src/swarm/__tests__/ACOEngine.test.ts +117 -0
- package/src/swarm/__tests__/CollectiveIntelligence.prod.test.ts +296 -0
- package/src/swarm/__tests__/CollectiveIntelligence.test.ts +457 -0
- package/src/swarm/__tests__/ContributionSynthesizer.prod.test.ts +269 -0
- package/src/swarm/__tests__/ContributionSynthesizer.test.ts +254 -0
- package/src/swarm/__tests__/LeaderElection.prod.test.ts +196 -0
- package/src/swarm/__tests__/LeaderElection.test.ts +151 -0
- package/src/swarm/__tests__/PSOEngine.prod.test.ts +162 -0
- package/src/swarm/__tests__/PSOEngine.test.ts +106 -0
- package/src/swarm/__tests__/QuorumPolicy.prod.test.ts +216 -0
- package/src/swarm/__tests__/QuorumPolicy.test.ts +177 -0
- package/src/swarm/__tests__/SwarmCoordinator.prod.test.ts +186 -0
- package/src/swarm/__tests__/SwarmCoordinator.test.ts +167 -0
- package/src/swarm/__tests__/SwarmManager.prod.test.ts +308 -0
- package/src/swarm/__tests__/SwarmManager.test.ts +373 -0
- package/src/swarm/__tests__/SwarmMembership.prod.test.ts +273 -0
- package/src/swarm/__tests__/SwarmMembership.test.ts +264 -0
- package/src/swarm/__tests__/VotingRound.prod.test.ts +233 -0
- package/src/swarm/__tests__/VotingRound.test.ts +174 -0
- package/src/swarm/analytics/SwarmInspector.ts +476 -0
- package/src/swarm/analytics/SwarmMetrics.ts +449 -0
- package/src/swarm/analytics/__tests__/SwarmInspector.prod.test.ts +366 -0
- package/src/swarm/analytics/__tests__/SwarmInspector.test.ts +454 -0
- package/src/swarm/analytics/__tests__/SwarmMetrics.prod.test.ts +254 -0
- package/src/swarm/analytics/__tests__/SwarmMetrics.test.ts +370 -0
- package/src/swarm/analytics/index.ts +7 -0
- package/src/swarm/index.ts +69 -0
- package/src/swarm/messaging/BroadcastChannel.ts +509 -0
- package/src/swarm/messaging/GossipProtocol.ts +565 -0
- package/src/swarm/messaging/SwarmEventBus.ts +443 -0
- package/src/swarm/messaging/__tests__/BroadcastChannel.prod.test.ts +331 -0
- package/src/swarm/messaging/__tests__/BroadcastChannel.test.ts +333 -0
- package/src/swarm/messaging/__tests__/GossipProtocol.prod.test.ts +356 -0
- package/src/swarm/messaging/__tests__/GossipProtocol.test.ts +437 -0
- package/src/swarm/messaging/__tests__/SwarmEventBus.prod.test.ts +191 -0
- package/src/swarm/messaging/__tests__/SwarmEventBus.test.ts +247 -0
- package/src/swarm/messaging/index.ts +8 -0
- package/src/swarm/spatial/FlockingBehavior.ts +462 -0
- package/src/swarm/spatial/FormationController.ts +500 -0
- package/src/swarm/spatial/Vector3.ts +170 -0
- package/src/swarm/spatial/ZoneClaiming.ts +509 -0
- package/src/swarm/spatial/__tests__/FlockingBehavior.prod.test.ts +239 -0
- package/src/swarm/spatial/__tests__/FlockingBehavior.test.ts +298 -0
- package/src/swarm/spatial/__tests__/FormationController.prod.test.ts +240 -0
- package/src/swarm/spatial/__tests__/FormationController.test.ts +297 -0
- package/src/swarm/spatial/__tests__/Vector3.prod.test.ts +283 -0
- package/src/swarm/spatial/__tests__/Vector3.test.ts +224 -0
- package/src/swarm/spatial/__tests__/ZoneClaiming.prod.test.ts +246 -0
- package/src/swarm/spatial/__tests__/ZoneClaiming.test.ts +374 -0
- package/src/swarm/spatial/index.ts +28 -0
- package/src/team.ts +1245 -0
- package/src/training/LRScheduler.ts +377 -0
- package/src/training/QualityScoringPipeline.ts +139 -0
- package/src/training/SoftDedup.ts +461 -0
- package/src/training/SparsityMonitor.ts +685 -0
- package/src/training/SparsityMonitorTypes.ts +209 -0
- package/src/training/SpatialTrainingDataGenerator.ts +1526 -0
- package/src/training/SpatialTrainingDataTypes.ts +216 -0
- package/src/training/TrainingPipelineConfig.ts +215 -0
- package/src/training/constants.ts +94 -0
- package/src/training/index.ts +138 -0
- package/src/training/schema.ts +147 -0
- package/src/training/scripts/generate-novel-use-cases-dataset.ts +272 -0
- package/src/training/scripts/generate-spatial-dataset.ts +521 -0
- package/src/training/training/data/novel-use-cases.jsonl +153 -0
- package/src/training/training/data/spatial-reasoning-10k.jsonl +9354 -0
- package/src/training/trainingmonkey/TrainingMonkeyIntegration.ts +477 -0
- package/src/training/trainingmonkey/TrainingMonkeyTypes.ts +230 -0
- package/src/training/trainingmonkey/index.ts +26 -0
- package/src/training/trait-mappings.ts +157 -0
- package/src/types/core-stubs.d.ts +113 -0
- package/src/types.ts +304 -0
- package/test-output.txt +0 -0
- package/test-result.json +1 -0
- package/tsc-errors.txt +4 -0
- package/tsc_output.txt +0 -0
- package/tsconfig.json +14 -0
- package/tsup-learning-esm.config.ts +12 -0
- package/tsup.config.ts +21 -0
- package/typescript-errors-2.txt +0 -0
- package/typescript-errors.txt +22 -0
- package/vitest-log-utf8.txt +268 -0
- package/vitest-log.txt +0 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,3659 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/economy/index.ts
|
|
21
|
+
var economy_exports = {};
|
|
22
|
+
__export(economy_exports, {
|
|
23
|
+
AgentBudgetEnforcer: () => AgentBudgetEnforcer,
|
|
24
|
+
BountyManager: () => BountyManager,
|
|
25
|
+
CHAIN_IDS: () => CHAIN_IDS,
|
|
26
|
+
CHAIN_ID_TO_NETWORK: () => CHAIN_ID_TO_NETWORK,
|
|
27
|
+
CreatorRevenueAggregator: () => CreatorRevenueAggregator,
|
|
28
|
+
DEFAULT_COST_FLOOR: () => DEFAULT_COST_FLOOR,
|
|
29
|
+
DEFAULT_LOD_SCALING: () => DEFAULT_LOD_SCALING,
|
|
30
|
+
DEFAULT_TRAIT_UTILITIES: () => DEFAULT_TRAIT_UTILITIES,
|
|
31
|
+
InvisibleWalletStub: () => InvisibleWalletStub,
|
|
32
|
+
KnowledgeMarketplace: () => KnowledgeMarketplace,
|
|
33
|
+
MICRO_PAYMENT_THRESHOLD: () => MICRO_PAYMENT_THRESHOLD,
|
|
34
|
+
MicroPaymentLedger: () => MicroPaymentLedger,
|
|
35
|
+
PLATFORM_LOD_SCALING: () => PLATFORM_LOD_SCALING,
|
|
36
|
+
PaymentGateway: () => PaymentGateway,
|
|
37
|
+
PaymentWebhookService: () => PaymentWebhookService,
|
|
38
|
+
RevenueSplitter: () => RevenueSplitter,
|
|
39
|
+
SubscriptionManager: () => SubscriptionManager,
|
|
40
|
+
USDC_CONTRACTS: () => USDC_CONTRACTS,
|
|
41
|
+
UnifiedBudgetOptimizer: () => UnifiedBudgetOptimizer,
|
|
42
|
+
UsageMeter: () => UsageMeter,
|
|
43
|
+
X402Facilitator: () => X402Facilitator,
|
|
44
|
+
X402_VERSION: () => X402_VERSION,
|
|
45
|
+
creditTraitHandler: () => creditTraitHandler
|
|
46
|
+
});
|
|
47
|
+
module.exports = __toCommonJS(economy_exports);
|
|
48
|
+
|
|
49
|
+
// src/economy/x402-facilitator.ts
|
|
50
|
+
var X402_VERSION = 1;
|
|
51
|
+
var USDC_CONTRACTS = {
|
|
52
|
+
base: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
53
|
+
"base-sepolia": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
54
|
+
solana: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
55
|
+
"solana-devnet": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"
|
|
56
|
+
};
|
|
57
|
+
var MICRO_PAYMENT_THRESHOLD = 1e5;
|
|
58
|
+
var MicroPaymentLedger = class {
|
|
59
|
+
entries = [];
|
|
60
|
+
balances = /* @__PURE__ */ new Map();
|
|
61
|
+
txCounter = 0;
|
|
62
|
+
maxEntries;
|
|
63
|
+
constructor(maxEntries = 1e4) {
|
|
64
|
+
this.maxEntries = maxEntries;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Record a micro-payment in the in-memory ledger.
|
|
68
|
+
*/
|
|
69
|
+
record(from, to, amount, resource) {
|
|
70
|
+
const entry = {
|
|
71
|
+
id: `micro_${Date.now()}_${this.txCounter++}`,
|
|
72
|
+
from,
|
|
73
|
+
to,
|
|
74
|
+
amount,
|
|
75
|
+
resource,
|
|
76
|
+
timestamp: Date.now(),
|
|
77
|
+
settled: false,
|
|
78
|
+
settlementTx: null
|
|
79
|
+
};
|
|
80
|
+
this.entries.push(entry);
|
|
81
|
+
const fromBalance = this.balances.get(from) ?? 0;
|
|
82
|
+
this.balances.set(from, fromBalance - amount);
|
|
83
|
+
const toBalance = this.balances.get(to) ?? 0;
|
|
84
|
+
this.balances.set(to, toBalance + amount);
|
|
85
|
+
if (this.entries.length > this.maxEntries) {
|
|
86
|
+
this.entries = this.entries.slice(-this.maxEntries);
|
|
87
|
+
}
|
|
88
|
+
return entry;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Get unsettled entries for batch settlement.
|
|
92
|
+
*/
|
|
93
|
+
getUnsettled() {
|
|
94
|
+
return this.entries.filter((e) => !e.settled);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Mark entries as settled after batch on-chain settlement.
|
|
98
|
+
*/
|
|
99
|
+
markSettled(entryIds, txHash) {
|
|
100
|
+
for (const entry of this.entries) {
|
|
101
|
+
if (entryIds.includes(entry.id)) {
|
|
102
|
+
entry.settled = true;
|
|
103
|
+
entry.settlementTx = txHash;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Get the net balance for an address (can be negative = owes).
|
|
109
|
+
*/
|
|
110
|
+
getBalance(address) {
|
|
111
|
+
return this.balances.get(address) ?? 0;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Get total unsettled volume.
|
|
115
|
+
*/
|
|
116
|
+
getUnsettledVolume() {
|
|
117
|
+
return this.getUnsettled().reduce((sum, e) => sum + e.amount, 0);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Get all entries for a specific payer.
|
|
121
|
+
*/
|
|
122
|
+
getEntriesForPayer(from) {
|
|
123
|
+
return this.entries.filter((e) => e.from === from);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Get ledger statistics.
|
|
127
|
+
*/
|
|
128
|
+
getStats() {
|
|
129
|
+
const unsettled = this.getUnsettled();
|
|
130
|
+
const payers = new Set(this.entries.map((e) => e.from));
|
|
131
|
+
const recipients = new Set(this.entries.map((e) => e.to));
|
|
132
|
+
return {
|
|
133
|
+
totalEntries: this.entries.length,
|
|
134
|
+
unsettledEntries: unsettled.length,
|
|
135
|
+
unsettledVolume: unsettled.reduce((sum, e) => sum + e.amount, 0),
|
|
136
|
+
uniquePayers: payers.size,
|
|
137
|
+
uniqueRecipients: recipients.size
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Clear all settled entries (garbage collection).
|
|
142
|
+
*/
|
|
143
|
+
pruneSettled() {
|
|
144
|
+
const before = this.entries.length;
|
|
145
|
+
this.entries = this.entries.filter((e) => !e.settled);
|
|
146
|
+
return before - this.entries.length;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Reset the entire ledger.
|
|
150
|
+
*/
|
|
151
|
+
reset() {
|
|
152
|
+
this.entries = [];
|
|
153
|
+
this.balances.clear();
|
|
154
|
+
this.txCounter = 0;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
var X402Facilitator = class {
|
|
158
|
+
config;
|
|
159
|
+
ledger;
|
|
160
|
+
usedNonces = /* @__PURE__ */ new Set();
|
|
161
|
+
pendingSettlements = /* @__PURE__ */ new Map();
|
|
162
|
+
settlementResults = /* @__PURE__ */ new Map();
|
|
163
|
+
batchSettlementTimer = null;
|
|
164
|
+
constructor(config) {
|
|
165
|
+
this.config = {
|
|
166
|
+
recipientAddress: config.recipientAddress,
|
|
167
|
+
chain: config.chain,
|
|
168
|
+
secondaryChain: config.secondaryChain ?? config.chain,
|
|
169
|
+
microPaymentThreshold: config.microPaymentThreshold ?? MICRO_PAYMENT_THRESHOLD,
|
|
170
|
+
maxTimeoutSeconds: config.maxTimeoutSeconds ?? 60,
|
|
171
|
+
optimisticExecution: config.optimisticExecution ?? true,
|
|
172
|
+
batchSettlementIntervalMs: config.batchSettlementIntervalMs ?? 3e5,
|
|
173
|
+
// 5 min
|
|
174
|
+
maxLedgerEntries: config.maxLedgerEntries ?? 1e4,
|
|
175
|
+
facilitatorUrl: config.facilitatorUrl ?? "https://x402.org/facilitator",
|
|
176
|
+
resourceDescription: config.resourceDescription ?? "HoloScript premium resource"
|
|
177
|
+
};
|
|
178
|
+
this.ledger = new MicroPaymentLedger(this.config.maxLedgerEntries);
|
|
179
|
+
}
|
|
180
|
+
// ===========================================================================
|
|
181
|
+
// PAYMENT REQUIRED RESPONSE GENERATION
|
|
182
|
+
// ===========================================================================
|
|
183
|
+
/**
|
|
184
|
+
* Generate an HTTP 402 PaymentRequired response body.
|
|
185
|
+
*
|
|
186
|
+
* @param resource - The resource path being requested (e.g., "/api/scene/premium")
|
|
187
|
+
* @param amountUSDC - Price in USDC (human-readable, e.g., 0.05 for 5 cents)
|
|
188
|
+
* @param description - Human-readable description of what is being paid for
|
|
189
|
+
* @returns PaymentRequired response body conforming to x402 spec
|
|
190
|
+
*/
|
|
191
|
+
createPaymentRequired(resource, amountUSDC, description) {
|
|
192
|
+
const baseUnits = Math.round(amountUSDC * 1e6).toString();
|
|
193
|
+
const desc = description ?? this.config.resourceDescription;
|
|
194
|
+
const accepts = [
|
|
195
|
+
{
|
|
196
|
+
scheme: "exact",
|
|
197
|
+
network: this.config.chain,
|
|
198
|
+
maxAmountRequired: baseUnits,
|
|
199
|
+
resource,
|
|
200
|
+
description: desc,
|
|
201
|
+
payTo: this.config.recipientAddress,
|
|
202
|
+
asset: USDC_CONTRACTS[this.config.chain],
|
|
203
|
+
maxTimeoutSeconds: this.config.maxTimeoutSeconds
|
|
204
|
+
}
|
|
205
|
+
];
|
|
206
|
+
if (this.config.secondaryChain !== this.config.chain) {
|
|
207
|
+
accepts.push({
|
|
208
|
+
scheme: "exact",
|
|
209
|
+
network: this.config.secondaryChain,
|
|
210
|
+
maxAmountRequired: baseUnits,
|
|
211
|
+
resource,
|
|
212
|
+
description: desc,
|
|
213
|
+
payTo: this.config.recipientAddress,
|
|
214
|
+
asset: USDC_CONTRACTS[this.config.secondaryChain],
|
|
215
|
+
maxTimeoutSeconds: this.config.maxTimeoutSeconds
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
x402Version: X402_VERSION,
|
|
220
|
+
accepts,
|
|
221
|
+
error: "X-PAYMENT header is required"
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
// ===========================================================================
|
|
225
|
+
// PAYMENT VERIFICATION
|
|
226
|
+
// ===========================================================================
|
|
227
|
+
/**
|
|
228
|
+
* Verify an X-PAYMENT header payload.
|
|
229
|
+
*
|
|
230
|
+
* Checks:
|
|
231
|
+
* 1. Protocol version matches
|
|
232
|
+
* 2. Nonce has not been used (replay protection)
|
|
233
|
+
* 3. Authorization window is valid (validAfter <= now <= validBefore)
|
|
234
|
+
* 4. Payment amount matches or exceeds required amount
|
|
235
|
+
* 5. Recipient matches configured address
|
|
236
|
+
* 6. Network is supported
|
|
237
|
+
*
|
|
238
|
+
* NOTE: Signature cryptographic verification requires on-chain verification
|
|
239
|
+
* via the facilitator service. This method validates the structural/temporal
|
|
240
|
+
* aspects. For full verification including signature, use verifyAndSettle().
|
|
241
|
+
*
|
|
242
|
+
* @param payment - Decoded X-PAYMENT payload
|
|
243
|
+
* @param requiredAmount - Minimum amount in USDC base units
|
|
244
|
+
* @returns Verification result
|
|
245
|
+
*/
|
|
246
|
+
verifyPayment(payment, requiredAmount) {
|
|
247
|
+
if (payment.x402Version !== X402_VERSION) {
|
|
248
|
+
return { isValid: false, invalidReason: `Unsupported x402 version: ${payment.x402Version}` };
|
|
249
|
+
}
|
|
250
|
+
if (payment.scheme !== "exact") {
|
|
251
|
+
return { isValid: false, invalidReason: `Unsupported scheme: ${payment.scheme}` };
|
|
252
|
+
}
|
|
253
|
+
if (!USDC_CONTRACTS[payment.network]) {
|
|
254
|
+
return { isValid: false, invalidReason: `Unsupported network: ${payment.network}` };
|
|
255
|
+
}
|
|
256
|
+
const nonce = payment.payload.authorization.nonce;
|
|
257
|
+
if (this.usedNonces.has(nonce)) {
|
|
258
|
+
return { isValid: false, invalidReason: "Nonce already used (replay attack prevented)" };
|
|
259
|
+
}
|
|
260
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
261
|
+
const validAfter = parseInt(payment.payload.authorization.validAfter, 10);
|
|
262
|
+
const validBefore = parseInt(payment.payload.authorization.validBefore, 10);
|
|
263
|
+
if (now < validAfter) {
|
|
264
|
+
return {
|
|
265
|
+
isValid: false,
|
|
266
|
+
invalidReason: "Authorization not yet valid (validAfter in future)"
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
if (now > validBefore) {
|
|
270
|
+
return { isValid: false, invalidReason: "Authorization expired (validBefore in past)" };
|
|
271
|
+
}
|
|
272
|
+
const paymentAmount = BigInt(payment.payload.authorization.value);
|
|
273
|
+
const required = BigInt(requiredAmount);
|
|
274
|
+
if (paymentAmount < required) {
|
|
275
|
+
return {
|
|
276
|
+
isValid: false,
|
|
277
|
+
invalidReason: `Insufficient payment: ${paymentAmount} < ${required}`
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
const payTo = payment.payload.authorization.to.toLowerCase();
|
|
281
|
+
const configRecipient = this.config.recipientAddress.toLowerCase();
|
|
282
|
+
if (payTo !== configRecipient) {
|
|
283
|
+
return {
|
|
284
|
+
isValid: false,
|
|
285
|
+
invalidReason: `Recipient mismatch: ${payTo} !== ${configRecipient}`
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
if (!payment.payload.signature || payment.payload.signature.length < 10) {
|
|
289
|
+
return { isValid: false, invalidReason: "Missing or invalid signature" };
|
|
290
|
+
}
|
|
291
|
+
return { isValid: true, invalidReason: null };
|
|
292
|
+
}
|
|
293
|
+
// ===========================================================================
|
|
294
|
+
// DUAL-MODE SETTLEMENT
|
|
295
|
+
// ===========================================================================
|
|
296
|
+
/**
|
|
297
|
+
* Determine the settlement mode based on the payment amount.
|
|
298
|
+
*
|
|
299
|
+
* @param amountBaseUnits - Amount in USDC base units (6 decimals)
|
|
300
|
+
* @returns Settlement mode
|
|
301
|
+
*/
|
|
302
|
+
getSettlementMode(amountBaseUnits) {
|
|
303
|
+
return amountBaseUnits < this.config.microPaymentThreshold ? "in_memory" : "on_chain";
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Process a payment with dual-mode settlement.
|
|
307
|
+
*
|
|
308
|
+
* For micro-payments (< threshold):
|
|
309
|
+
* - Records in in-memory ledger immediately
|
|
310
|
+
* - Returns instant success
|
|
311
|
+
* - Batch settles periodically
|
|
312
|
+
*
|
|
313
|
+
* For macro-payments (>= threshold):
|
|
314
|
+
* - If optimistic execution enabled: grants access immediately, settles async
|
|
315
|
+
* - If not: waits for settlement before granting access
|
|
316
|
+
*
|
|
317
|
+
* @param payment - Decoded X-PAYMENT payload
|
|
318
|
+
* @param resource - Resource being accessed
|
|
319
|
+
* @param requiredAmount - Required amount in USDC base units
|
|
320
|
+
* @returns Settlement result
|
|
321
|
+
*/
|
|
322
|
+
async processPayment(payment, resource, requiredAmount) {
|
|
323
|
+
const verification = this.verifyPayment(payment, requiredAmount);
|
|
324
|
+
if (!verification.isValid) {
|
|
325
|
+
return {
|
|
326
|
+
success: false,
|
|
327
|
+
transaction: null,
|
|
328
|
+
network: payment.network,
|
|
329
|
+
payer: payment.payload.authorization.from,
|
|
330
|
+
errorReason: verification.invalidReason,
|
|
331
|
+
mode: "on_chain",
|
|
332
|
+
settledAt: Date.now()
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
this.usedNonces.add(payment.payload.authorization.nonce);
|
|
336
|
+
const amount = parseInt(payment.payload.authorization.value, 10);
|
|
337
|
+
const mode = this.getSettlementMode(amount);
|
|
338
|
+
if (mode === "in_memory") {
|
|
339
|
+
return this.settleMicroPayment(payment, resource, amount);
|
|
340
|
+
} else {
|
|
341
|
+
return this.settleOnChain(payment, resource, requiredAmount);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Settle a micro-payment in the in-memory ledger.
|
|
346
|
+
*/
|
|
347
|
+
settleMicroPayment(payment, resource, amount) {
|
|
348
|
+
const entry = this.ledger.record(
|
|
349
|
+
payment.payload.authorization.from,
|
|
350
|
+
payment.payload.authorization.to,
|
|
351
|
+
amount,
|
|
352
|
+
resource
|
|
353
|
+
);
|
|
354
|
+
return {
|
|
355
|
+
success: true,
|
|
356
|
+
transaction: entry.id,
|
|
357
|
+
// In-memory tx ID
|
|
358
|
+
network: "in_memory",
|
|
359
|
+
payer: payment.payload.authorization.from,
|
|
360
|
+
errorReason: null,
|
|
361
|
+
mode: "in_memory",
|
|
362
|
+
settledAt: Date.now()
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Settle a payment on-chain via the facilitator service.
|
|
367
|
+
*
|
|
368
|
+
* In optimistic mode: returns success immediately, verifies async.
|
|
369
|
+
* In non-optimistic mode: waits for facilitator confirmation.
|
|
370
|
+
*/
|
|
371
|
+
async settleOnChain(payment, _resource, _requiredAmount) {
|
|
372
|
+
const payer = payment.payload.authorization.from;
|
|
373
|
+
const nonce = payment.payload.authorization.nonce;
|
|
374
|
+
if (this.config.optimisticExecution) {
|
|
375
|
+
this.pendingSettlements.set(nonce, payment);
|
|
376
|
+
this.verifySettlementAsync(payment).catch((err) => {
|
|
377
|
+
console.error("[x402] Async settlement verification failed:", err);
|
|
378
|
+
this.settlementResults.set(nonce, {
|
|
379
|
+
success: false,
|
|
380
|
+
transaction: null,
|
|
381
|
+
network: payment.network,
|
|
382
|
+
payer,
|
|
383
|
+
errorReason: `Async verification failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
384
|
+
mode: "on_chain",
|
|
385
|
+
settledAt: Date.now()
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
return {
|
|
389
|
+
success: true,
|
|
390
|
+
transaction: `pending_${nonce}`,
|
|
391
|
+
network: payment.network,
|
|
392
|
+
payer,
|
|
393
|
+
errorReason: null,
|
|
394
|
+
mode: "on_chain",
|
|
395
|
+
settledAt: Date.now()
|
|
396
|
+
};
|
|
397
|
+
} else {
|
|
398
|
+
return this.verifySettlementSync(payment);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Verify settlement asynchronously (for optimistic execution).
|
|
403
|
+
* Calls the facilitator service to verify and execute the on-chain transfer.
|
|
404
|
+
*/
|
|
405
|
+
async verifySettlementAsync(payment) {
|
|
406
|
+
const nonce = payment.payload.authorization.nonce;
|
|
407
|
+
try {
|
|
408
|
+
const result = await this.callFacilitator(payment);
|
|
409
|
+
this.settlementResults.set(nonce, result);
|
|
410
|
+
this.pendingSettlements.delete(nonce);
|
|
411
|
+
} catch (err) {
|
|
412
|
+
this.settlementResults.set(nonce, {
|
|
413
|
+
success: false,
|
|
414
|
+
transaction: null,
|
|
415
|
+
network: payment.network,
|
|
416
|
+
payer: payment.payload.authorization.from,
|
|
417
|
+
errorReason: `Facilitator error: ${err instanceof Error ? err.message : String(err)}`,
|
|
418
|
+
mode: "on_chain",
|
|
419
|
+
settledAt: Date.now()
|
|
420
|
+
});
|
|
421
|
+
this.pendingSettlements.delete(nonce);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Verify settlement synchronously (blocking).
|
|
426
|
+
*/
|
|
427
|
+
async verifySettlementSync(payment) {
|
|
428
|
+
return this.callFacilitator(payment);
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Call the x402 facilitator service for on-chain settlement.
|
|
432
|
+
*
|
|
433
|
+
* The facilitator:
|
|
434
|
+
* 1. Validates the EIP-712/Ed25519 signature cryptographically
|
|
435
|
+
* 2. Submits the `transferWithAuthorization` transaction on-chain
|
|
436
|
+
* 3. Returns the transaction hash and confirmation
|
|
437
|
+
*/
|
|
438
|
+
async callFacilitator(payment) {
|
|
439
|
+
const payer = payment.payload.authorization.from;
|
|
440
|
+
try {
|
|
441
|
+
const response = await fetch(`${this.config.facilitatorUrl}/settle`, {
|
|
442
|
+
method: "POST",
|
|
443
|
+
headers: { "Content-Type": "application/json" },
|
|
444
|
+
body: JSON.stringify({
|
|
445
|
+
x402Version: X402_VERSION,
|
|
446
|
+
scheme: payment.scheme,
|
|
447
|
+
network: payment.network,
|
|
448
|
+
payload: payment.payload
|
|
449
|
+
})
|
|
450
|
+
});
|
|
451
|
+
if (!response.ok) {
|
|
452
|
+
const error = await response.text().catch(() => response.statusText);
|
|
453
|
+
return {
|
|
454
|
+
success: false,
|
|
455
|
+
transaction: null,
|
|
456
|
+
network: payment.network,
|
|
457
|
+
payer,
|
|
458
|
+
errorReason: `Facilitator returned ${response.status}: ${error}`,
|
|
459
|
+
mode: "on_chain",
|
|
460
|
+
settledAt: Date.now()
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
const result = await response.json();
|
|
464
|
+
return {
|
|
465
|
+
success: result.success,
|
|
466
|
+
transaction: result.transaction ?? null,
|
|
467
|
+
network: payment.network,
|
|
468
|
+
payer,
|
|
469
|
+
errorReason: result.errorReason ?? null,
|
|
470
|
+
mode: "on_chain",
|
|
471
|
+
settledAt: Date.now()
|
|
472
|
+
};
|
|
473
|
+
} catch (err) {
|
|
474
|
+
return {
|
|
475
|
+
success: false,
|
|
476
|
+
transaction: null,
|
|
477
|
+
network: payment.network,
|
|
478
|
+
payer,
|
|
479
|
+
errorReason: `Network error: ${err instanceof Error ? err.message : String(err)}`,
|
|
480
|
+
mode: "on_chain",
|
|
481
|
+
settledAt: Date.now()
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
// ===========================================================================
|
|
486
|
+
// BATCH SETTLEMENT
|
|
487
|
+
// ===========================================================================
|
|
488
|
+
/**
|
|
489
|
+
* Start automatic batch settlement of in-memory ledger entries.
|
|
490
|
+
*/
|
|
491
|
+
startBatchSettlement() {
|
|
492
|
+
if (this.batchSettlementTimer) return;
|
|
493
|
+
this.batchSettlementTimer = setInterval(() => {
|
|
494
|
+
this.runBatchSettlement().catch((err) => {
|
|
495
|
+
console.error("[x402] Batch settlement error:", err);
|
|
496
|
+
});
|
|
497
|
+
}, this.config.batchSettlementIntervalMs);
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Stop automatic batch settlement.
|
|
501
|
+
*/
|
|
502
|
+
stopBatchSettlement() {
|
|
503
|
+
if (this.batchSettlementTimer) {
|
|
504
|
+
clearInterval(this.batchSettlementTimer);
|
|
505
|
+
this.batchSettlementTimer = null;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Run a single batch settlement cycle.
|
|
510
|
+
* Aggregates unsettled micro-payments by payer and submits on-chain.
|
|
511
|
+
*/
|
|
512
|
+
async runBatchSettlement() {
|
|
513
|
+
const unsettled = this.ledger.getUnsettled();
|
|
514
|
+
if (unsettled.length === 0) {
|
|
515
|
+
return { settled: 0, failed: 0, totalVolume: 0 };
|
|
516
|
+
}
|
|
517
|
+
const byPayer = /* @__PURE__ */ new Map();
|
|
518
|
+
for (const entry of unsettled) {
|
|
519
|
+
const existing = byPayer.get(entry.from) ?? [];
|
|
520
|
+
existing.push(entry);
|
|
521
|
+
byPayer.set(entry.from, existing);
|
|
522
|
+
}
|
|
523
|
+
let settled = 0;
|
|
524
|
+
const failed = 0;
|
|
525
|
+
let totalVolume = 0;
|
|
526
|
+
for (const [_payer, entries] of byPayer) {
|
|
527
|
+
const totalAmount = entries.reduce((sum, e) => sum + e.amount, 0);
|
|
528
|
+
totalVolume += totalAmount;
|
|
529
|
+
const entryIds = entries.map((e) => e.id);
|
|
530
|
+
const batchTxHash = `batch_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
531
|
+
this.ledger.markSettled(entryIds, batchTxHash);
|
|
532
|
+
settled += entries.length;
|
|
533
|
+
}
|
|
534
|
+
return { settled, failed, totalVolume };
|
|
535
|
+
}
|
|
536
|
+
// ===========================================================================
|
|
537
|
+
// X-PAYMENT HEADER HELPERS
|
|
538
|
+
// ===========================================================================
|
|
539
|
+
/**
|
|
540
|
+
* Decode a base64-encoded X-PAYMENT header into a payment payload.
|
|
541
|
+
*/
|
|
542
|
+
static decodeXPaymentHeader(header) {
|
|
543
|
+
try {
|
|
544
|
+
const decoded = typeof atob === "function" ? atob(header) : Buffer.from(header, "base64").toString("utf-8");
|
|
545
|
+
return JSON.parse(decoded);
|
|
546
|
+
} catch {
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Encode a payment payload into a base64 string for the X-PAYMENT header.
|
|
552
|
+
*/
|
|
553
|
+
static encodeXPaymentHeader(payload) {
|
|
554
|
+
const json = JSON.stringify(payload);
|
|
555
|
+
return typeof btoa === "function" ? btoa(json) : Buffer.from(json, "utf-8").toString("base64");
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Create an X-PAYMENT-RESPONSE header value from a settlement result.
|
|
559
|
+
*/
|
|
560
|
+
static createPaymentResponseHeader(result) {
|
|
561
|
+
const response = {
|
|
562
|
+
success: result.success,
|
|
563
|
+
transaction: result.transaction,
|
|
564
|
+
network: result.network,
|
|
565
|
+
payer: result.payer,
|
|
566
|
+
errorReason: result.errorReason
|
|
567
|
+
};
|
|
568
|
+
const json = JSON.stringify(response);
|
|
569
|
+
return typeof btoa === "function" ? btoa(json) : Buffer.from(json, "utf-8").toString("base64");
|
|
570
|
+
}
|
|
571
|
+
// ===========================================================================
|
|
572
|
+
// QUERY / STATUS
|
|
573
|
+
// ===========================================================================
|
|
574
|
+
/**
|
|
575
|
+
* Check the settlement status of a pending optimistic execution.
|
|
576
|
+
*/
|
|
577
|
+
getSettlementStatus(nonce) {
|
|
578
|
+
const result = this.settlementResults.get(nonce);
|
|
579
|
+
if (result) return result;
|
|
580
|
+
if (this.pendingSettlements.has(nonce)) return "pending";
|
|
581
|
+
return "unknown";
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Get the in-memory ledger instance.
|
|
585
|
+
*/
|
|
586
|
+
getLedger() {
|
|
587
|
+
return this.ledger;
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Get facilitator statistics.
|
|
591
|
+
*/
|
|
592
|
+
getStats() {
|
|
593
|
+
return {
|
|
594
|
+
usedNonces: this.usedNonces.size,
|
|
595
|
+
pendingSettlements: this.pendingSettlements.size,
|
|
596
|
+
completedSettlements: this.settlementResults.size,
|
|
597
|
+
ledger: this.ledger.getStats()
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Clean up resources.
|
|
602
|
+
*/
|
|
603
|
+
dispose() {
|
|
604
|
+
this.stopBatchSettlement();
|
|
605
|
+
this.usedNonces.clear();
|
|
606
|
+
this.pendingSettlements.clear();
|
|
607
|
+
this.settlementResults.clear();
|
|
608
|
+
this.ledger.reset();
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
var creditTraitHandler = {
|
|
612
|
+
name: "credit",
|
|
613
|
+
defaultConfig: {
|
|
614
|
+
price: 0.01,
|
|
615
|
+
chain: "base",
|
|
616
|
+
recipient: "0x0000000000000000000000000000000000000000",
|
|
617
|
+
description: "HoloScript premium resource",
|
|
618
|
+
timeout: 60,
|
|
619
|
+
optimistic: true
|
|
620
|
+
},
|
|
621
|
+
onAttach(node, config, context) {
|
|
622
|
+
const facilitator = new X402Facilitator({
|
|
623
|
+
recipientAddress: config.recipient,
|
|
624
|
+
chain: config.chain,
|
|
625
|
+
secondaryChain: config.secondary_chain,
|
|
626
|
+
microPaymentThreshold: config.micro_threshold ? Math.round(config.micro_threshold * 1e6) : void 0,
|
|
627
|
+
maxTimeoutSeconds: config.timeout,
|
|
628
|
+
optimisticExecution: config.optimistic,
|
|
629
|
+
resourceDescription: config.description
|
|
630
|
+
});
|
|
631
|
+
const state = {
|
|
632
|
+
facilitator,
|
|
633
|
+
accessGranted: /* @__PURE__ */ new Map(),
|
|
634
|
+
totalRevenue: 0,
|
|
635
|
+
totalRequests: 0,
|
|
636
|
+
totalGranted: 0,
|
|
637
|
+
totalDenied: 0
|
|
638
|
+
};
|
|
639
|
+
node.__creditState = state;
|
|
640
|
+
context?.emit?.("credit:initialized", {
|
|
641
|
+
price: config.price,
|
|
642
|
+
chain: config.chain,
|
|
643
|
+
recipient: config.recipient,
|
|
644
|
+
description: config.description
|
|
645
|
+
});
|
|
646
|
+
},
|
|
647
|
+
onDetach(node, _config, context) {
|
|
648
|
+
const state = node.__creditState;
|
|
649
|
+
if (state) {
|
|
650
|
+
state.facilitator.dispose();
|
|
651
|
+
context.emit?.("credit:shutdown", {
|
|
652
|
+
totalRevenue: state.totalRevenue,
|
|
653
|
+
totalRequests: state.totalRequests,
|
|
654
|
+
totalGranted: state.totalGranted,
|
|
655
|
+
totalDenied: state.totalDenied
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
delete node.__creditState;
|
|
659
|
+
},
|
|
660
|
+
onUpdate(node, _config, context, _delta) {
|
|
661
|
+
const state = node.__creditState;
|
|
662
|
+
if (!state) return;
|
|
663
|
+
const now = Date.now();
|
|
664
|
+
for (const [payer, grant] of state.accessGranted) {
|
|
665
|
+
if (grant.expiresAt > 0 && now > grant.expiresAt) {
|
|
666
|
+
state.accessGranted.delete(payer);
|
|
667
|
+
context.emit?.("credit:access_expired", { payer, resource: _config.description });
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
},
|
|
671
|
+
onEvent(node, config, context, event) {
|
|
672
|
+
const state = node.__creditState;
|
|
673
|
+
if (!state) return;
|
|
674
|
+
const eventType = typeof event === "string" ? event : event.type;
|
|
675
|
+
const payload = event?.payload ?? event;
|
|
676
|
+
switch (eventType) {
|
|
677
|
+
// ─── Request access (generates 402 response) ─────────────────────
|
|
678
|
+
case "credit:request_access": {
|
|
679
|
+
state.totalRequests++;
|
|
680
|
+
const resource = payload.resource ?? config.description;
|
|
681
|
+
const payer = payload.payer ?? payload.from;
|
|
682
|
+
const existing = state.accessGranted.get(payer);
|
|
683
|
+
if (existing && (existing.expiresAt === 0 || Date.now() < existing.expiresAt)) {
|
|
684
|
+
context.emit?.("credit:access_granted", {
|
|
685
|
+
payer,
|
|
686
|
+
amount: 0,
|
|
687
|
+
mode: "cached",
|
|
688
|
+
resource
|
|
689
|
+
});
|
|
690
|
+
state.totalGranted++;
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
const paymentRequired = state.facilitator.createPaymentRequired(
|
|
694
|
+
resource,
|
|
695
|
+
config.price,
|
|
696
|
+
config.description
|
|
697
|
+
);
|
|
698
|
+
context.emit?.("credit:payment_required", {
|
|
699
|
+
resource,
|
|
700
|
+
paymentRequired,
|
|
701
|
+
statusCode: 402,
|
|
702
|
+
headers: {
|
|
703
|
+
"Content-Type": "application/json"
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
708
|
+
// ─── Submit payment (process X-PAYMENT header) ───────────────────
|
|
709
|
+
case "credit:submit_payment": {
|
|
710
|
+
const resource = payload.resource ?? config.description;
|
|
711
|
+
const xPaymentHeader = payload.xPayment ?? payload.payment;
|
|
712
|
+
if (!xPaymentHeader) {
|
|
713
|
+
context.emit?.("credit:access_denied", {
|
|
714
|
+
payer: "unknown",
|
|
715
|
+
reason: "Missing X-PAYMENT header",
|
|
716
|
+
resource
|
|
717
|
+
});
|
|
718
|
+
state.totalDenied++;
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
const paymentPayload = typeof xPaymentHeader === "string" ? X402Facilitator.decodeXPaymentHeader(xPaymentHeader) : xPaymentHeader;
|
|
722
|
+
if (!paymentPayload) {
|
|
723
|
+
context.emit?.("credit:access_denied", {
|
|
724
|
+
payer: "unknown",
|
|
725
|
+
reason: "Invalid X-PAYMENT header encoding",
|
|
726
|
+
resource
|
|
727
|
+
});
|
|
728
|
+
state.totalDenied++;
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
const requiredAmount = Math.round(config.price * 1e6).toString();
|
|
732
|
+
const payer = paymentPayload.payload.authorization.from;
|
|
733
|
+
state.facilitator.processPayment(paymentPayload, resource, requiredAmount).then((result) => {
|
|
734
|
+
if (result.success) {
|
|
735
|
+
state.accessGranted.set(payer, {
|
|
736
|
+
grantedAt: Date.now(),
|
|
737
|
+
expiresAt: config.timeout > 0 ? Date.now() + config.timeout * 1e3 : 0,
|
|
738
|
+
settlementId: result.transaction ?? ""
|
|
739
|
+
});
|
|
740
|
+
state.totalRevenue += config.price;
|
|
741
|
+
state.totalGranted++;
|
|
742
|
+
context.emit?.("credit:access_granted", {
|
|
743
|
+
payer,
|
|
744
|
+
amount: config.price,
|
|
745
|
+
mode: result.mode,
|
|
746
|
+
resource,
|
|
747
|
+
transaction: result.transaction,
|
|
748
|
+
network: result.network
|
|
749
|
+
});
|
|
750
|
+
context.emit?.("credit:payment_response", {
|
|
751
|
+
resource,
|
|
752
|
+
headers: {
|
|
753
|
+
"X-PAYMENT-RESPONSE": X402Facilitator.createPaymentResponseHeader(result)
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
} else {
|
|
757
|
+
state.totalDenied++;
|
|
758
|
+
context.emit?.("credit:access_denied", {
|
|
759
|
+
payer,
|
|
760
|
+
reason: result.errorReason,
|
|
761
|
+
resource
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
}).catch((err) => {
|
|
765
|
+
state.totalDenied++;
|
|
766
|
+
context.emit?.("credit:access_denied", {
|
|
767
|
+
payer,
|
|
768
|
+
reason: `Settlement error: ${err instanceof Error ? err.message : String(err)}`,
|
|
769
|
+
resource
|
|
770
|
+
});
|
|
771
|
+
});
|
|
772
|
+
break;
|
|
773
|
+
}
|
|
774
|
+
// ─── Check settlement status ─────────────────────────────────────
|
|
775
|
+
case "credit:check_settlement": {
|
|
776
|
+
const nonce = payload.nonce;
|
|
777
|
+
if (!nonce) return;
|
|
778
|
+
const status = state.facilitator.getSettlementStatus(nonce);
|
|
779
|
+
context.emit?.("credit:settlement_status", { nonce, status });
|
|
780
|
+
break;
|
|
781
|
+
}
|
|
782
|
+
// ─── Run batch settlement ────────────────────────────────────────
|
|
783
|
+
case "credit:batch_settle": {
|
|
784
|
+
state.facilitator.runBatchSettlement().then((result) => {
|
|
785
|
+
context.emit?.("credit:batch_settled", result);
|
|
786
|
+
}).catch((err) => {
|
|
787
|
+
context.emit?.("credit:batch_error", {
|
|
788
|
+
error: err instanceof Error ? err.message : String(err)
|
|
789
|
+
});
|
|
790
|
+
});
|
|
791
|
+
break;
|
|
792
|
+
}
|
|
793
|
+
// ─── Query stats ─────────────────────────────────────────────────
|
|
794
|
+
case "credit:get_stats": {
|
|
795
|
+
const facilStats = state.facilitator.getStats();
|
|
796
|
+
context.emit?.("credit:stats", {
|
|
797
|
+
totalRevenue: state.totalRevenue,
|
|
798
|
+
totalRequests: state.totalRequests,
|
|
799
|
+
totalGranted: state.totalGranted,
|
|
800
|
+
totalDenied: state.totalDenied,
|
|
801
|
+
facilitator: facilStats
|
|
802
|
+
});
|
|
803
|
+
break;
|
|
804
|
+
}
|
|
805
|
+
// ─── Revoke access ───────────────────────────────────────────────
|
|
806
|
+
case "credit:revoke_access": {
|
|
807
|
+
const payer = payload.payer ?? payload.from;
|
|
808
|
+
if (payer) {
|
|
809
|
+
state.accessGranted.delete(payer);
|
|
810
|
+
context.emit?.("credit:access_revoked", { payer });
|
|
811
|
+
}
|
|
812
|
+
break;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
var CHAIN_IDS = {
|
|
818
|
+
base: 8453,
|
|
819
|
+
"base-sepolia": 84532
|
|
820
|
+
};
|
|
821
|
+
var CHAIN_ID_TO_NETWORK = {
|
|
822
|
+
8453: "base",
|
|
823
|
+
84532: "base-sepolia"
|
|
824
|
+
};
|
|
825
|
+
var PaymentGateway = class {
|
|
826
|
+
facilitator;
|
|
827
|
+
listeners = /* @__PURE__ */ new Map();
|
|
828
|
+
refundLedger = /* @__PURE__ */ new Map();
|
|
829
|
+
eventCounter = 0;
|
|
830
|
+
config;
|
|
831
|
+
constructor(config) {
|
|
832
|
+
this.config = config;
|
|
833
|
+
this.facilitator = new X402Facilitator(config);
|
|
834
|
+
}
|
|
835
|
+
// ===========================================================================
|
|
836
|
+
// EVENT EMITTER (Audit Trail)
|
|
837
|
+
// ===========================================================================
|
|
838
|
+
/**
|
|
839
|
+
* Subscribe to settlement audit events.
|
|
840
|
+
* Use '*' to receive all events.
|
|
841
|
+
*
|
|
842
|
+
* @param eventType - Event type to listen for, or '*' for all
|
|
843
|
+
* @param listener - Callback function
|
|
844
|
+
* @returns Unsubscribe function
|
|
845
|
+
*/
|
|
846
|
+
on(eventType, listener) {
|
|
847
|
+
if (!this.listeners.has(eventType)) {
|
|
848
|
+
this.listeners.set(eventType, /* @__PURE__ */ new Set());
|
|
849
|
+
}
|
|
850
|
+
this.listeners.get(eventType).add(listener);
|
|
851
|
+
return () => {
|
|
852
|
+
this.listeners.get(eventType)?.delete(listener);
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Remove a specific listener.
|
|
857
|
+
*/
|
|
858
|
+
off(eventType, listener) {
|
|
859
|
+
this.listeners.get(eventType)?.delete(listener);
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Emit a settlement audit event.
|
|
863
|
+
*/
|
|
864
|
+
emit(type, data) {
|
|
865
|
+
const event = {
|
|
866
|
+
type,
|
|
867
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
868
|
+
eventId: `evt_${Date.now()}_${this.eventCounter++}`,
|
|
869
|
+
nonce: data.nonce ?? null,
|
|
870
|
+
payer: data.payer ?? null,
|
|
871
|
+
recipient: data.recipient ?? null,
|
|
872
|
+
amount: data.amount ?? null,
|
|
873
|
+
network: data.network ?? null,
|
|
874
|
+
transaction: data.transaction ?? null,
|
|
875
|
+
metadata: data.metadata ?? {}
|
|
876
|
+
};
|
|
877
|
+
const typeListeners = this.listeners.get(type);
|
|
878
|
+
if (typeListeners) {
|
|
879
|
+
for (const listener of typeListeners) {
|
|
880
|
+
try {
|
|
881
|
+
listener(event);
|
|
882
|
+
} catch {
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
const wildcardListeners = this.listeners.get("*");
|
|
887
|
+
if (wildcardListeners) {
|
|
888
|
+
for (const listener of wildcardListeners) {
|
|
889
|
+
try {
|
|
890
|
+
listener(event);
|
|
891
|
+
} catch {
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return event;
|
|
896
|
+
}
|
|
897
|
+
// ===========================================================================
|
|
898
|
+
// PAYMENT AUTHORIZATION
|
|
899
|
+
// ===========================================================================
|
|
900
|
+
/**
|
|
901
|
+
* Create a payment authorization (HTTP 402 response body).
|
|
902
|
+
*
|
|
903
|
+
* This is step 1 of the x402 flow: the server tells the agent what payment
|
|
904
|
+
* is required to access the resource.
|
|
905
|
+
*
|
|
906
|
+
* @param resource - Resource path being gated (e.g., "/api/premium-scene")
|
|
907
|
+
* @param amountUSDC - Price in USDC (human-readable, e.g., 0.05 for 5 cents)
|
|
908
|
+
* @param description - Human-readable description
|
|
909
|
+
* @returns x402 PaymentRequired response body
|
|
910
|
+
*/
|
|
911
|
+
createPaymentAuthorization(resource, amountUSDC, description) {
|
|
912
|
+
const paymentRequired = this.facilitator.createPaymentRequired(
|
|
913
|
+
resource,
|
|
914
|
+
amountUSDC,
|
|
915
|
+
description
|
|
916
|
+
);
|
|
917
|
+
this.emit("payment:authorization_created", {
|
|
918
|
+
recipient: this.config.recipientAddress,
|
|
919
|
+
amount: Math.round(amountUSDC * 1e6).toString(),
|
|
920
|
+
network: this.config.chain,
|
|
921
|
+
metadata: { resource, description: description ?? "" }
|
|
922
|
+
});
|
|
923
|
+
return {
|
|
924
|
+
...paymentRequired,
|
|
925
|
+
chainId: CHAIN_IDS[this.config.chain] ?? 0
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
// ===========================================================================
|
|
929
|
+
// PAYMENT VERIFICATION
|
|
930
|
+
// ===========================================================================
|
|
931
|
+
/**
|
|
932
|
+
* Verify an X-PAYMENT header.
|
|
933
|
+
*
|
|
934
|
+
* Accepts either a raw base64 string (from HTTP header) or a decoded payload.
|
|
935
|
+
* Validates protocol version, nonce, temporal window, amount, and recipient.
|
|
936
|
+
*
|
|
937
|
+
* @param payment - Base64-encoded X-PAYMENT header string or decoded payload
|
|
938
|
+
* @param requiredAmount - Required amount in USDC base units (string for precision)
|
|
939
|
+
* @returns Verification result
|
|
940
|
+
*/
|
|
941
|
+
verifyPayment(payment, requiredAmount) {
|
|
942
|
+
const payload = typeof payment === "string" ? X402Facilitator.decodeXPaymentHeader(payment) : payment;
|
|
943
|
+
if (!payload) {
|
|
944
|
+
this.emit("payment:verification_failed", {
|
|
945
|
+
metadata: { reason: "Failed to decode X-PAYMENT header" }
|
|
946
|
+
});
|
|
947
|
+
return {
|
|
948
|
+
isValid: false,
|
|
949
|
+
invalidReason: "Failed to decode X-PAYMENT header",
|
|
950
|
+
decodedPayload: null
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
const payer = payload.payload.authorization.from;
|
|
954
|
+
const nonce = payload.payload.authorization.nonce;
|
|
955
|
+
this.emit("payment:verification_started", {
|
|
956
|
+
payer,
|
|
957
|
+
nonce,
|
|
958
|
+
amount: payload.payload.authorization.value,
|
|
959
|
+
network: payload.network
|
|
960
|
+
});
|
|
961
|
+
const result = this.facilitator.verifyPayment(payload, requiredAmount);
|
|
962
|
+
if (result.isValid) {
|
|
963
|
+
this.emit("payment:verification_passed", {
|
|
964
|
+
payer,
|
|
965
|
+
nonce,
|
|
966
|
+
amount: payload.payload.authorization.value,
|
|
967
|
+
network: payload.network
|
|
968
|
+
});
|
|
969
|
+
} else {
|
|
970
|
+
this.emit("payment:verification_failed", {
|
|
971
|
+
payer,
|
|
972
|
+
nonce,
|
|
973
|
+
metadata: { reason: result.invalidReason }
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
return { ...result, decodedPayload: payload };
|
|
977
|
+
}
|
|
978
|
+
// ===========================================================================
|
|
979
|
+
// PAYMENT SETTLEMENT
|
|
980
|
+
// ===========================================================================
|
|
981
|
+
/**
|
|
982
|
+
* Settle a verified payment.
|
|
983
|
+
*
|
|
984
|
+
* Routes to in-memory micro-payment ledger or on-chain settlement
|
|
985
|
+
* depending on the amount. Emits audit events at each stage.
|
|
986
|
+
*
|
|
987
|
+
* @param payment - Decoded X-PAYMENT payload (or base64 string)
|
|
988
|
+
* @param resource - Resource being accessed
|
|
989
|
+
* @param requiredAmount - Required amount in USDC base units
|
|
990
|
+
* @returns Settlement result
|
|
991
|
+
*/
|
|
992
|
+
async settlePayment(payment, resource, requiredAmount) {
|
|
993
|
+
const payload = typeof payment === "string" ? X402Facilitator.decodeXPaymentHeader(payment) : payment;
|
|
994
|
+
if (!payload) {
|
|
995
|
+
return {
|
|
996
|
+
success: false,
|
|
997
|
+
transaction: null,
|
|
998
|
+
network: this.config.chain,
|
|
999
|
+
payer: "unknown",
|
|
1000
|
+
errorReason: "Failed to decode payment payload",
|
|
1001
|
+
mode: "on_chain",
|
|
1002
|
+
settledAt: Date.now()
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
const payer = payload.payload.authorization.from;
|
|
1006
|
+
const nonce = payload.payload.authorization.nonce;
|
|
1007
|
+
const amount = payload.payload.authorization.value;
|
|
1008
|
+
this.emit("payment:settlement_started", {
|
|
1009
|
+
payer,
|
|
1010
|
+
nonce,
|
|
1011
|
+
amount,
|
|
1012
|
+
network: payload.network,
|
|
1013
|
+
recipient: this.config.recipientAddress,
|
|
1014
|
+
metadata: { resource }
|
|
1015
|
+
});
|
|
1016
|
+
const result = await this.facilitator.processPayment(payload, resource, requiredAmount);
|
|
1017
|
+
if (result.success) {
|
|
1018
|
+
this.emit("payment:settlement_completed", {
|
|
1019
|
+
payer,
|
|
1020
|
+
nonce,
|
|
1021
|
+
amount,
|
|
1022
|
+
network: result.network,
|
|
1023
|
+
transaction: result.transaction,
|
|
1024
|
+
recipient: this.config.recipientAddress,
|
|
1025
|
+
metadata: { resource, mode: result.mode }
|
|
1026
|
+
});
|
|
1027
|
+
} else {
|
|
1028
|
+
this.emit("payment:settlement_failed", {
|
|
1029
|
+
payer,
|
|
1030
|
+
nonce,
|
|
1031
|
+
amount,
|
|
1032
|
+
network: result.network,
|
|
1033
|
+
metadata: { resource, errorReason: result.errorReason }
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
return result;
|
|
1037
|
+
}
|
|
1038
|
+
// ===========================================================================
|
|
1039
|
+
// REFUND
|
|
1040
|
+
// ===========================================================================
|
|
1041
|
+
/**
|
|
1042
|
+
* Refund a completed payment.
|
|
1043
|
+
*
|
|
1044
|
+
* For in-memory micro-payments: records a reverse entry in the ledger.
|
|
1045
|
+
* For on-chain payments: calls the facilitator service to initiate refund.
|
|
1046
|
+
*
|
|
1047
|
+
* @param request - Refund request details
|
|
1048
|
+
* @returns Refund result
|
|
1049
|
+
*/
|
|
1050
|
+
async refundPayment(request) {
|
|
1051
|
+
const { originalNonce, reason, partialAmount } = request;
|
|
1052
|
+
this.emit("payment:refund_initiated", {
|
|
1053
|
+
nonce: originalNonce,
|
|
1054
|
+
metadata: { reason, partialAmount }
|
|
1055
|
+
});
|
|
1056
|
+
const originalStatus = this.facilitator.getSettlementStatus(originalNonce);
|
|
1057
|
+
const ledger = this.facilitator.getLedger();
|
|
1058
|
+
const allEntries = ledger.getEntriesForPayer("");
|
|
1059
|
+
let originalEntry;
|
|
1060
|
+
const ledgerStats = ledger.getStats();
|
|
1061
|
+
if (originalStatus !== "unknown" && originalStatus !== "pending") {
|
|
1062
|
+
const settlement = originalStatus;
|
|
1063
|
+
if (settlement.mode === "in_memory" && settlement.transaction) {
|
|
1064
|
+
const refundAmount = partialAmount ?? settlement.transaction ? "0" : "0";
|
|
1065
|
+
const refundId2 = `refund_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
1066
|
+
const reverseEntry = ledger.record(
|
|
1067
|
+
this.config.recipientAddress,
|
|
1068
|
+
settlement.payer,
|
|
1069
|
+
partialAmount ? parseInt(partialAmount, 10) : 0,
|
|
1070
|
+
`refund:${originalNonce}`
|
|
1071
|
+
);
|
|
1072
|
+
const result2 = {
|
|
1073
|
+
success: true,
|
|
1074
|
+
refundId: refundId2,
|
|
1075
|
+
amountRefunded: partialAmount ?? "0",
|
|
1076
|
+
originalNonce,
|
|
1077
|
+
transaction: reverseEntry.id,
|
|
1078
|
+
originalMode: "in_memory",
|
|
1079
|
+
reason,
|
|
1080
|
+
errorReason: null,
|
|
1081
|
+
refundedAt: Date.now()
|
|
1082
|
+
};
|
|
1083
|
+
this.refundLedger.set(refundId2, result2);
|
|
1084
|
+
this.emit("payment:refund_completed", {
|
|
1085
|
+
nonce: originalNonce,
|
|
1086
|
+
payer: settlement.payer,
|
|
1087
|
+
amount: result2.amountRefunded,
|
|
1088
|
+
transaction: reverseEntry.id,
|
|
1089
|
+
network: "in_memory",
|
|
1090
|
+
metadata: { reason, refundId: refundId2 }
|
|
1091
|
+
});
|
|
1092
|
+
return result2;
|
|
1093
|
+
}
|
|
1094
|
+
if (settlement.mode === "on_chain") {
|
|
1095
|
+
try {
|
|
1096
|
+
const response = await fetch(
|
|
1097
|
+
`${this.config.facilitatorUrl ?? "https://x402.org/facilitator"}/refund`,
|
|
1098
|
+
{
|
|
1099
|
+
method: "POST",
|
|
1100
|
+
headers: { "Content-Type": "application/json" },
|
|
1101
|
+
body: JSON.stringify({
|
|
1102
|
+
originalTransaction: settlement.transaction,
|
|
1103
|
+
originalNonce,
|
|
1104
|
+
refundAmount: partialAmount,
|
|
1105
|
+
reason,
|
|
1106
|
+
network: settlement.network
|
|
1107
|
+
})
|
|
1108
|
+
}
|
|
1109
|
+
);
|
|
1110
|
+
const refundId2 = `refund_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
1111
|
+
if (response.ok) {
|
|
1112
|
+
const body = await response.json();
|
|
1113
|
+
const result2 = {
|
|
1114
|
+
success: body.success,
|
|
1115
|
+
refundId: refundId2,
|
|
1116
|
+
amountRefunded: body.amountRefunded ?? partialAmount ?? "0",
|
|
1117
|
+
originalNonce,
|
|
1118
|
+
transaction: body.transaction ?? null,
|
|
1119
|
+
originalMode: "on_chain",
|
|
1120
|
+
reason,
|
|
1121
|
+
errorReason: body.errorReason ?? null,
|
|
1122
|
+
refundedAt: Date.now()
|
|
1123
|
+
};
|
|
1124
|
+
this.refundLedger.set(refundId2, result2);
|
|
1125
|
+
if (body.success) {
|
|
1126
|
+
this.emit("payment:refund_completed", {
|
|
1127
|
+
nonce: originalNonce,
|
|
1128
|
+
payer: settlement.payer,
|
|
1129
|
+
amount: result2.amountRefunded,
|
|
1130
|
+
transaction: body.transaction ?? null,
|
|
1131
|
+
network: settlement.network,
|
|
1132
|
+
metadata: { reason, refundId: refundId2 }
|
|
1133
|
+
});
|
|
1134
|
+
} else {
|
|
1135
|
+
this.emit("payment:refund_failed", {
|
|
1136
|
+
nonce: originalNonce,
|
|
1137
|
+
metadata: { reason, errorReason: body.errorReason, refundId: refundId2 }
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
return result2;
|
|
1141
|
+
} else {
|
|
1142
|
+
const errorText = await response.text().catch(() => response.statusText);
|
|
1143
|
+
const result2 = {
|
|
1144
|
+
success: false,
|
|
1145
|
+
refundId: refundId2,
|
|
1146
|
+
amountRefunded: "0",
|
|
1147
|
+
originalNonce,
|
|
1148
|
+
transaction: null,
|
|
1149
|
+
originalMode: "on_chain",
|
|
1150
|
+
reason,
|
|
1151
|
+
errorReason: `Facilitator returned ${response.status}: ${errorText}`,
|
|
1152
|
+
refundedAt: Date.now()
|
|
1153
|
+
};
|
|
1154
|
+
this.refundLedger.set(refundId2, result2);
|
|
1155
|
+
this.emit("payment:refund_failed", {
|
|
1156
|
+
nonce: originalNonce,
|
|
1157
|
+
metadata: { reason, errorReason: result2.errorReason, refundId: refundId2 }
|
|
1158
|
+
});
|
|
1159
|
+
return result2;
|
|
1160
|
+
}
|
|
1161
|
+
} catch (err) {
|
|
1162
|
+
const refundId2 = `refund_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
1163
|
+
const result2 = {
|
|
1164
|
+
success: false,
|
|
1165
|
+
refundId: refundId2,
|
|
1166
|
+
amountRefunded: "0",
|
|
1167
|
+
originalNonce,
|
|
1168
|
+
transaction: null,
|
|
1169
|
+
originalMode: "on_chain",
|
|
1170
|
+
reason,
|
|
1171
|
+
errorReason: `Network error: ${err instanceof Error ? err.message : String(err)}`,
|
|
1172
|
+
refundedAt: Date.now()
|
|
1173
|
+
};
|
|
1174
|
+
this.refundLedger.set(refundId2, result2);
|
|
1175
|
+
this.emit("payment:refund_failed", {
|
|
1176
|
+
nonce: originalNonce,
|
|
1177
|
+
metadata: { reason, errorReason: result2.errorReason, refundId: refundId2 }
|
|
1178
|
+
});
|
|
1179
|
+
return result2;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
const refundId = `refund_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
1184
|
+
const result = {
|
|
1185
|
+
success: false,
|
|
1186
|
+
refundId,
|
|
1187
|
+
amountRefunded: "0",
|
|
1188
|
+
originalNonce,
|
|
1189
|
+
transaction: null,
|
|
1190
|
+
originalMode: "in_memory",
|
|
1191
|
+
reason,
|
|
1192
|
+
errorReason: originalStatus === "pending" ? "Cannot refund: original payment still pending settlement" : "Cannot refund: original payment not found",
|
|
1193
|
+
refundedAt: Date.now()
|
|
1194
|
+
};
|
|
1195
|
+
this.refundLedger.set(refundId, result);
|
|
1196
|
+
this.emit("payment:refund_failed", {
|
|
1197
|
+
nonce: originalNonce,
|
|
1198
|
+
metadata: { reason, errorReason: result.errorReason, refundId }
|
|
1199
|
+
});
|
|
1200
|
+
return result;
|
|
1201
|
+
}
|
|
1202
|
+
// ===========================================================================
|
|
1203
|
+
// BATCH SETTLEMENT
|
|
1204
|
+
// ===========================================================================
|
|
1205
|
+
/**
|
|
1206
|
+
* Run a batch settlement of accumulated micro-payments.
|
|
1207
|
+
*/
|
|
1208
|
+
async runBatchSettlement() {
|
|
1209
|
+
this.emit("payment:batch_settlement_started", {
|
|
1210
|
+
metadata: {
|
|
1211
|
+
unsettledEntries: this.facilitator.getLedger().getStats().unsettledEntries,
|
|
1212
|
+
unsettledVolume: this.facilitator.getLedger().getStats().unsettledVolume
|
|
1213
|
+
}
|
|
1214
|
+
});
|
|
1215
|
+
const result = await this.facilitator.runBatchSettlement();
|
|
1216
|
+
this.emit("payment:batch_settlement_completed", {
|
|
1217
|
+
metadata: {
|
|
1218
|
+
settled: result.settled,
|
|
1219
|
+
failed: result.failed,
|
|
1220
|
+
totalVolume: result.totalVolume
|
|
1221
|
+
}
|
|
1222
|
+
});
|
|
1223
|
+
return result;
|
|
1224
|
+
}
|
|
1225
|
+
// ===========================================================================
|
|
1226
|
+
// QUERY / STATUS
|
|
1227
|
+
// ===========================================================================
|
|
1228
|
+
/**
|
|
1229
|
+
* Get the underlying facilitator instance.
|
|
1230
|
+
*/
|
|
1231
|
+
getFacilitator() {
|
|
1232
|
+
return this.facilitator;
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* Get chain ID for the configured settlement chain.
|
|
1236
|
+
*/
|
|
1237
|
+
getChainId() {
|
|
1238
|
+
return CHAIN_IDS[this.config.chain] ?? 0;
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* Get the USDC contract address for the configured chain.
|
|
1242
|
+
*/
|
|
1243
|
+
getUSDCContract() {
|
|
1244
|
+
return USDC_CONTRACTS[this.config.chain];
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* Look up a refund result by refund ID.
|
|
1248
|
+
*/
|
|
1249
|
+
getRefund(refundId) {
|
|
1250
|
+
return this.refundLedger.get(refundId);
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Get all refund results.
|
|
1254
|
+
*/
|
|
1255
|
+
getAllRefunds() {
|
|
1256
|
+
return Array.from(this.refundLedger.values());
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Get comprehensive gateway statistics.
|
|
1260
|
+
*/
|
|
1261
|
+
getStats() {
|
|
1262
|
+
let listenerCount = 0;
|
|
1263
|
+
for (const listeners of this.listeners.values()) {
|
|
1264
|
+
listenerCount += listeners.size;
|
|
1265
|
+
}
|
|
1266
|
+
return {
|
|
1267
|
+
facilitator: this.facilitator.getStats(),
|
|
1268
|
+
chainId: this.getChainId(),
|
|
1269
|
+
usdcContract: this.getUSDCContract(),
|
|
1270
|
+
totalRefunds: this.refundLedger.size,
|
|
1271
|
+
listenerCount
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Clean up all resources.
|
|
1276
|
+
*/
|
|
1277
|
+
dispose() {
|
|
1278
|
+
this.facilitator.dispose();
|
|
1279
|
+
this.listeners.clear();
|
|
1280
|
+
this.refundLedger.clear();
|
|
1281
|
+
this.eventCounter = 0;
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1284
|
+
|
|
1285
|
+
// src/economy/CreatorRevenueAggregator.ts
|
|
1286
|
+
var CreatorRevenueAggregator = class {
|
|
1287
|
+
config;
|
|
1288
|
+
events = /* @__PURE__ */ new Map();
|
|
1289
|
+
payouts = [];
|
|
1290
|
+
eventCounter = 0;
|
|
1291
|
+
constructor(config) {
|
|
1292
|
+
this.config = {
|
|
1293
|
+
platformFeeRate: config?.platformFeeRate ?? 0.15,
|
|
1294
|
+
minPayoutThreshold: config?.minPayoutThreshold ?? 5e6,
|
|
1295
|
+
maxEventsPerCreator: config?.maxEventsPerCreator ?? 1e4,
|
|
1296
|
+
telemetry: config?.telemetry
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
// ===========================================================================
|
|
1300
|
+
// RECORDING
|
|
1301
|
+
// ===========================================================================
|
|
1302
|
+
/**
|
|
1303
|
+
* Record a revenue event.
|
|
1304
|
+
*/
|
|
1305
|
+
recordRevenue(creatorId, pluginId, grossAmount, payerId, ledgerEntryId) {
|
|
1306
|
+
const platformFee = Math.floor(grossAmount * this.config.platformFeeRate);
|
|
1307
|
+
const netAmount = grossAmount - platformFee;
|
|
1308
|
+
const event = {
|
|
1309
|
+
id: `rev-${++this.eventCounter}`,
|
|
1310
|
+
creatorId,
|
|
1311
|
+
pluginId,
|
|
1312
|
+
grossAmount,
|
|
1313
|
+
platformFee,
|
|
1314
|
+
netAmount,
|
|
1315
|
+
timestamp: Date.now(),
|
|
1316
|
+
payerId,
|
|
1317
|
+
ledgerEntryId
|
|
1318
|
+
};
|
|
1319
|
+
const creatorEvents = this.events.get(creatorId) ?? [];
|
|
1320
|
+
creatorEvents.push(event);
|
|
1321
|
+
if (creatorEvents.length > this.config.maxEventsPerCreator) {
|
|
1322
|
+
creatorEvents.splice(0, creatorEvents.length - this.config.maxEventsPerCreator);
|
|
1323
|
+
}
|
|
1324
|
+
this.events.set(creatorId, creatorEvents);
|
|
1325
|
+
this.emitTelemetry("revenue_recorded", {
|
|
1326
|
+
creatorId,
|
|
1327
|
+
pluginId,
|
|
1328
|
+
grossAmount,
|
|
1329
|
+
netAmount,
|
|
1330
|
+
payerId
|
|
1331
|
+
});
|
|
1332
|
+
return event;
|
|
1333
|
+
}
|
|
1334
|
+
// ===========================================================================
|
|
1335
|
+
// AGGREGATION
|
|
1336
|
+
// ===========================================================================
|
|
1337
|
+
/**
|
|
1338
|
+
* Get earnings for a specific creator.
|
|
1339
|
+
*/
|
|
1340
|
+
getCreatorEarnings(creatorId, period = "monthly") {
|
|
1341
|
+
const events = this.events.get(creatorId) ?? [];
|
|
1342
|
+
const { start, end } = this.getPeriodBounds(period);
|
|
1343
|
+
const filtered = events.filter((e) => e.timestamp >= start && e.timestamp < end);
|
|
1344
|
+
const byPlugin = /* @__PURE__ */ new Map();
|
|
1345
|
+
let totalGross = 0;
|
|
1346
|
+
let totalFees = 0;
|
|
1347
|
+
let totalNet = 0;
|
|
1348
|
+
for (const event of filtered) {
|
|
1349
|
+
totalGross += event.grossAmount;
|
|
1350
|
+
totalFees += event.platformFee;
|
|
1351
|
+
totalNet += event.netAmount;
|
|
1352
|
+
let pluginRev = byPlugin.get(event.pluginId);
|
|
1353
|
+
if (!pluginRev) {
|
|
1354
|
+
pluginRev = {
|
|
1355
|
+
pluginId: event.pluginId,
|
|
1356
|
+
grossAmount: 0,
|
|
1357
|
+
platformFee: 0,
|
|
1358
|
+
netAmount: 0,
|
|
1359
|
+
eventCount: 0,
|
|
1360
|
+
uniquePayers: /* @__PURE__ */ new Set()
|
|
1361
|
+
};
|
|
1362
|
+
byPlugin.set(event.pluginId, pluginRev);
|
|
1363
|
+
}
|
|
1364
|
+
pluginRev.grossAmount += event.grossAmount;
|
|
1365
|
+
pluginRev.platformFee += event.platformFee;
|
|
1366
|
+
pluginRev.netAmount += event.netAmount;
|
|
1367
|
+
pluginRev.eventCount++;
|
|
1368
|
+
pluginRev.uniquePayers.add(event.payerId);
|
|
1369
|
+
}
|
|
1370
|
+
return {
|
|
1371
|
+
creatorId,
|
|
1372
|
+
totalGross,
|
|
1373
|
+
totalFees,
|
|
1374
|
+
totalNet,
|
|
1375
|
+
byPlugin,
|
|
1376
|
+
eventCount: filtered.length,
|
|
1377
|
+
periodStart: new Date(start).toISOString(),
|
|
1378
|
+
periodEnd: new Date(end).toISOString()
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* Get top creators by revenue.
|
|
1383
|
+
*/
|
|
1384
|
+
getTopCreators(period = "monthly", limit = 10) {
|
|
1385
|
+
const results = [];
|
|
1386
|
+
for (const creatorId of this.events.keys()) {
|
|
1387
|
+
const earnings = this.getCreatorEarnings(creatorId, period);
|
|
1388
|
+
results.push({
|
|
1389
|
+
creatorId,
|
|
1390
|
+
totalNet: earnings.totalNet,
|
|
1391
|
+
eventCount: earnings.eventCount
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
return results.sort((a, b) => b.totalNet - a.totalNet).slice(0, limit);
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1397
|
+
* Get platform revenue (total fees collected).
|
|
1398
|
+
*/
|
|
1399
|
+
getPlatformRevenue(period = "monthly") {
|
|
1400
|
+
let totalFees = 0;
|
|
1401
|
+
let totalGross = 0;
|
|
1402
|
+
let creatorCount = 0;
|
|
1403
|
+
for (const creatorId of this.events.keys()) {
|
|
1404
|
+
const earnings = this.getCreatorEarnings(creatorId, period);
|
|
1405
|
+
if (earnings.eventCount > 0) {
|
|
1406
|
+
totalFees += earnings.totalFees;
|
|
1407
|
+
totalGross += earnings.totalGross;
|
|
1408
|
+
creatorCount++;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
return { totalFees, totalGross, creatorCount };
|
|
1412
|
+
}
|
|
1413
|
+
// ===========================================================================
|
|
1414
|
+
// PAYOUTS
|
|
1415
|
+
// ===========================================================================
|
|
1416
|
+
/**
|
|
1417
|
+
* Get creators eligible for payout (net earnings >= threshold).
|
|
1418
|
+
*/
|
|
1419
|
+
getPayoutEligible(period = "monthly") {
|
|
1420
|
+
const eligible = [];
|
|
1421
|
+
for (const creatorId of this.events.keys()) {
|
|
1422
|
+
const earnings = this.getCreatorEarnings(creatorId, period);
|
|
1423
|
+
const previousPayouts = this.getCreatorPayouts(creatorId, period);
|
|
1424
|
+
const alreadyPaid = previousPayouts.reduce(
|
|
1425
|
+
(sum, p) => sum + (p.status === "completed" ? p.amount : 0),
|
|
1426
|
+
0
|
|
1427
|
+
);
|
|
1428
|
+
const unpaid = earnings.totalNet - alreadyPaid;
|
|
1429
|
+
if (unpaid >= this.config.minPayoutThreshold) {
|
|
1430
|
+
eligible.push({ creatorId, amount: unpaid });
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
return eligible.sort((a, b) => b.amount - a.amount);
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* Record a payout to a creator.
|
|
1437
|
+
*/
|
|
1438
|
+
recordPayout(creatorId, amount, method, transactionHash) {
|
|
1439
|
+
const now = /* @__PURE__ */ new Date();
|
|
1440
|
+
const { start, end } = this.getPeriodBounds("monthly");
|
|
1441
|
+
const record = {
|
|
1442
|
+
id: `payout-${Date.now()}-${creatorId}`,
|
|
1443
|
+
creatorId,
|
|
1444
|
+
amount,
|
|
1445
|
+
method,
|
|
1446
|
+
transactionHash,
|
|
1447
|
+
paidAt: now.toISOString(),
|
|
1448
|
+
status: transactionHash ? "completed" : "pending",
|
|
1449
|
+
periodStart: new Date(start).toISOString(),
|
|
1450
|
+
periodEnd: new Date(end).toISOString()
|
|
1451
|
+
};
|
|
1452
|
+
this.payouts.push(record);
|
|
1453
|
+
this.emitTelemetry("payout_recorded", {
|
|
1454
|
+
creatorId,
|
|
1455
|
+
amount,
|
|
1456
|
+
method,
|
|
1457
|
+
status: record.status
|
|
1458
|
+
});
|
|
1459
|
+
return record;
|
|
1460
|
+
}
|
|
1461
|
+
/**
|
|
1462
|
+
* Update payout status.
|
|
1463
|
+
*/
|
|
1464
|
+
updatePayoutStatus(payoutId, status, transactionHash) {
|
|
1465
|
+
const payout = this.payouts.find((p) => p.id === payoutId);
|
|
1466
|
+
if (!payout) return false;
|
|
1467
|
+
payout.status = status;
|
|
1468
|
+
if (transactionHash) {
|
|
1469
|
+
payout.transactionHash = transactionHash;
|
|
1470
|
+
}
|
|
1471
|
+
return true;
|
|
1472
|
+
}
|
|
1473
|
+
/**
|
|
1474
|
+
* Get payouts for a creator.
|
|
1475
|
+
*/
|
|
1476
|
+
getCreatorPayouts(creatorId, period) {
|
|
1477
|
+
let filtered = this.payouts.filter((p) => p.creatorId === creatorId);
|
|
1478
|
+
if (period) {
|
|
1479
|
+
const { start, end } = this.getPeriodBounds(period);
|
|
1480
|
+
filtered = filtered.filter((p) => {
|
|
1481
|
+
const paidAt = new Date(p.paidAt).getTime();
|
|
1482
|
+
return paidAt >= start && paidAt < end;
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
return filtered;
|
|
1486
|
+
}
|
|
1487
|
+
/**
|
|
1488
|
+
* Get all payouts.
|
|
1489
|
+
*/
|
|
1490
|
+
getAllPayouts() {
|
|
1491
|
+
return [...this.payouts];
|
|
1492
|
+
}
|
|
1493
|
+
// ===========================================================================
|
|
1494
|
+
// QUERIES
|
|
1495
|
+
// ===========================================================================
|
|
1496
|
+
/**
|
|
1497
|
+
* Get all tracked creator IDs.
|
|
1498
|
+
*/
|
|
1499
|
+
getCreatorIds() {
|
|
1500
|
+
return [...this.events.keys()];
|
|
1501
|
+
}
|
|
1502
|
+
/**
|
|
1503
|
+
* Get platform fee rate.
|
|
1504
|
+
*/
|
|
1505
|
+
getPlatformFeeRate() {
|
|
1506
|
+
return this.config.platformFeeRate;
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Set platform fee rate.
|
|
1510
|
+
*/
|
|
1511
|
+
setPlatformFeeRate(rate) {
|
|
1512
|
+
if (rate < 0 || rate > 1) throw new Error("Fee rate must be between 0 and 1");
|
|
1513
|
+
this.config.platformFeeRate = rate;
|
|
1514
|
+
}
|
|
1515
|
+
// ===========================================================================
|
|
1516
|
+
// INTERNALS
|
|
1517
|
+
// ===========================================================================
|
|
1518
|
+
getPeriodBounds(period) {
|
|
1519
|
+
const now = /* @__PURE__ */ new Date();
|
|
1520
|
+
switch (period) {
|
|
1521
|
+
case "daily": {
|
|
1522
|
+
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
1523
|
+
return { start: start.getTime(), end: start.getTime() + 864e5 };
|
|
1524
|
+
}
|
|
1525
|
+
case "weekly": {
|
|
1526
|
+
const dayOfWeek = now.getDay();
|
|
1527
|
+
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - dayOfWeek);
|
|
1528
|
+
return { start: start.getTime(), end: start.getTime() + 7 * 864e5 };
|
|
1529
|
+
}
|
|
1530
|
+
case "monthly": {
|
|
1531
|
+
const start = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
1532
|
+
const end = new Date(now.getFullYear(), now.getMonth() + 1, 1);
|
|
1533
|
+
return { start: start.getTime(), end: end.getTime() };
|
|
1534
|
+
}
|
|
1535
|
+
case "all-time":
|
|
1536
|
+
return { start: 0, end: Date.now() + 864e5 };
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
emitTelemetry(type, data) {
|
|
1540
|
+
this.config.telemetry?.record({
|
|
1541
|
+
type,
|
|
1542
|
+
severity: "info",
|
|
1543
|
+
agentId: "creator-revenue",
|
|
1544
|
+
data
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
};
|
|
1548
|
+
|
|
1549
|
+
// src/economy/PaymentWebhookService.ts
|
|
1550
|
+
function computeHmac(payload, secret) {
|
|
1551
|
+
try {
|
|
1552
|
+
const crypto = require("crypto");
|
|
1553
|
+
return crypto.createHmac("sha256", secret).update(payload).digest("hex");
|
|
1554
|
+
} catch {
|
|
1555
|
+
let hash = 0;
|
|
1556
|
+
const combined = payload + secret;
|
|
1557
|
+
for (let i = 0; i < combined.length; i++) {
|
|
1558
|
+
const char = combined.charCodeAt(i);
|
|
1559
|
+
hash = (hash << 5) - hash + char;
|
|
1560
|
+
hash = hash & hash;
|
|
1561
|
+
}
|
|
1562
|
+
return Math.abs(hash).toString(16).padStart(8, "0");
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
function timingSafeEqual(a, b) {
|
|
1566
|
+
if (a.length !== b.length) return false;
|
|
1567
|
+
let result = 0;
|
|
1568
|
+
for (let i = 0; i < a.length; i++) {
|
|
1569
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
1570
|
+
}
|
|
1571
|
+
return result === 0;
|
|
1572
|
+
}
|
|
1573
|
+
var PaymentWebhookService = class {
|
|
1574
|
+
config;
|
|
1575
|
+
handlers = /* @__PURE__ */ new Map();
|
|
1576
|
+
processedEvents = /* @__PURE__ */ new Set();
|
|
1577
|
+
retryQueue = [];
|
|
1578
|
+
ledgerUpdateCallback;
|
|
1579
|
+
// Stats
|
|
1580
|
+
stats = {
|
|
1581
|
+
received: 0,
|
|
1582
|
+
verified: 0,
|
|
1583
|
+
processed: 0,
|
|
1584
|
+
failed: 0,
|
|
1585
|
+
retried: 0,
|
|
1586
|
+
rejected: 0,
|
|
1587
|
+
duplicates: 0
|
|
1588
|
+
};
|
|
1589
|
+
constructor(config) {
|
|
1590
|
+
this.config = {
|
|
1591
|
+
secrets: config.secrets,
|
|
1592
|
+
maxAgeMs: config.maxAgeMs ?? 5 * 60 * 1e3,
|
|
1593
|
+
// 5 minutes
|
|
1594
|
+
maxRetries: config.maxRetries ?? 3,
|
|
1595
|
+
retryBackoffMs: config.retryBackoffMs ?? 1e3,
|
|
1596
|
+
telemetry: config.telemetry
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
// ===========================================================================
|
|
1600
|
+
// VERIFICATION
|
|
1601
|
+
// ===========================================================================
|
|
1602
|
+
/**
|
|
1603
|
+
* Verify a webhook's HMAC-SHA256 signature.
|
|
1604
|
+
*/
|
|
1605
|
+
verifySignature(rawBody, signature, provider) {
|
|
1606
|
+
this.stats.received++;
|
|
1607
|
+
const secret = this.config.secrets[provider];
|
|
1608
|
+
if (!secret) {
|
|
1609
|
+
this.stats.rejected++;
|
|
1610
|
+
return {
|
|
1611
|
+
verified: false,
|
|
1612
|
+
provider,
|
|
1613
|
+
error: `No secret configured for provider: ${provider}`
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
const expected = computeHmac(rawBody, secret);
|
|
1617
|
+
const isValid = timingSafeEqual(expected, signature);
|
|
1618
|
+
if (!isValid) {
|
|
1619
|
+
this.stats.rejected++;
|
|
1620
|
+
this.emitTelemetry("webhook_signature_invalid", { provider });
|
|
1621
|
+
return {
|
|
1622
|
+
verified: false,
|
|
1623
|
+
provider,
|
|
1624
|
+
error: "Invalid HMAC-SHA256 signature"
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
let payload;
|
|
1628
|
+
try {
|
|
1629
|
+
payload = JSON.parse(rawBody);
|
|
1630
|
+
} catch {
|
|
1631
|
+
this.stats.rejected++;
|
|
1632
|
+
return {
|
|
1633
|
+
verified: false,
|
|
1634
|
+
provider,
|
|
1635
|
+
error: "Invalid JSON payload"
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
const eventAge = Date.now() - new Date(payload.timestamp).getTime();
|
|
1639
|
+
if (eventAge > this.config.maxAgeMs) {
|
|
1640
|
+
this.stats.rejected++;
|
|
1641
|
+
return {
|
|
1642
|
+
verified: false,
|
|
1643
|
+
provider,
|
|
1644
|
+
error: `Webhook too old: ${Math.round(eventAge / 1e3)}s (max: ${Math.round(this.config.maxAgeMs / 1e3)}s)`
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
this.stats.verified++;
|
|
1648
|
+
this.emitTelemetry("webhook_verified", { provider, eventId: payload.eventId });
|
|
1649
|
+
return {
|
|
1650
|
+
verified: true,
|
|
1651
|
+
provider,
|
|
1652
|
+
payload
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
// ===========================================================================
|
|
1656
|
+
// PROCESSING
|
|
1657
|
+
// ===========================================================================
|
|
1658
|
+
/**
|
|
1659
|
+
* Process a verified webhook payload.
|
|
1660
|
+
*/
|
|
1661
|
+
async processWebhook(payload) {
|
|
1662
|
+
if (this.processedEvents.has(payload.eventId)) {
|
|
1663
|
+
this.stats.duplicates++;
|
|
1664
|
+
return {
|
|
1665
|
+
success: true,
|
|
1666
|
+
eventId: payload.eventId,
|
|
1667
|
+
type: payload.type
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
let updatedEntry;
|
|
1671
|
+
if (payload.ledgerEntryId && this.ledgerUpdateCallback) {
|
|
1672
|
+
try {
|
|
1673
|
+
if (payload.type === "payment.confirmed" || payload.type === "settlement.completed") {
|
|
1674
|
+
this.ledgerUpdateCallback(payload.ledgerEntryId, {
|
|
1675
|
+
settled: true,
|
|
1676
|
+
settlementTx: payload.transactionHash || null
|
|
1677
|
+
});
|
|
1678
|
+
} else if (payload.type === "payment.failed" || payload.type === "settlement.failed") {
|
|
1679
|
+
this.ledgerUpdateCallback(payload.ledgerEntryId, {
|
|
1680
|
+
settled: false,
|
|
1681
|
+
settlementTx: null
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
} catch (err) {
|
|
1685
|
+
this.emitTelemetry("webhook_ledger_update_error", {
|
|
1686
|
+
eventId: payload.eventId,
|
|
1687
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
const handlers = this.handlers.get(payload.type) || [];
|
|
1692
|
+
const errors = [];
|
|
1693
|
+
for (const handler of handlers) {
|
|
1694
|
+
try {
|
|
1695
|
+
const result = await handler(payload);
|
|
1696
|
+
if (!result.success && result.error) {
|
|
1697
|
+
errors.push(result.error);
|
|
1698
|
+
}
|
|
1699
|
+
if (result.updatedEntry) {
|
|
1700
|
+
updatedEntry = result.updatedEntry;
|
|
1701
|
+
}
|
|
1702
|
+
} catch (err) {
|
|
1703
|
+
errors.push(err instanceof Error ? err.message : String(err));
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
if (errors.length > 0) {
|
|
1707
|
+
this.stats.failed++;
|
|
1708
|
+
this.emitTelemetry("webhook_processing_failed", {
|
|
1709
|
+
eventId: payload.eventId,
|
|
1710
|
+
type: payload.type,
|
|
1711
|
+
errors
|
|
1712
|
+
});
|
|
1713
|
+
this.addToRetryQueue(payload, errors.join("; "));
|
|
1714
|
+
return {
|
|
1715
|
+
success: false,
|
|
1716
|
+
eventId: payload.eventId,
|
|
1717
|
+
type: payload.type,
|
|
1718
|
+
error: errors.join("; ")
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
this.processedEvents.add(payload.eventId);
|
|
1722
|
+
if (this.processedEvents.size > 1e4) {
|
|
1723
|
+
const entries = [...this.processedEvents];
|
|
1724
|
+
this.processedEvents = new Set(entries.slice(-5e3));
|
|
1725
|
+
}
|
|
1726
|
+
this.stats.processed++;
|
|
1727
|
+
this.emitTelemetry("webhook_processed", {
|
|
1728
|
+
eventId: payload.eventId,
|
|
1729
|
+
type: payload.type,
|
|
1730
|
+
provider: payload.provider
|
|
1731
|
+
});
|
|
1732
|
+
return {
|
|
1733
|
+
success: true,
|
|
1734
|
+
eventId: payload.eventId,
|
|
1735
|
+
type: payload.type,
|
|
1736
|
+
updatedEntry
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
// ===========================================================================
|
|
1740
|
+
// HANDLER REGISTRATION
|
|
1741
|
+
// ===========================================================================
|
|
1742
|
+
/**
|
|
1743
|
+
* Register a handler for a specific webhook event type.
|
|
1744
|
+
*/
|
|
1745
|
+
on(eventType, handler) {
|
|
1746
|
+
const existing = this.handlers.get(eventType) || [];
|
|
1747
|
+
existing.push(handler);
|
|
1748
|
+
this.handlers.set(eventType, existing);
|
|
1749
|
+
}
|
|
1750
|
+
/**
|
|
1751
|
+
* Remove a handler for a specific webhook event type.
|
|
1752
|
+
*/
|
|
1753
|
+
off(eventType, handler) {
|
|
1754
|
+
const existing = this.handlers.get(eventType) || [];
|
|
1755
|
+
this.handlers.set(
|
|
1756
|
+
eventType,
|
|
1757
|
+
existing.filter((h) => h !== handler)
|
|
1758
|
+
);
|
|
1759
|
+
}
|
|
1760
|
+
/**
|
|
1761
|
+
* Set callback for ledger entry updates.
|
|
1762
|
+
*/
|
|
1763
|
+
onLedgerUpdate(callback) {
|
|
1764
|
+
this.ledgerUpdateCallback = callback;
|
|
1765
|
+
}
|
|
1766
|
+
// ===========================================================================
|
|
1767
|
+
// RETRY QUEUE
|
|
1768
|
+
// ===========================================================================
|
|
1769
|
+
/**
|
|
1770
|
+
* Add a failed webhook to the retry queue.
|
|
1771
|
+
*/
|
|
1772
|
+
addToRetryQueue(payload, error) {
|
|
1773
|
+
const existing = this.retryQueue.find((r) => r.payload.eventId === payload.eventId);
|
|
1774
|
+
if (existing) {
|
|
1775
|
+
existing.attempts++;
|
|
1776
|
+
existing.lastError = error;
|
|
1777
|
+
existing.nextRetryAt = Date.now() + this.config.retryBackoffMs * Math.pow(2, existing.attempts - 1);
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
if (this.retryQueue.length >= 1e3) {
|
|
1781
|
+
this.retryQueue.shift();
|
|
1782
|
+
}
|
|
1783
|
+
this.retryQueue.push({
|
|
1784
|
+
payload,
|
|
1785
|
+
attempts: 1,
|
|
1786
|
+
nextRetryAt: Date.now() + this.config.retryBackoffMs,
|
|
1787
|
+
lastError: error
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
1790
|
+
/**
|
|
1791
|
+
* Process retry queue entries that are ready.
|
|
1792
|
+
*/
|
|
1793
|
+
async processRetryQueue() {
|
|
1794
|
+
const now = Date.now();
|
|
1795
|
+
const ready = this.retryQueue.filter(
|
|
1796
|
+
(r) => r.nextRetryAt <= now && r.attempts < this.config.maxRetries
|
|
1797
|
+
);
|
|
1798
|
+
let processed = 0;
|
|
1799
|
+
for (const entry of ready) {
|
|
1800
|
+
const result = await this.processWebhook(entry.payload);
|
|
1801
|
+
if (result.success) {
|
|
1802
|
+
const idx = this.retryQueue.indexOf(entry);
|
|
1803
|
+
if (idx >= 0) this.retryQueue.splice(idx, 1);
|
|
1804
|
+
this.stats.retried++;
|
|
1805
|
+
processed++;
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
this.retryQueue = this.retryQueue.filter((r) => r.attempts < this.config.maxRetries);
|
|
1809
|
+
return processed;
|
|
1810
|
+
}
|
|
1811
|
+
/**
|
|
1812
|
+
* Get retry queue length.
|
|
1813
|
+
*/
|
|
1814
|
+
getRetryQueueLength() {
|
|
1815
|
+
return this.retryQueue.length;
|
|
1816
|
+
}
|
|
1817
|
+
// ===========================================================================
|
|
1818
|
+
// QUERIES
|
|
1819
|
+
// ===========================================================================
|
|
1820
|
+
/**
|
|
1821
|
+
* Check if an event has been processed.
|
|
1822
|
+
*/
|
|
1823
|
+
isProcessed(eventId) {
|
|
1824
|
+
return this.processedEvents.has(eventId);
|
|
1825
|
+
}
|
|
1826
|
+
/**
|
|
1827
|
+
* Get comprehensive stats.
|
|
1828
|
+
*/
|
|
1829
|
+
getStats() {
|
|
1830
|
+
return {
|
|
1831
|
+
...this.stats,
|
|
1832
|
+
retryQueueLength: this.retryQueue.length
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1835
|
+
/**
|
|
1836
|
+
* Create a webhook signature for testing purposes.
|
|
1837
|
+
*/
|
|
1838
|
+
createSignature(rawBody, provider) {
|
|
1839
|
+
const secret = this.config.secrets[provider];
|
|
1840
|
+
if (!secret) throw new Error(`No secret for provider: ${provider}`);
|
|
1841
|
+
return computeHmac(rawBody, secret);
|
|
1842
|
+
}
|
|
1843
|
+
// ===========================================================================
|
|
1844
|
+
// TELEMETRY
|
|
1845
|
+
// ===========================================================================
|
|
1846
|
+
emitTelemetry(type, data) {
|
|
1847
|
+
this.config.telemetry?.record({
|
|
1848
|
+
type,
|
|
1849
|
+
severity: type.includes("error") || type.includes("failed") || type.includes("invalid") ? "error" : "info",
|
|
1850
|
+
agentId: "payment-webhook-service",
|
|
1851
|
+
data
|
|
1852
|
+
});
|
|
1853
|
+
}
|
|
1854
|
+
};
|
|
1855
|
+
|
|
1856
|
+
// src/economy/SubscriptionManager.ts
|
|
1857
|
+
var SubscriptionManager = class {
|
|
1858
|
+
config;
|
|
1859
|
+
subscriptions = /* @__PURE__ */ new Map();
|
|
1860
|
+
plans = /* @__PURE__ */ new Map();
|
|
1861
|
+
renewalCallback;
|
|
1862
|
+
subCounter = 0;
|
|
1863
|
+
constructor(config) {
|
|
1864
|
+
this.config = {
|
|
1865
|
+
gracePeriodDays: config?.gracePeriodDays ?? 3,
|
|
1866
|
+
maxFailedRenewals: config?.maxFailedRenewals ?? 3,
|
|
1867
|
+
telemetry: config?.telemetry
|
|
1868
|
+
};
|
|
1869
|
+
}
|
|
1870
|
+
// ===========================================================================
|
|
1871
|
+
// PLAN MANAGEMENT
|
|
1872
|
+
// ===========================================================================
|
|
1873
|
+
/**
|
|
1874
|
+
* Register a subscription plan.
|
|
1875
|
+
*/
|
|
1876
|
+
registerPlan(plan) {
|
|
1877
|
+
this.plans.set(plan.id, { ...plan });
|
|
1878
|
+
}
|
|
1879
|
+
/**
|
|
1880
|
+
* Get a plan by ID.
|
|
1881
|
+
*/
|
|
1882
|
+
getPlan(planId) {
|
|
1883
|
+
return this.plans.get(planId);
|
|
1884
|
+
}
|
|
1885
|
+
/**
|
|
1886
|
+
* List all plans.
|
|
1887
|
+
*/
|
|
1888
|
+
listPlans() {
|
|
1889
|
+
return [...this.plans.values()];
|
|
1890
|
+
}
|
|
1891
|
+
// ===========================================================================
|
|
1892
|
+
// LIFECYCLE: CREATE
|
|
1893
|
+
// ===========================================================================
|
|
1894
|
+
/**
|
|
1895
|
+
* Create a new subscription.
|
|
1896
|
+
*/
|
|
1897
|
+
create(subscriberId, planId, metadata) {
|
|
1898
|
+
const plan = this.plans.get(planId);
|
|
1899
|
+
if (!plan) {
|
|
1900
|
+
throw new Error(`Plan "${planId}" not found`);
|
|
1901
|
+
}
|
|
1902
|
+
const id = `sub-${++this.subCounter}`;
|
|
1903
|
+
const now = /* @__PURE__ */ new Date();
|
|
1904
|
+
const hasTrial = plan.trialDays > 0;
|
|
1905
|
+
const trialEnd = hasTrial ? new Date(now.getTime() + plan.trialDays * 864e5).toISOString() : null;
|
|
1906
|
+
const periodEnd = this.computePeriodEnd(now, plan.interval);
|
|
1907
|
+
const subscription = {
|
|
1908
|
+
id,
|
|
1909
|
+
subscriberId,
|
|
1910
|
+
planId,
|
|
1911
|
+
state: hasTrial ? "trial" : "active",
|
|
1912
|
+
amount: plan.amount,
|
|
1913
|
+
interval: plan.interval,
|
|
1914
|
+
createdAt: now.toISOString(),
|
|
1915
|
+
currentPeriodStart: now.toISOString(),
|
|
1916
|
+
currentPeriodEnd: periodEnd.toISOString(),
|
|
1917
|
+
trialEnd,
|
|
1918
|
+
inTrial: hasTrial,
|
|
1919
|
+
failedRenewals: 0,
|
|
1920
|
+
gracePeriodEnd: null,
|
|
1921
|
+
cancelledAt: null,
|
|
1922
|
+
cancelAtPeriodEnd: false,
|
|
1923
|
+
metadata
|
|
1924
|
+
};
|
|
1925
|
+
this.subscriptions.set(id, subscription);
|
|
1926
|
+
this.emitTelemetry("subscription_created", {
|
|
1927
|
+
subscriptionId: id,
|
|
1928
|
+
subscriberId,
|
|
1929
|
+
planId,
|
|
1930
|
+
state: subscription.state
|
|
1931
|
+
});
|
|
1932
|
+
return subscription;
|
|
1933
|
+
}
|
|
1934
|
+
// ===========================================================================
|
|
1935
|
+
// LIFECYCLE: RENEW
|
|
1936
|
+
// ===========================================================================
|
|
1937
|
+
/**
|
|
1938
|
+
* Attempt to renew a subscription.
|
|
1939
|
+
*/
|
|
1940
|
+
async renew(subscriptionId) {
|
|
1941
|
+
const sub = this.requireSubscription(subscriptionId);
|
|
1942
|
+
if (sub.state === "cancelled" || sub.state === "expired") {
|
|
1943
|
+
return {
|
|
1944
|
+
success: false,
|
|
1945
|
+
subscription: sub,
|
|
1946
|
+
error: `Cannot renew ${sub.state} subscription`
|
|
1947
|
+
};
|
|
1948
|
+
}
|
|
1949
|
+
if (sub.inTrial && sub.trialEnd) {
|
|
1950
|
+
const trialEndMs = new Date(sub.trialEnd).getTime();
|
|
1951
|
+
if (Date.now() >= trialEndMs) {
|
|
1952
|
+
sub.inTrial = false;
|
|
1953
|
+
sub.state = "active";
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
let paymentSuccess = true;
|
|
1957
|
+
if (this.renewalCallback) {
|
|
1958
|
+
try {
|
|
1959
|
+
paymentSuccess = await this.renewalCallback(subscriptionId, sub.amount);
|
|
1960
|
+
} catch {
|
|
1961
|
+
paymentSuccess = false;
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
if (paymentSuccess) {
|
|
1965
|
+
const now = /* @__PURE__ */ new Date();
|
|
1966
|
+
sub.currentPeriodStart = now.toISOString();
|
|
1967
|
+
sub.currentPeriodEnd = this.computePeriodEnd(now, sub.interval).toISOString();
|
|
1968
|
+
sub.state = "active";
|
|
1969
|
+
sub.failedRenewals = 0;
|
|
1970
|
+
sub.gracePeriodEnd = null;
|
|
1971
|
+
if (sub.cancelAtPeriodEnd) {
|
|
1972
|
+
sub.state = "cancelled";
|
|
1973
|
+
sub.cancelledAt = now.toISOString();
|
|
1974
|
+
sub.cancelAtPeriodEnd = false;
|
|
1975
|
+
this.emitTelemetry("subscription_cancelled_at_period_end", { subscriptionId });
|
|
1976
|
+
return { success: true, subscription: sub };
|
|
1977
|
+
}
|
|
1978
|
+
this.emitTelemetry("subscription_renewed", { subscriptionId });
|
|
1979
|
+
return { success: true, subscription: sub };
|
|
1980
|
+
}
|
|
1981
|
+
sub.failedRenewals++;
|
|
1982
|
+
if (sub.failedRenewals >= this.config.maxFailedRenewals) {
|
|
1983
|
+
sub.state = "suspended";
|
|
1984
|
+
this.emitTelemetry("subscription_suspended", {
|
|
1985
|
+
subscriptionId,
|
|
1986
|
+
failedRenewals: sub.failedRenewals
|
|
1987
|
+
});
|
|
1988
|
+
return {
|
|
1989
|
+
success: false,
|
|
1990
|
+
subscription: sub,
|
|
1991
|
+
error: `Subscription suspended after ${sub.failedRenewals} failed renewals`
|
|
1992
|
+
};
|
|
1993
|
+
}
|
|
1994
|
+
sub.state = "past_due";
|
|
1995
|
+
const gracePeriodEnd = new Date(
|
|
1996
|
+
Date.now() + this.config.gracePeriodDays * 864e5
|
|
1997
|
+
).toISOString();
|
|
1998
|
+
sub.gracePeriodEnd = gracePeriodEnd;
|
|
1999
|
+
this.emitTelemetry("subscription_renewal_failed", {
|
|
2000
|
+
subscriptionId,
|
|
2001
|
+
failedRenewals: sub.failedRenewals,
|
|
2002
|
+
gracePeriodEnd
|
|
2003
|
+
});
|
|
2004
|
+
return {
|
|
2005
|
+
success: false,
|
|
2006
|
+
subscription: sub,
|
|
2007
|
+
error: `Renewal failed (attempt ${sub.failedRenewals}/${this.config.maxFailedRenewals})`,
|
|
2008
|
+
enteredGracePeriod: true
|
|
2009
|
+
};
|
|
2010
|
+
}
|
|
2011
|
+
// ===========================================================================
|
|
2012
|
+
// LIFECYCLE: CANCEL
|
|
2013
|
+
// ===========================================================================
|
|
2014
|
+
/**
|
|
2015
|
+
* Cancel a subscription.
|
|
2016
|
+
* @param immediate If true, cancel immediately; otherwise cancel at period end.
|
|
2017
|
+
*/
|
|
2018
|
+
cancel(subscriptionId, immediate = false) {
|
|
2019
|
+
const sub = this.requireSubscription(subscriptionId);
|
|
2020
|
+
if (sub.state === "cancelled" || sub.state === "expired") {
|
|
2021
|
+
throw new Error(`Subscription already ${sub.state}`);
|
|
2022
|
+
}
|
|
2023
|
+
if (immediate) {
|
|
2024
|
+
sub.state = "cancelled";
|
|
2025
|
+
sub.cancelledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2026
|
+
sub.cancelAtPeriodEnd = false;
|
|
2027
|
+
} else {
|
|
2028
|
+
sub.cancelAtPeriodEnd = true;
|
|
2029
|
+
}
|
|
2030
|
+
this.emitTelemetry("subscription_cancel_requested", {
|
|
2031
|
+
subscriptionId,
|
|
2032
|
+
immediate
|
|
2033
|
+
});
|
|
2034
|
+
return sub;
|
|
2035
|
+
}
|
|
2036
|
+
// ===========================================================================
|
|
2037
|
+
// LIFECYCLE: SUSPEND / REACTIVATE
|
|
2038
|
+
// ===========================================================================
|
|
2039
|
+
/**
|
|
2040
|
+
* Suspend a subscription (e.g., after payment failures).
|
|
2041
|
+
*/
|
|
2042
|
+
suspend(subscriptionId) {
|
|
2043
|
+
const sub = this.requireSubscription(subscriptionId);
|
|
2044
|
+
sub.state = "suspended";
|
|
2045
|
+
this.emitTelemetry("subscription_suspended", { subscriptionId });
|
|
2046
|
+
return sub;
|
|
2047
|
+
}
|
|
2048
|
+
/**
|
|
2049
|
+
* Reactivate a suspended or cancelled subscription.
|
|
2050
|
+
*/
|
|
2051
|
+
reactivate(subscriptionId) {
|
|
2052
|
+
const sub = this.requireSubscription(subscriptionId);
|
|
2053
|
+
if (sub.state !== "suspended" && sub.state !== "cancelled" && sub.state !== "past_due") {
|
|
2054
|
+
throw new Error(`Cannot reactivate subscription in state: ${sub.state}`);
|
|
2055
|
+
}
|
|
2056
|
+
const now = /* @__PURE__ */ new Date();
|
|
2057
|
+
sub.state = "active";
|
|
2058
|
+
sub.failedRenewals = 0;
|
|
2059
|
+
sub.gracePeriodEnd = null;
|
|
2060
|
+
sub.cancelledAt = null;
|
|
2061
|
+
sub.cancelAtPeriodEnd = false;
|
|
2062
|
+
sub.currentPeriodStart = now.toISOString();
|
|
2063
|
+
sub.currentPeriodEnd = this.computePeriodEnd(now, sub.interval).toISOString();
|
|
2064
|
+
this.emitTelemetry("subscription_reactivated", { subscriptionId });
|
|
2065
|
+
return sub;
|
|
2066
|
+
}
|
|
2067
|
+
// ===========================================================================
|
|
2068
|
+
// RENEWAL CALLBACK
|
|
2069
|
+
// ===========================================================================
|
|
2070
|
+
/**
|
|
2071
|
+
* Set callback for renewal payment processing.
|
|
2072
|
+
* Should return true if payment succeeded.
|
|
2073
|
+
*/
|
|
2074
|
+
onRenewal(callback) {
|
|
2075
|
+
this.renewalCallback = callback;
|
|
2076
|
+
}
|
|
2077
|
+
// ===========================================================================
|
|
2078
|
+
// QUERIES
|
|
2079
|
+
// ===========================================================================
|
|
2080
|
+
/**
|
|
2081
|
+
* Get a subscription by ID.
|
|
2082
|
+
*/
|
|
2083
|
+
getSubscription(id) {
|
|
2084
|
+
return this.subscriptions.get(id);
|
|
2085
|
+
}
|
|
2086
|
+
/**
|
|
2087
|
+
* Get all subscriptions for a subscriber.
|
|
2088
|
+
*/
|
|
2089
|
+
getSubscriberSubscriptions(subscriberId) {
|
|
2090
|
+
return [...this.subscriptions.values()].filter((s) => s.subscriberId === subscriberId);
|
|
2091
|
+
}
|
|
2092
|
+
/**
|
|
2093
|
+
* Get subscriptions by state.
|
|
2094
|
+
*/
|
|
2095
|
+
getByState(state) {
|
|
2096
|
+
return [...this.subscriptions.values()].filter((s) => s.state === state);
|
|
2097
|
+
}
|
|
2098
|
+
/**
|
|
2099
|
+
* Get subscriptions due for renewal.
|
|
2100
|
+
*/
|
|
2101
|
+
getDueForRenewal() {
|
|
2102
|
+
const now = Date.now();
|
|
2103
|
+
return [...this.subscriptions.values()].filter((s) => {
|
|
2104
|
+
if (s.state === "cancelled" || s.state === "expired" || s.state === "suspended") {
|
|
2105
|
+
return false;
|
|
2106
|
+
}
|
|
2107
|
+
return new Date(s.currentPeriodEnd).getTime() <= now;
|
|
2108
|
+
});
|
|
2109
|
+
}
|
|
2110
|
+
/**
|
|
2111
|
+
* Get subscriptions in grace period.
|
|
2112
|
+
*/
|
|
2113
|
+
getInGracePeriod() {
|
|
2114
|
+
return [...this.subscriptions.values()].filter(
|
|
2115
|
+
(s) => s.state === "past_due" && s.gracePeriodEnd !== null
|
|
2116
|
+
);
|
|
2117
|
+
}
|
|
2118
|
+
/**
|
|
2119
|
+
* Check if grace period has expired for past_due subscriptions.
|
|
2120
|
+
*/
|
|
2121
|
+
processExpiredGracePeriods() {
|
|
2122
|
+
const now = Date.now();
|
|
2123
|
+
const expired = [];
|
|
2124
|
+
for (const sub of this.subscriptions.values()) {
|
|
2125
|
+
if (sub.state === "past_due" && sub.gracePeriodEnd) {
|
|
2126
|
+
if (new Date(sub.gracePeriodEnd).getTime() <= now) {
|
|
2127
|
+
sub.state = "suspended";
|
|
2128
|
+
sub.gracePeriodEnd = null;
|
|
2129
|
+
expired.push(sub);
|
|
2130
|
+
this.emitTelemetry("subscription_grace_expired", { subscriptionId: sub.id });
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
return expired;
|
|
2135
|
+
}
|
|
2136
|
+
/**
|
|
2137
|
+
* Get stats.
|
|
2138
|
+
*/
|
|
2139
|
+
getStats() {
|
|
2140
|
+
const byState = {};
|
|
2141
|
+
let totalMRR = 0;
|
|
2142
|
+
for (const sub of this.subscriptions.values()) {
|
|
2143
|
+
byState[sub.state] = (byState[sub.state] || 0) + 1;
|
|
2144
|
+
if (sub.state === "active" || sub.state === "trial") {
|
|
2145
|
+
totalMRR += this.normalizeToMonthly(sub.amount, sub.interval);
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
return {
|
|
2149
|
+
total: this.subscriptions.size,
|
|
2150
|
+
byState,
|
|
2151
|
+
totalMRR,
|
|
2152
|
+
planCount: this.plans.size
|
|
2153
|
+
};
|
|
2154
|
+
}
|
|
2155
|
+
/**
|
|
2156
|
+
* Total subscription count.
|
|
2157
|
+
*/
|
|
2158
|
+
getSubscriptionCount() {
|
|
2159
|
+
return this.subscriptions.size;
|
|
2160
|
+
}
|
|
2161
|
+
// ===========================================================================
|
|
2162
|
+
// INTERNALS
|
|
2163
|
+
// ===========================================================================
|
|
2164
|
+
requireSubscription(id) {
|
|
2165
|
+
const sub = this.subscriptions.get(id);
|
|
2166
|
+
if (!sub) throw new Error(`Subscription "${id}" not found`);
|
|
2167
|
+
return sub;
|
|
2168
|
+
}
|
|
2169
|
+
computePeriodEnd(start, interval) {
|
|
2170
|
+
const end = new Date(start);
|
|
2171
|
+
switch (interval) {
|
|
2172
|
+
case "daily":
|
|
2173
|
+
end.setDate(end.getDate() + 1);
|
|
2174
|
+
break;
|
|
2175
|
+
case "weekly":
|
|
2176
|
+
end.setDate(end.getDate() + 7);
|
|
2177
|
+
break;
|
|
2178
|
+
case "monthly":
|
|
2179
|
+
end.setMonth(end.getMonth() + 1);
|
|
2180
|
+
break;
|
|
2181
|
+
case "yearly":
|
|
2182
|
+
end.setFullYear(end.getFullYear() + 1);
|
|
2183
|
+
break;
|
|
2184
|
+
}
|
|
2185
|
+
return end;
|
|
2186
|
+
}
|
|
2187
|
+
normalizeToMonthly(amount, interval) {
|
|
2188
|
+
switch (interval) {
|
|
2189
|
+
case "daily":
|
|
2190
|
+
return amount * 30;
|
|
2191
|
+
case "weekly":
|
|
2192
|
+
return amount * 4;
|
|
2193
|
+
case "monthly":
|
|
2194
|
+
return amount;
|
|
2195
|
+
case "yearly":
|
|
2196
|
+
return Math.floor(amount / 12);
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
emitTelemetry(type, data) {
|
|
2200
|
+
this.config.telemetry?.record({
|
|
2201
|
+
type,
|
|
2202
|
+
severity: type.includes("failed") || type.includes("suspended") ? "warning" : "info",
|
|
2203
|
+
agentId: "subscription-manager",
|
|
2204
|
+
data
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2207
|
+
};
|
|
2208
|
+
|
|
2209
|
+
// src/economy/AgentBudgetEnforcer.ts
|
|
2210
|
+
var AgentBudgetEnforcer = class {
|
|
2211
|
+
trackers = /* @__PURE__ */ new Map();
|
|
2212
|
+
config;
|
|
2213
|
+
sessionCounter = 0;
|
|
2214
|
+
constructor(config) {
|
|
2215
|
+
this.config = {
|
|
2216
|
+
defaultBudget: {
|
|
2217
|
+
agentId: "",
|
|
2218
|
+
maxSpend: 1e7,
|
|
2219
|
+
// $10.00 default
|
|
2220
|
+
period: "daily",
|
|
2221
|
+
mode: "soft",
|
|
2222
|
+
warnThreshold: 0.8,
|
|
2223
|
+
circuitBreakerThreshold: 5,
|
|
2224
|
+
...config?.defaultBudget
|
|
2225
|
+
},
|
|
2226
|
+
circuitBreakerResetMs: config?.circuitBreakerResetMs ?? 6e4,
|
|
2227
|
+
telemetry: config?.telemetry
|
|
2228
|
+
};
|
|
2229
|
+
}
|
|
2230
|
+
// ===========================================================================
|
|
2231
|
+
// BUDGET MANAGEMENT
|
|
2232
|
+
// ===========================================================================
|
|
2233
|
+
/**
|
|
2234
|
+
* Set budget for an agent.
|
|
2235
|
+
*/
|
|
2236
|
+
setBudget(budget) {
|
|
2237
|
+
const existing = this.trackers.get(budget.agentId);
|
|
2238
|
+
if (existing) {
|
|
2239
|
+
existing.budget = { ...budget };
|
|
2240
|
+
} else {
|
|
2241
|
+
this.trackers.set(budget.agentId, {
|
|
2242
|
+
budget: { ...budget },
|
|
2243
|
+
spent: 0,
|
|
2244
|
+
requestCount: 0,
|
|
2245
|
+
periodStart: Date.now(),
|
|
2246
|
+
consecutiveFailures: 0,
|
|
2247
|
+
circuitBreakerTrippedAt: null,
|
|
2248
|
+
sessionId: `session-${++this.sessionCounter}`
|
|
2249
|
+
});
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
/**
|
|
2253
|
+
* Get budget configuration for an agent.
|
|
2254
|
+
*/
|
|
2255
|
+
getBudget(agentId) {
|
|
2256
|
+
return this.trackers.get(agentId)?.budget;
|
|
2257
|
+
}
|
|
2258
|
+
/**
|
|
2259
|
+
* Remove budget for an agent.
|
|
2260
|
+
*/
|
|
2261
|
+
removeBudget(agentId) {
|
|
2262
|
+
return this.trackers.delete(agentId);
|
|
2263
|
+
}
|
|
2264
|
+
// ===========================================================================
|
|
2265
|
+
// AUTHORIZATION
|
|
2266
|
+
// ===========================================================================
|
|
2267
|
+
/**
|
|
2268
|
+
* Check if an agent is authorized to spend a given amount.
|
|
2269
|
+
*/
|
|
2270
|
+
authorize(agentId, amount) {
|
|
2271
|
+
const tracker = this.getOrCreateTracker(agentId);
|
|
2272
|
+
this.checkPeriodReset(tracker);
|
|
2273
|
+
const state = this.buildState(tracker);
|
|
2274
|
+
if (state.circuitBreaker.isOpen) {
|
|
2275
|
+
if (tracker.circuitBreakerTrippedAt) {
|
|
2276
|
+
const elapsed = Date.now() - tracker.circuitBreakerTrippedAt;
|
|
2277
|
+
if (elapsed >= this.config.circuitBreakerResetMs) {
|
|
2278
|
+
tracker.consecutiveFailures = 0;
|
|
2279
|
+
tracker.circuitBreakerTrippedAt = null;
|
|
2280
|
+
} else {
|
|
2281
|
+
this.emitTelemetry("budget_circuit_breaker_blocked", { agentId, amount });
|
|
2282
|
+
return {
|
|
2283
|
+
authorized: false,
|
|
2284
|
+
reason: `Circuit breaker open: ${tracker.consecutiveFailures} consecutive failures. Resets in ${Math.ceil((this.config.circuitBreakerResetMs - elapsed) / 1e3)}s`,
|
|
2285
|
+
state: this.buildState(tracker)
|
|
2286
|
+
};
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
const wouldExceed = tracker.spent + amount > tracker.budget.maxSpend;
|
|
2291
|
+
const warnThreshold = tracker.budget.warnThreshold ?? 0.8;
|
|
2292
|
+
const atWarningLevel = (tracker.spent + amount) / tracker.budget.maxSpend >= warnThreshold;
|
|
2293
|
+
if (wouldExceed) {
|
|
2294
|
+
switch (tracker.budget.mode) {
|
|
2295
|
+
case "hard":
|
|
2296
|
+
this.emitTelemetry("budget_hard_denied", {
|
|
2297
|
+
agentId,
|
|
2298
|
+
amount,
|
|
2299
|
+
spent: tracker.spent,
|
|
2300
|
+
limit: tracker.budget.maxSpend
|
|
2301
|
+
});
|
|
2302
|
+
return {
|
|
2303
|
+
authorized: false,
|
|
2304
|
+
reason: `Budget exhausted (hard limit): spent ${tracker.spent} + ${amount} > limit ${tracker.budget.maxSpend}`,
|
|
2305
|
+
state: this.buildState(tracker)
|
|
2306
|
+
};
|
|
2307
|
+
case "soft":
|
|
2308
|
+
this.emitTelemetry("budget_soft_denied", { agentId, amount, spent: tracker.spent });
|
|
2309
|
+
return {
|
|
2310
|
+
authorized: false,
|
|
2311
|
+
reason: `Budget exhausted (soft limit): spent ${tracker.spent} + ${amount} > limit ${tracker.budget.maxSpend}`,
|
|
2312
|
+
state: this.buildState(tracker)
|
|
2313
|
+
};
|
|
2314
|
+
case "warn":
|
|
2315
|
+
this.emitTelemetry("budget_warn_overspend", { agentId, amount });
|
|
2316
|
+
return {
|
|
2317
|
+
authorized: true,
|
|
2318
|
+
state: this.buildState(tracker),
|
|
2319
|
+
warning: true,
|
|
2320
|
+
warningMessage: `Budget exceeded: spent ${tracker.spent} + ${amount} > limit ${tracker.budget.maxSpend}`
|
|
2321
|
+
};
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
if (atWarningLevel && !wouldExceed) {
|
|
2325
|
+
return {
|
|
2326
|
+
authorized: true,
|
|
2327
|
+
state: this.buildState(tracker),
|
|
2328
|
+
warning: true,
|
|
2329
|
+
warningMessage: `Approaching budget limit: ${Math.round((tracker.spent + amount) / tracker.budget.maxSpend * 100)}% used`
|
|
2330
|
+
};
|
|
2331
|
+
}
|
|
2332
|
+
return {
|
|
2333
|
+
authorized: true,
|
|
2334
|
+
state: this.buildState(tracker)
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
2337
|
+
/**
|
|
2338
|
+
* Record a spend. Call after a successful tool execution.
|
|
2339
|
+
*/
|
|
2340
|
+
recordSpend(agentId, amount) {
|
|
2341
|
+
const tracker = this.getOrCreateTracker(agentId);
|
|
2342
|
+
this.checkPeriodReset(tracker);
|
|
2343
|
+
tracker.spent += amount;
|
|
2344
|
+
tracker.requestCount++;
|
|
2345
|
+
tracker.consecutiveFailures = 0;
|
|
2346
|
+
this.emitTelemetry("budget_spend_recorded", { agentId, amount, totalSpent: tracker.spent });
|
|
2347
|
+
}
|
|
2348
|
+
/**
|
|
2349
|
+
* Record a failure. Increments circuit breaker counter.
|
|
2350
|
+
*/
|
|
2351
|
+
recordFailure(agentId) {
|
|
2352
|
+
const tracker = this.getOrCreateTracker(agentId);
|
|
2353
|
+
tracker.consecutiveFailures++;
|
|
2354
|
+
const threshold = tracker.budget.circuitBreakerThreshold ?? 5;
|
|
2355
|
+
if (tracker.consecutiveFailures >= threshold && !tracker.circuitBreakerTrippedAt) {
|
|
2356
|
+
tracker.circuitBreakerTrippedAt = Date.now();
|
|
2357
|
+
this.emitTelemetry("budget_circuit_breaker_tripped", {
|
|
2358
|
+
agentId,
|
|
2359
|
+
failures: tracker.consecutiveFailures,
|
|
2360
|
+
threshold
|
|
2361
|
+
});
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
// ===========================================================================
|
|
2365
|
+
// QUERIES
|
|
2366
|
+
// ===========================================================================
|
|
2367
|
+
/**
|
|
2368
|
+
* Get current budget state for an agent.
|
|
2369
|
+
*/
|
|
2370
|
+
getState(agentId) {
|
|
2371
|
+
const tracker = this.trackers.get(agentId);
|
|
2372
|
+
if (!tracker) return void 0;
|
|
2373
|
+
this.checkPeriodReset(tracker);
|
|
2374
|
+
return this.buildState(tracker);
|
|
2375
|
+
}
|
|
2376
|
+
/**
|
|
2377
|
+
* Get all agent budget states.
|
|
2378
|
+
*/
|
|
2379
|
+
getAllStates() {
|
|
2380
|
+
return [...this.trackers.keys()].map((id) => this.getState(id)).filter(Boolean);
|
|
2381
|
+
}
|
|
2382
|
+
/**
|
|
2383
|
+
* Get agents that are over budget.
|
|
2384
|
+
*/
|
|
2385
|
+
getOverBudgetAgents() {
|
|
2386
|
+
return this.getAllStates().filter((s) => s.exhausted || s.circuitBreaker.isOpen);
|
|
2387
|
+
}
|
|
2388
|
+
/**
|
|
2389
|
+
* Reset an agent's period spending.
|
|
2390
|
+
*/
|
|
2391
|
+
resetSpending(agentId) {
|
|
2392
|
+
const tracker = this.trackers.get(agentId);
|
|
2393
|
+
if (tracker) {
|
|
2394
|
+
tracker.spent = 0;
|
|
2395
|
+
tracker.requestCount = 0;
|
|
2396
|
+
tracker.periodStart = Date.now();
|
|
2397
|
+
tracker.consecutiveFailures = 0;
|
|
2398
|
+
tracker.circuitBreakerTrippedAt = null;
|
|
2399
|
+
tracker.sessionId = `session-${++this.sessionCounter}`;
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
/**
|
|
2403
|
+
* Reset circuit breaker for an agent.
|
|
2404
|
+
*/
|
|
2405
|
+
resetCircuitBreaker(agentId) {
|
|
2406
|
+
const tracker = this.trackers.get(agentId);
|
|
2407
|
+
if (tracker) {
|
|
2408
|
+
tracker.consecutiveFailures = 0;
|
|
2409
|
+
tracker.circuitBreakerTrippedAt = null;
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
// ===========================================================================
|
|
2413
|
+
// INTERNALS
|
|
2414
|
+
// ===========================================================================
|
|
2415
|
+
getOrCreateTracker(agentId) {
|
|
2416
|
+
let tracker = this.trackers.get(agentId);
|
|
2417
|
+
if (!tracker) {
|
|
2418
|
+
tracker = {
|
|
2419
|
+
budget: { ...this.config.defaultBudget, agentId },
|
|
2420
|
+
spent: 0,
|
|
2421
|
+
requestCount: 0,
|
|
2422
|
+
periodStart: Date.now(),
|
|
2423
|
+
consecutiveFailures: 0,
|
|
2424
|
+
circuitBreakerTrippedAt: null,
|
|
2425
|
+
sessionId: `session-${++this.sessionCounter}`
|
|
2426
|
+
};
|
|
2427
|
+
this.trackers.set(agentId, tracker);
|
|
2428
|
+
}
|
|
2429
|
+
return tracker;
|
|
2430
|
+
}
|
|
2431
|
+
checkPeriodReset(tracker) {
|
|
2432
|
+
const now = Date.now();
|
|
2433
|
+
const elapsed = now - tracker.periodStart;
|
|
2434
|
+
let shouldReset = false;
|
|
2435
|
+
switch (tracker.budget.period) {
|
|
2436
|
+
case "per-request":
|
|
2437
|
+
shouldReset = tracker.requestCount > 0;
|
|
2438
|
+
break;
|
|
2439
|
+
case "per-session":
|
|
2440
|
+
break;
|
|
2441
|
+
case "daily":
|
|
2442
|
+
shouldReset = elapsed >= 864e5;
|
|
2443
|
+
break;
|
|
2444
|
+
case "monthly":
|
|
2445
|
+
shouldReset = elapsed >= 30 * 864e5;
|
|
2446
|
+
break;
|
|
2447
|
+
}
|
|
2448
|
+
if (shouldReset) {
|
|
2449
|
+
tracker.spent = 0;
|
|
2450
|
+
tracker.requestCount = 0;
|
|
2451
|
+
tracker.periodStart = now;
|
|
2452
|
+
tracker.sessionId = `session-${++this.sessionCounter}`;
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
buildState(tracker) {
|
|
2456
|
+
const remaining = Math.max(0, tracker.budget.maxSpend - tracker.spent);
|
|
2457
|
+
const warnThreshold = tracker.budget.warnThreshold ?? 0.8;
|
|
2458
|
+
const cbThreshold = tracker.budget.circuitBreakerThreshold ?? 5;
|
|
2459
|
+
return {
|
|
2460
|
+
agentId: tracker.budget.agentId,
|
|
2461
|
+
spent: tracker.spent,
|
|
2462
|
+
limit: tracker.budget.maxSpend,
|
|
2463
|
+
remaining,
|
|
2464
|
+
exhausted: remaining === 0,
|
|
2465
|
+
warning: tracker.spent / tracker.budget.maxSpend >= warnThreshold,
|
|
2466
|
+
mode: tracker.budget.mode,
|
|
2467
|
+
period: tracker.budget.period,
|
|
2468
|
+
periodStart: new Date(tracker.periodStart).toISOString(),
|
|
2469
|
+
requestCount: tracker.requestCount,
|
|
2470
|
+
circuitBreaker: {
|
|
2471
|
+
isOpen: tracker.circuitBreakerTrippedAt !== null && Date.now() - tracker.circuitBreakerTrippedAt < this.config.circuitBreakerResetMs,
|
|
2472
|
+
consecutiveFailures: tracker.consecutiveFailures,
|
|
2473
|
+
threshold: cbThreshold,
|
|
2474
|
+
trippedAt: tracker.circuitBreakerTrippedAt ? new Date(tracker.circuitBreakerTrippedAt).toISOString() : null,
|
|
2475
|
+
resetAt: tracker.circuitBreakerTrippedAt ? new Date(
|
|
2476
|
+
tracker.circuitBreakerTrippedAt + this.config.circuitBreakerResetMs
|
|
2477
|
+
).toISOString() : null
|
|
2478
|
+
}
|
|
2479
|
+
};
|
|
2480
|
+
}
|
|
2481
|
+
emitTelemetry(type, data) {
|
|
2482
|
+
this.config.telemetry?.record({
|
|
2483
|
+
type,
|
|
2484
|
+
severity: type.includes("denied") || type.includes("tripped") ? "warning" : "info",
|
|
2485
|
+
agentId: data?.agentId || "budget-enforcer",
|
|
2486
|
+
data
|
|
2487
|
+
});
|
|
2488
|
+
}
|
|
2489
|
+
};
|
|
2490
|
+
|
|
2491
|
+
// src/economy/_core-stubs.ts
|
|
2492
|
+
var PLATFORM_BUDGETS = {
|
|
2493
|
+
quest3: {
|
|
2494
|
+
particles: 5e3,
|
|
2495
|
+
physicsBodies: 200,
|
|
2496
|
+
audioSources: 16,
|
|
2497
|
+
meshInstances: 500,
|
|
2498
|
+
gaussians: 18e4,
|
|
2499
|
+
shaderPasses: 4,
|
|
2500
|
+
networkMsgs: 60,
|
|
2501
|
+
agentCount: 10,
|
|
2502
|
+
memoryMB: 512,
|
|
2503
|
+
gpuDrawCalls: 200
|
|
2504
|
+
},
|
|
2505
|
+
"desktop-vr": {
|
|
2506
|
+
particles: 5e4,
|
|
2507
|
+
physicsBodies: 2e3,
|
|
2508
|
+
audioSources: 64,
|
|
2509
|
+
meshInstances: 5e3,
|
|
2510
|
+
gaussians: 2e6,
|
|
2511
|
+
shaderPasses: 16,
|
|
2512
|
+
networkMsgs: 200,
|
|
2513
|
+
agentCount: 50,
|
|
2514
|
+
memoryMB: 4096,
|
|
2515
|
+
gpuDrawCalls: 2e3
|
|
2516
|
+
},
|
|
2517
|
+
webgpu: {
|
|
2518
|
+
particles: 2e4,
|
|
2519
|
+
physicsBodies: 500,
|
|
2520
|
+
audioSources: 32,
|
|
2521
|
+
meshInstances: 2e3,
|
|
2522
|
+
gaussians: 5e5,
|
|
2523
|
+
shaderPasses: 8,
|
|
2524
|
+
networkMsgs: 100,
|
|
2525
|
+
agentCount: 20,
|
|
2526
|
+
memoryMB: 1024,
|
|
2527
|
+
gpuDrawCalls: 500
|
|
2528
|
+
},
|
|
2529
|
+
"mobile-ar": {
|
|
2530
|
+
particles: 2e3,
|
|
2531
|
+
physicsBodies: 50,
|
|
2532
|
+
audioSources: 8,
|
|
2533
|
+
meshInstances: 200,
|
|
2534
|
+
gaussians: 1e5,
|
|
2535
|
+
shaderPasses: 2,
|
|
2536
|
+
networkMsgs: 30,
|
|
2537
|
+
agentCount: 5,
|
|
2538
|
+
memoryMB: 256,
|
|
2539
|
+
gpuDrawCalls: 100
|
|
2540
|
+
}
|
|
2541
|
+
};
|
|
2542
|
+
var TRAIT_RESOURCE_COSTS = {
|
|
2543
|
+
"@mesh": { meshInstances: 1, gpuDrawCalls: 1 },
|
|
2544
|
+
"@material": { shaderPasses: 1, gpuDrawCalls: 1, memoryMB: 0.5 },
|
|
2545
|
+
"@shader": { shaderPasses: 1, gpuDrawCalls: 1 },
|
|
2546
|
+
"@advanced_pbr": { shaderPasses: 2, gpuDrawCalls: 1, memoryMB: 1 },
|
|
2547
|
+
"@advanced_lighting": { gpuDrawCalls: 2, shaderPasses: 1 },
|
|
2548
|
+
"@advanced_texturing": { memoryMB: 2, gpuDrawCalls: 1 },
|
|
2549
|
+
"@light": { gpuDrawCalls: 1, shaderPasses: 1 },
|
|
2550
|
+
"@lighting": { gpuDrawCalls: 2, shaderPasses: 1 },
|
|
2551
|
+
"@global_illumination": { gpuDrawCalls: 4, shaderPasses: 3, memoryMB: 8 },
|
|
2552
|
+
"@ray_tracing": { gpuDrawCalls: 8, shaderPasses: 4, memoryMB: 16 },
|
|
2553
|
+
"@screen_space_effects": { shaderPasses: 2, gpuDrawCalls: 2 },
|
|
2554
|
+
"@subsurface_scattering": { shaderPasses: 2, gpuDrawCalls: 1 },
|
|
2555
|
+
"@rendering": { meshInstances: 1, gpuDrawCalls: 1, memoryMB: 1 },
|
|
2556
|
+
"@render_network": { gpuDrawCalls: 2, networkMsgs: 5, memoryMB: 4 },
|
|
2557
|
+
"@particle": { particles: 100, gpuDrawCalls: 1, memoryMB: 2 },
|
|
2558
|
+
"@vfx": { particles: 200, shaderPasses: 1, gpuDrawCalls: 2 },
|
|
2559
|
+
"@volumetric": { gpuDrawCalls: 3, shaderPasses: 2, memoryMB: 4 },
|
|
2560
|
+
"@volumetric_window": { gpuDrawCalls: 2, memoryMB: 2 },
|
|
2561
|
+
"@gaussian": { gaussians: 1e5, memoryMB: 10 },
|
|
2562
|
+
"@gaussian_splat": { gaussians: 1e5, memoryMB: 10 },
|
|
2563
|
+
"@multiview_gaussian_renderer": { gaussians: 2e5, memoryMB: 20, gpuDrawCalls: 4 },
|
|
2564
|
+
"@nerf": { gpuDrawCalls: 4, memoryMB: 16, shaderPasses: 2 },
|
|
2565
|
+
"@physics": { physicsBodies: 1 },
|
|
2566
|
+
"@rigidbody": { physicsBodies: 1 },
|
|
2567
|
+
"@collider": { physicsBodies: 1 },
|
|
2568
|
+
"@joint": { physicsBodies: 2 },
|
|
2569
|
+
"@trigger": { physicsBodies: 1 },
|
|
2570
|
+
"@fluid_simulation": { particles: 500, physicsBodies: 10, memoryMB: 8 },
|
|
2571
|
+
"@advanced_cloth": { particles: 200, physicsBodies: 5, memoryMB: 4 },
|
|
2572
|
+
"@granular_material": { particles: 300, physicsBodies: 8, memoryMB: 6 },
|
|
2573
|
+
"@voronoi_fracture": { physicsBodies: 20, meshInstances: 20, memoryMB: 4 },
|
|
2574
|
+
"@audio": { audioSources: 1 },
|
|
2575
|
+
"@spatial_audio": { audioSources: 1 },
|
|
2576
|
+
"@environmental_audio": { audioSources: 4, memoryMB: 2 },
|
|
2577
|
+
"@voice_mesh": { audioSources: 1, networkMsgs: 10 },
|
|
2578
|
+
"@voice_input": { audioSources: 1, memoryMB: 1 },
|
|
2579
|
+
"@voice_output": { audioSources: 1, memoryMB: 1 },
|
|
2580
|
+
"@lip_sync": { memoryMB: 2, gpuDrawCalls: 1 },
|
|
2581
|
+
"@ambisonics": { audioSources: 4, memoryMB: 4 },
|
|
2582
|
+
"@networked": { networkMsgs: 1 },
|
|
2583
|
+
"@networked_avatar": { networkMsgs: 10, meshInstances: 1, memoryMB: 2 },
|
|
2584
|
+
"@lobby": { networkMsgs: 5, memoryMB: 2 },
|
|
2585
|
+
"@mqtt_sink": { networkMsgs: 5 },
|
|
2586
|
+
"@mqtt_source": { networkMsgs: 5 },
|
|
2587
|
+
"@sync_tier": { networkMsgs: 2 },
|
|
2588
|
+
"@crdt_room": { networkMsgs: 10, memoryMB: 4 },
|
|
2589
|
+
"@shareplay": { networkMsgs: 5, memoryMB: 2 },
|
|
2590
|
+
"@agent": { agentCount: 1, memoryMB: 5 },
|
|
2591
|
+
"@npc": { agentCount: 1, memoryMB: 3, physicsBodies: 1 },
|
|
2592
|
+
"@npc_ai": { agentCount: 1, memoryMB: 8 },
|
|
2593
|
+
"@ai_npc_brain": { agentCount: 1, memoryMB: 10 },
|
|
2594
|
+
"@multi_agent": { agentCount: 3, memoryMB: 15 },
|
|
2595
|
+
"@agent_discovery": { agentCount: 1, memoryMB: 2 },
|
|
2596
|
+
"@neural_animation": { memoryMB: 8, gpuDrawCalls: 2 },
|
|
2597
|
+
"@neural_forge": { memoryMB: 16, agentCount: 1 },
|
|
2598
|
+
"@local_llm": { memoryMB: 32, agentCount: 1 },
|
|
2599
|
+
"@rag_knowledge": { memoryMB: 8 },
|
|
2600
|
+
"@embedding_search": { memoryMB: 4 },
|
|
2601
|
+
"@vector_db": { memoryMB: 8 },
|
|
2602
|
+
"@stable_diffusion": { memoryMB: 32, gpuDrawCalls: 1 },
|
|
2603
|
+
"@diffusion_realtime": { memoryMB: 16, gpuDrawCalls: 2 },
|
|
2604
|
+
"@vision": { memoryMB: 4 },
|
|
2605
|
+
"@pose_estimation": { memoryMB: 4, agentCount: 1 },
|
|
2606
|
+
"@object_tracking": { memoryMB: 4 },
|
|
2607
|
+
"@hand_mesh_ai": { memoryMB: 4, gpuDrawCalls: 1 },
|
|
2608
|
+
"@animation": { gpuDrawCalls: 1, memoryMB: 2 },
|
|
2609
|
+
"@skeleton": { gpuDrawCalls: 1, memoryMB: 1 },
|
|
2610
|
+
"@ik": { memoryMB: 1 },
|
|
2611
|
+
"@morph": { gpuDrawCalls: 1, memoryMB: 1 },
|
|
2612
|
+
"@character": { physicsBodies: 1, meshInstances: 1, gpuDrawCalls: 2, memoryMB: 4 },
|
|
2613
|
+
"@emotion_directive": { memoryMB: 1 },
|
|
2614
|
+
"@dialog": { memoryMB: 2 },
|
|
2615
|
+
"@scene_reconstruction": { meshInstances: 10, memoryMB: 8, gpuDrawCalls: 5 },
|
|
2616
|
+
"@realitykit_mesh": { meshInstances: 5, memoryMB: 4, gpuDrawCalls: 3 },
|
|
2617
|
+
"@openxr_hal": { memoryMB: 2 },
|
|
2618
|
+
"@spatial_navigation": { memoryMB: 2 },
|
|
2619
|
+
"@spatial_persona": { meshInstances: 1, memoryMB: 2, networkMsgs: 5 },
|
|
2620
|
+
"@spatial_awareness": { memoryMB: 2 },
|
|
2621
|
+
"@orbital": { physicsBodies: 1, memoryMB: 1 },
|
|
2622
|
+
"@grabbable": { physicsBodies: 1 },
|
|
2623
|
+
"@pressable": { physicsBodies: 1 },
|
|
2624
|
+
"@slidable": { physicsBodies: 1 },
|
|
2625
|
+
"@wot_thing": { networkMsgs: 2, memoryMB: 1 },
|
|
2626
|
+
"@urdf_robot": { physicsBodies: 10, meshInstances: 10, memoryMB: 4 },
|
|
2627
|
+
"@computer_use": { memoryMB: 4 },
|
|
2628
|
+
"@pid_controller": { memoryMB: 0.5 },
|
|
2629
|
+
"@biofeedback": { memoryMB: 1 }
|
|
2630
|
+
};
|
|
2631
|
+
|
|
2632
|
+
// src/economy/UnifiedBudgetOptimizer.ts
|
|
2633
|
+
var DEFAULT_TRAIT_UTILITIES = {
|
|
2634
|
+
// ── Core (required, high utility) ──
|
|
2635
|
+
"@mesh": { baseUtility: 100, category: "visual", required: true, minLODLevel: 0 },
|
|
2636
|
+
"@material": { baseUtility: 95, category: "visual", required: true, minLODLevel: 0 },
|
|
2637
|
+
"@physics": { baseUtility: 90, category: "physics", required: true, minLODLevel: 0 },
|
|
2638
|
+
"@rigidbody": { baseUtility: 90, category: "physics", required: true, minLODLevel: 0 },
|
|
2639
|
+
"@collider": { baseUtility: 88, category: "physics", required: true, minLODLevel: 0 },
|
|
2640
|
+
"@rendering": { baseUtility: 95, category: "visual", required: true, minLODLevel: 0 },
|
|
2641
|
+
"@character": { baseUtility: 92, category: "visual", required: true, minLODLevel: 0 },
|
|
2642
|
+
"@networked": { baseUtility: 85, category: "network", required: true, minLODLevel: 0 },
|
|
2643
|
+
// C6 Layer 2 re-score: @agent was hand-assigned at 85 (required). Data-derived
|
|
2644
|
+
// analysis shows agents are a luxury in most rendering scenes — only ~35% of
|
|
2645
|
+
// compositions use @agent, and it consumes 5MB memory. Downgraded to non-required
|
|
2646
|
+
// enhancement. Scenes that genuinely need agents can override via custom utilities.
|
|
2647
|
+
"@agent": { baseUtility: 35, category: "ai", required: false, minLODLevel: 2 },
|
|
2648
|
+
// ── Important quality (droppable at high LOD) ──
|
|
2649
|
+
"@light": { baseUtility: 80, category: "visual", required: false, minLODLevel: 3 },
|
|
2650
|
+
"@lighting": { baseUtility: 78, category: "visual", required: false, minLODLevel: 3 },
|
|
2651
|
+
"@shader": { baseUtility: 75, category: "visual", required: false, minLODLevel: 2 },
|
|
2652
|
+
"@advanced_pbr": { baseUtility: 72, category: "visual", required: false, minLODLevel: 2 },
|
|
2653
|
+
"@particle": { baseUtility: 70, category: "visual", required: false, minLODLevel: 2 },
|
|
2654
|
+
"@audio": { baseUtility: 75, category: "audio", required: false, minLODLevel: 3 },
|
|
2655
|
+
"@spatial_audio": { baseUtility: 72, category: "audio", required: false, minLODLevel: 2 },
|
|
2656
|
+
"@animation": { baseUtility: 78, category: "visual", required: false, minLODLevel: 3 },
|
|
2657
|
+
"@skeleton": { baseUtility: 76, category: "visual", required: false, minLODLevel: 3 },
|
|
2658
|
+
// ── Enhancement (drop at medium LOD) ──
|
|
2659
|
+
"@vfx": { baseUtility: 60, category: "visual", required: false, minLODLevel: 2 },
|
|
2660
|
+
"@volumetric": { baseUtility: 55, category: "visual", required: false, minLODLevel: 2 },
|
|
2661
|
+
"@advanced_lighting": { baseUtility: 58, category: "visual", required: false, minLODLevel: 2 },
|
|
2662
|
+
"@advanced_texturing": { baseUtility: 56, category: "visual", required: false, minLODLevel: 2 },
|
|
2663
|
+
"@screen_space_effects": { baseUtility: 52, category: "visual", required: false, minLODLevel: 2 },
|
|
2664
|
+
"@environmental_audio": { baseUtility: 55, category: "audio", required: false, minLODLevel: 2 },
|
|
2665
|
+
"@npc": { baseUtility: 65, category: "ai", required: false, minLODLevel: 3 },
|
|
2666
|
+
"@npc_ai": { baseUtility: 62, category: "ai", required: false, minLODLevel: 3 },
|
|
2667
|
+
"@fluid_simulation": { baseUtility: 50, category: "physics", required: false, minLODLevel: 1 },
|
|
2668
|
+
"@advanced_cloth": { baseUtility: 48, category: "physics", required: false, minLODLevel: 1 },
|
|
2669
|
+
"@joint": { baseUtility: 65, category: "physics", required: false, minLODLevel: 3 },
|
|
2670
|
+
// ── Nice-to-have (drop early) ──
|
|
2671
|
+
"@subsurface_scattering": {
|
|
2672
|
+
baseUtility: 38,
|
|
2673
|
+
category: "visual",
|
|
2674
|
+
required: false,
|
|
2675
|
+
minLODLevel: 1
|
|
2676
|
+
},
|
|
2677
|
+
"@ambisonics": { baseUtility: 35, category: "audio", required: false, minLODLevel: 1 },
|
|
2678
|
+
"@voronoi_fracture": { baseUtility: 40, category: "physics", required: false, minLODLevel: 1 },
|
|
2679
|
+
"@granular_material": { baseUtility: 38, category: "physics", required: false, minLODLevel: 1 },
|
|
2680
|
+
"@lip_sync": { baseUtility: 42, category: "visual", required: false, minLODLevel: 1 },
|
|
2681
|
+
// ── Luxury (drop first) ──
|
|
2682
|
+
"@ray_tracing": { baseUtility: 20, category: "visual", required: false, minLODLevel: 1 },
|
|
2683
|
+
"@global_illumination": { baseUtility: 22, category: "visual", required: false, minLODLevel: 1 },
|
|
2684
|
+
"@nerf": { baseUtility: 18, category: "visual", required: false, minLODLevel: 1 },
|
|
2685
|
+
"@multiview_gaussian_renderer": {
|
|
2686
|
+
baseUtility: 25,
|
|
2687
|
+
category: "visual",
|
|
2688
|
+
required: false,
|
|
2689
|
+
minLODLevel: 1
|
|
2690
|
+
},
|
|
2691
|
+
// C6 Layer 2 re-score: @gaussian/@gaussian_splat were hand-assigned at 65.
|
|
2692
|
+
// Data-derived analysis: 100K gaussians on Quest 3 = 55% of total GPU budget,
|
|
2693
|
+
// making the value/cost ratio much lower than hand-scores suggested.
|
|
2694
|
+
// Downgraded by 45 points to reflect actual GPU cost relative to utility.
|
|
2695
|
+
"@gaussian": { baseUtility: 20, category: "visual", required: false, minLODLevel: 1 },
|
|
2696
|
+
"@gaussian_splat": { baseUtility: 20, category: "visual", required: false, minLODLevel: 1 },
|
|
2697
|
+
"@stable_diffusion": { baseUtility: 15, category: "ai", required: false, minLODLevel: 1 },
|
|
2698
|
+
"@diffusion_realtime": { baseUtility: 18, category: "ai", required: false, minLODLevel: 1 },
|
|
2699
|
+
"@local_llm": { baseUtility: 30, category: "ai", required: false, minLODLevel: 1 }
|
|
2700
|
+
};
|
|
2701
|
+
var PLATFORM_LOD_SCALING = {
|
|
2702
|
+
quest3: [1, 0.6, 0.3, 0.12, 0.04],
|
|
2703
|
+
// Aggressive — tight budget
|
|
2704
|
+
"mobile-ar": [1, 0.5, 0.2, 0.08, 0.02],
|
|
2705
|
+
// Most aggressive — tightest budget
|
|
2706
|
+
webgpu: [1, 0.7, 0.4, 0.18, 0.06],
|
|
2707
|
+
// Moderate — matches default
|
|
2708
|
+
"desktop-vr": [1, 0.85, 0.6, 0.3, 0.1],
|
|
2709
|
+
// Gentle — plenty of headroom
|
|
2710
|
+
visionos: [1, 0.8, 0.5, 0.25, 0.08]
|
|
2711
|
+
// Moderate-gentle — good hardware
|
|
2712
|
+
};
|
|
2713
|
+
var DEFAULT_COST_FLOOR = {
|
|
2714
|
+
perGaussian: 0.01,
|
|
2715
|
+
// 10K gaussians = $0.10 minimum
|
|
2716
|
+
perDrawCall: 1e3,
|
|
2717
|
+
// $0.001 per draw call
|
|
2718
|
+
perMemoryMB: 5e3,
|
|
2719
|
+
// $0.005 per MB
|
|
2720
|
+
perParticle: 0.1,
|
|
2721
|
+
// 1000 particles = $0.10 minimum
|
|
2722
|
+
perPhysicsBody: 500,
|
|
2723
|
+
// $0.0005 per body
|
|
2724
|
+
baseFee: 1e5
|
|
2725
|
+
// $0.10 base fee on all marketplace traits
|
|
2726
|
+
};
|
|
2727
|
+
var DEFAULT_LOD_SCALING = [1, 0.7, 0.4, 0.18, 0.06];
|
|
2728
|
+
var UnifiedBudgetOptimizer = class {
|
|
2729
|
+
platform;
|
|
2730
|
+
costFloor;
|
|
2731
|
+
traitUtilities;
|
|
2732
|
+
lodScaling;
|
|
2733
|
+
economicBudget;
|
|
2734
|
+
economicSpent;
|
|
2735
|
+
constructor(config) {
|
|
2736
|
+
this.platform = config.platform;
|
|
2737
|
+
this.costFloor = config.costFloor;
|
|
2738
|
+
this.lodScaling = config.lodScaling ?? PLATFORM_LOD_SCALING[config.platform] ?? DEFAULT_LOD_SCALING;
|
|
2739
|
+
this.economicBudget = config.economicBudget ?? 0;
|
|
2740
|
+
this.economicSpent = config.economicSpent ?? 0;
|
|
2741
|
+
this.traitUtilities = /* @__PURE__ */ new Map();
|
|
2742
|
+
for (const [trait, util] of Object.entries(DEFAULT_TRAIT_UTILITIES)) {
|
|
2743
|
+
this.traitUtilities.set(trait, { trait, ...util });
|
|
2744
|
+
}
|
|
2745
|
+
if (config.traitUtilities) {
|
|
2746
|
+
for (const [trait, util] of config.traitUtilities) {
|
|
2747
|
+
this.traitUtilities.set(trait, util);
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
// ===========================================================================
|
|
2752
|
+
// CORE: Equimarginal Allocation
|
|
2753
|
+
// ===========================================================================
|
|
2754
|
+
/**
|
|
2755
|
+
* Allocate traits across a resource budget using the equimarginal principle.
|
|
2756
|
+
* Instead of greedily dropping the deepest LOD first, this sorts by value/cost
|
|
2757
|
+
* ratio and drops traits with the lowest marginal utility first.
|
|
2758
|
+
*
|
|
2759
|
+
* @param nodes - Resource usage nodes (traits + counts)
|
|
2760
|
+
* @param maxLOD - Maximum LOD level to consider (default: 4)
|
|
2761
|
+
* @returns Allocation decisions for each trait
|
|
2762
|
+
*/
|
|
2763
|
+
allocate(nodes, maxLOD = 4) {
|
|
2764
|
+
const limits = PLATFORM_BUDGETS[this.platform];
|
|
2765
|
+
if (!limits) {
|
|
2766
|
+
return this.flattenToAllocations(nodes, 0);
|
|
2767
|
+
}
|
|
2768
|
+
const allocations = this.flattenToAllocations(nodes, 0);
|
|
2769
|
+
let pressure = this.computeResourcePressure(allocations, limits);
|
|
2770
|
+
if (pressure.maxPressure <= 1) {
|
|
2771
|
+
return allocations;
|
|
2772
|
+
}
|
|
2773
|
+
for (let lod = 1; lod <= maxLOD && pressure.maxPressure > 1; lod++) {
|
|
2774
|
+
const candidates = allocations.filter((a) => a.included && !this.isRequired(a.trait) && this.canDropAtLOD(a.trait, lod)).sort((a, b) => a.valueCostRatio - b.valueCostRatio);
|
|
2775
|
+
for (const candidate of candidates) {
|
|
2776
|
+
if (pressure.maxPressure <= 1) break;
|
|
2777
|
+
const idx = allocations.findIndex((a) => a.trait === candidate.trait);
|
|
2778
|
+
if (idx >= 0) {
|
|
2779
|
+
allocations[idx] = this.buildAllocation(candidate.trait, lod, 1);
|
|
2780
|
+
}
|
|
2781
|
+
pressure = this.computeResourcePressure(allocations, limits);
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
if (pressure.maxPressure > 1) {
|
|
2785
|
+
const excludeable = allocations.filter((a) => a.included && !this.isRequired(a.trait)).sort((a, b) => a.valueCostRatio - b.valueCostRatio);
|
|
2786
|
+
for (const candidate of excludeable) {
|
|
2787
|
+
if (pressure.maxPressure <= 1) break;
|
|
2788
|
+
const idx = allocations.findIndex((a) => a.trait === candidate.trait);
|
|
2789
|
+
if (idx >= 0) {
|
|
2790
|
+
allocations[idx] = {
|
|
2791
|
+
...allocations[idx],
|
|
2792
|
+
included: false,
|
|
2793
|
+
excludeReason: `Excluded to fit ${this.platform} budget (value/cost ratio: ${candidate.valueCostRatio.toFixed(2)})`
|
|
2794
|
+
};
|
|
2795
|
+
}
|
|
2796
|
+
pressure = this.computeResourcePressure(allocations, limits);
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
return allocations;
|
|
2800
|
+
}
|
|
2801
|
+
// ===========================================================================
|
|
2802
|
+
// RESOURCE COST FLOOR PRICING
|
|
2803
|
+
// ===========================================================================
|
|
2804
|
+
/**
|
|
2805
|
+
* Calculate the minimum marketplace price for a trait based on its resource cost.
|
|
2806
|
+
* Prevents economic denial-of-rendering attacks where a cheap marketplace trait
|
|
2807
|
+
* consumes massive GPU resources.
|
|
2808
|
+
*
|
|
2809
|
+
* @param traitName - The trait to price
|
|
2810
|
+
* @param instanceCount - How many instances (default: 1)
|
|
2811
|
+
* @returns Minimum price in USDC base units (6 decimals)
|
|
2812
|
+
*/
|
|
2813
|
+
calculateCostFloor(traitName, instanceCount = 1) {
|
|
2814
|
+
const normalized = traitName.startsWith("@") ? traitName : `@${traitName}`;
|
|
2815
|
+
const costs = TRAIT_RESOURCE_COSTS[normalized];
|
|
2816
|
+
if (!costs) return this.costFloor.baseFee;
|
|
2817
|
+
let floor = this.costFloor.baseFee;
|
|
2818
|
+
if (costs.gaussians) floor += costs.gaussians * instanceCount * this.costFloor.perGaussian;
|
|
2819
|
+
if (costs.gpuDrawCalls)
|
|
2820
|
+
floor += costs.gpuDrawCalls * instanceCount * this.costFloor.perDrawCall;
|
|
2821
|
+
if (costs.memoryMB) floor += costs.memoryMB * instanceCount * this.costFloor.perMemoryMB;
|
|
2822
|
+
if (costs.particles) floor += costs.particles * instanceCount * this.costFloor.perParticle;
|
|
2823
|
+
if (costs.physicsBodies)
|
|
2824
|
+
floor += costs.physicsBodies * instanceCount * this.costFloor.perPhysicsBody;
|
|
2825
|
+
return Math.ceil(floor);
|
|
2826
|
+
}
|
|
2827
|
+
/**
|
|
2828
|
+
* Validate that a marketplace listing price meets the resource cost floor.
|
|
2829
|
+
*
|
|
2830
|
+
* @param traitName - The trait being listed
|
|
2831
|
+
* @param listPrice - Proposed listing price (USDC base units)
|
|
2832
|
+
* @param instanceCount - Expected instance count
|
|
2833
|
+
* @returns Validation result
|
|
2834
|
+
*/
|
|
2835
|
+
validateMarketplacePrice(traitName, listPrice, instanceCount = 1) {
|
|
2836
|
+
const floor = this.calculateCostFloor(traitName, instanceCount);
|
|
2837
|
+
const deficit = Math.max(0, floor - listPrice);
|
|
2838
|
+
if (listPrice >= floor) {
|
|
2839
|
+
return {
|
|
2840
|
+
valid: true,
|
|
2841
|
+
floor,
|
|
2842
|
+
deficit: 0,
|
|
2843
|
+
message: `Price ${listPrice} meets resource cost floor of ${floor}`
|
|
2844
|
+
};
|
|
2845
|
+
}
|
|
2846
|
+
return {
|
|
2847
|
+
valid: false,
|
|
2848
|
+
floor,
|
|
2849
|
+
deficit,
|
|
2850
|
+
message: `Price ${listPrice} is below resource cost floor of ${floor}. The trait's GPU/memory cost exceeds its economic price by ${deficit} USDC base units.`
|
|
2851
|
+
};
|
|
2852
|
+
}
|
|
2853
|
+
// ===========================================================================
|
|
2854
|
+
// UNIFIED BUDGET STATE
|
|
2855
|
+
// ===========================================================================
|
|
2856
|
+
/**
|
|
2857
|
+
* Get a unified view of budget pressure across economy + rendering.
|
|
2858
|
+
*
|
|
2859
|
+
* @param agentId - Agent identifier
|
|
2860
|
+
* @param nodes - Current resource usage nodes
|
|
2861
|
+
* @param economicSpent - Current economic spend (USDC base units)
|
|
2862
|
+
* @param economicLimit - Economic budget limit (USDC base units)
|
|
2863
|
+
* @returns Unified budget state
|
|
2864
|
+
*/
|
|
2865
|
+
getUnifiedState(agentId, nodes, economicSpent, economicLimit) {
|
|
2866
|
+
const spent = economicSpent ?? this.economicSpent;
|
|
2867
|
+
const limit = economicLimit ?? this.economicBudget;
|
|
2868
|
+
const economicPressure = limit > 0 ? spent / limit : 0;
|
|
2869
|
+
const allocations = this.flattenToAllocations(nodes, 0);
|
|
2870
|
+
const limits = PLATFORM_BUDGETS[this.platform] ?? {};
|
|
2871
|
+
const pressure = this.computeResourcePressure(allocations, limits);
|
|
2872
|
+
const overallPressure = Math.max(economicPressure, pressure.maxPressure);
|
|
2873
|
+
let suggestedLOD = 0;
|
|
2874
|
+
if (overallPressure > 0.95) suggestedLOD = 3;
|
|
2875
|
+
else if (overallPressure > 0.8) suggestedLOD = 2;
|
|
2876
|
+
else if (overallPressure > 0.6) suggestedLOD = 1;
|
|
2877
|
+
const shedCandidates = allocations.filter((a) => a.included && !this.isRequired(a.trait)).sort((a, b) => a.valueCostRatio - b.valueCostRatio).slice(0, 10);
|
|
2878
|
+
return {
|
|
2879
|
+
agentId,
|
|
2880
|
+
economicPressure: Math.min(1, economicPressure),
|
|
2881
|
+
resourcePressure: pressure.perCategory,
|
|
2882
|
+
overallPressure: Math.min(1, overallPressure),
|
|
2883
|
+
suggestedLOD,
|
|
2884
|
+
hardLimitBreached: pressure.maxPressure > 1 || economicPressure > 1,
|
|
2885
|
+
shedCandidates
|
|
2886
|
+
};
|
|
2887
|
+
}
|
|
2888
|
+
// ===========================================================================
|
|
2889
|
+
// UTILITY QUERIES
|
|
2890
|
+
// ===========================================================================
|
|
2891
|
+
/**
|
|
2892
|
+
* Get the utility score for a trait.
|
|
2893
|
+
*/
|
|
2894
|
+
getUtility(traitName) {
|
|
2895
|
+
const normalized = traitName.startsWith("@") ? traitName : `@${traitName}`;
|
|
2896
|
+
return this.traitUtilities.get(normalized);
|
|
2897
|
+
}
|
|
2898
|
+
/**
|
|
2899
|
+
* Set custom utility for a trait.
|
|
2900
|
+
*/
|
|
2901
|
+
setUtility(utility) {
|
|
2902
|
+
this.traitUtilities.set(utility.trait, utility);
|
|
2903
|
+
}
|
|
2904
|
+
/**
|
|
2905
|
+
* Get the total weighted resource cost of a trait at a given LOD level.
|
|
2906
|
+
* Collapses multi-dimensional resource cost into a single scalar
|
|
2907
|
+
* using platform limits as normalization weights.
|
|
2908
|
+
*/
|
|
2909
|
+
getWeightedCost(traitName, lodLevel = 0, instanceCount = 1) {
|
|
2910
|
+
const normalized = traitName.startsWith("@") ? traitName : `@${traitName}`;
|
|
2911
|
+
const costs = TRAIT_RESOURCE_COSTS[normalized];
|
|
2912
|
+
if (!costs) return 0;
|
|
2913
|
+
const limits = PLATFORM_BUDGETS[this.platform] ?? {};
|
|
2914
|
+
const scale = this.lodScaling[Math.min(lodLevel, this.lodScaling.length - 1)] ?? 0.05;
|
|
2915
|
+
let weighted = 0;
|
|
2916
|
+
for (const [cat, cost] of Object.entries(costs)) {
|
|
2917
|
+
const limit = limits[cat];
|
|
2918
|
+
if (limit && limit > 0) {
|
|
2919
|
+
weighted += cost * instanceCount * scale / limit;
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
return weighted;
|
|
2923
|
+
}
|
|
2924
|
+
/**
|
|
2925
|
+
* Compute value/cost ratio for a trait at a given LOD level.
|
|
2926
|
+
* Higher = more efficient use of resources.
|
|
2927
|
+
*/
|
|
2928
|
+
getValueCostRatio(traitName, lodLevel = 0, instanceCount = 1) {
|
|
2929
|
+
const normalized = traitName.startsWith("@") ? traitName : `@${traitName}`;
|
|
2930
|
+
const util = this.traitUtilities.get(normalized);
|
|
2931
|
+
const utility = util?.baseUtility ?? 50;
|
|
2932
|
+
const cost = this.getWeightedCost(normalized, lodLevel, instanceCount);
|
|
2933
|
+
if (cost === 0) return utility * 100;
|
|
2934
|
+
return utility / cost;
|
|
2935
|
+
}
|
|
2936
|
+
// ===========================================================================
|
|
2937
|
+
// INTERNALS
|
|
2938
|
+
// ===========================================================================
|
|
2939
|
+
flattenToAllocations(nodes, lodLevel) {
|
|
2940
|
+
const allocations = [];
|
|
2941
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2942
|
+
for (const node of nodes) {
|
|
2943
|
+
for (const trait of node.traits) {
|
|
2944
|
+
const normalized = trait.startsWith("@") ? trait : `@${trait}`;
|
|
2945
|
+
if (seen.has(normalized)) continue;
|
|
2946
|
+
seen.add(normalized);
|
|
2947
|
+
allocations.push(this.buildAllocation(normalized, lodLevel, node.count || 1));
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
return allocations;
|
|
2951
|
+
}
|
|
2952
|
+
buildAllocation(trait, lodLevel, instanceCount) {
|
|
2953
|
+
const normalized = trait.startsWith("@") ? trait : `@${trait}`;
|
|
2954
|
+
const costs = TRAIT_RESOURCE_COSTS[normalized] ?? {};
|
|
2955
|
+
const scale = this.lodScaling[Math.min(lodLevel, this.lodScaling.length - 1)] ?? 0.05;
|
|
2956
|
+
const scaledCosts = {};
|
|
2957
|
+
for (const [cat, cost] of Object.entries(costs)) {
|
|
2958
|
+
scaledCosts[cat] = Math.ceil(cost * instanceCount * scale);
|
|
2959
|
+
}
|
|
2960
|
+
const utility = this.traitUtilities.get(normalized)?.baseUtility ?? 50;
|
|
2961
|
+
const weightedCost = this.getWeightedCost(normalized, lodLevel, instanceCount);
|
|
2962
|
+
const valueCostRatio = weightedCost > 0 ? utility / weightedCost : utility * 100;
|
|
2963
|
+
return {
|
|
2964
|
+
trait: normalized,
|
|
2965
|
+
included: true,
|
|
2966
|
+
lodLevel,
|
|
2967
|
+
resourceCost: scaledCosts,
|
|
2968
|
+
economicCost: 0,
|
|
2969
|
+
// Set externally if marketplace pricing applies
|
|
2970
|
+
utility,
|
|
2971
|
+
valueCostRatio
|
|
2972
|
+
};
|
|
2973
|
+
}
|
|
2974
|
+
isRequired(trait) {
|
|
2975
|
+
const normalized = trait.startsWith("@") ? trait : `@${trait}`;
|
|
2976
|
+
return this.traitUtilities.get(normalized)?.required ?? false;
|
|
2977
|
+
}
|
|
2978
|
+
canDropAtLOD(trait, lodLevel) {
|
|
2979
|
+
const normalized = trait.startsWith("@") ? trait : `@${trait}`;
|
|
2980
|
+
const util = this.traitUtilities.get(normalized);
|
|
2981
|
+
if (!util) return lodLevel >= 2;
|
|
2982
|
+
return lodLevel >= util.minLODLevel;
|
|
2983
|
+
}
|
|
2984
|
+
computeResourcePressure(allocations, limits) {
|
|
2985
|
+
const totals = {};
|
|
2986
|
+
for (const alloc of allocations) {
|
|
2987
|
+
if (!alloc.included) continue;
|
|
2988
|
+
for (const [cat, cost] of Object.entries(alloc.resourceCost)) {
|
|
2989
|
+
totals[cat] = (totals[cat] || 0) + cost;
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
const perCategory = {};
|
|
2993
|
+
let maxPressure = 0;
|
|
2994
|
+
for (const [cat, limit] of Object.entries(limits)) {
|
|
2995
|
+
const used = totals[cat] || 0;
|
|
2996
|
+
const pressure = limit ? used / limit : 0;
|
|
2997
|
+
perCategory[cat] = pressure;
|
|
2998
|
+
if (pressure > maxPressure) maxPressure = pressure;
|
|
2999
|
+
}
|
|
3000
|
+
return { maxPressure, perCategory };
|
|
3001
|
+
}
|
|
3002
|
+
};
|
|
3003
|
+
|
|
3004
|
+
// src/economy/UsageMeter.ts
|
|
3005
|
+
var UsageMeter = class {
|
|
3006
|
+
config;
|
|
3007
|
+
/** All usage events keyed by agentId */
|
|
3008
|
+
events = /* @__PURE__ */ new Map();
|
|
3009
|
+
/** Free-tier consumption per agent (monthly, USDC base units) */
|
|
3010
|
+
freeTierUsed = /* @__PURE__ */ new Map();
|
|
3011
|
+
/** Current month key for free-tier reset */
|
|
3012
|
+
currentMonthKey;
|
|
3013
|
+
eventCounter = 0;
|
|
3014
|
+
constructor(config) {
|
|
3015
|
+
this.config = {
|
|
3016
|
+
freeTier: config?.freeTier ?? { monthlyAllowance: 5e5 },
|
|
3017
|
+
// $0.50 free
|
|
3018
|
+
defaultToolCost: config?.defaultToolCost ?? 100,
|
|
3019
|
+
// $0.0001
|
|
3020
|
+
toolCosts: config?.toolCosts ?? {},
|
|
3021
|
+
maxEventsPerAgent: config?.maxEventsPerAgent ?? 1e4,
|
|
3022
|
+
telemetry: config?.telemetry
|
|
3023
|
+
};
|
|
3024
|
+
this.currentMonthKey = this.getMonthKey(Date.now());
|
|
3025
|
+
}
|
|
3026
|
+
// ===========================================================================
|
|
3027
|
+
// RECORDING
|
|
3028
|
+
// ===========================================================================
|
|
3029
|
+
/**
|
|
3030
|
+
* Record a tool call usage event.
|
|
3031
|
+
*/
|
|
3032
|
+
recordUsage(agentId, toolId, metadata) {
|
|
3033
|
+
this.checkMonthReset();
|
|
3034
|
+
const cost = this.getToolCost(toolId);
|
|
3035
|
+
const freeTierUsed = this.freeTierUsed.get(agentId) ?? 0;
|
|
3036
|
+
const freeTierRemaining = Math.max(0, this.config.freeTier.monthlyAllowance - freeTierUsed);
|
|
3037
|
+
const isFreeTier = cost <= freeTierRemaining;
|
|
3038
|
+
if (isFreeTier) {
|
|
3039
|
+
this.freeTierUsed.set(agentId, freeTierUsed + cost);
|
|
3040
|
+
}
|
|
3041
|
+
const event = {
|
|
3042
|
+
id: `usage-${++this.eventCounter}`,
|
|
3043
|
+
agentId,
|
|
3044
|
+
toolId,
|
|
3045
|
+
cost,
|
|
3046
|
+
timestamp: Date.now(),
|
|
3047
|
+
freeTier: isFreeTier,
|
|
3048
|
+
metadata
|
|
3049
|
+
};
|
|
3050
|
+
const agentEvents = this.events.get(agentId) ?? [];
|
|
3051
|
+
agentEvents.push(event);
|
|
3052
|
+
if (agentEvents.length > this.config.maxEventsPerAgent) {
|
|
3053
|
+
agentEvents.splice(0, agentEvents.length - this.config.maxEventsPerAgent);
|
|
3054
|
+
}
|
|
3055
|
+
this.events.set(agentId, agentEvents);
|
|
3056
|
+
this.emitTelemetry("usage_recorded", {
|
|
3057
|
+
agentId,
|
|
3058
|
+
toolId,
|
|
3059
|
+
cost,
|
|
3060
|
+
freeTier: isFreeTier
|
|
3061
|
+
});
|
|
3062
|
+
return event;
|
|
3063
|
+
}
|
|
3064
|
+
// ===========================================================================
|
|
3065
|
+
// COST LOOKUP
|
|
3066
|
+
// ===========================================================================
|
|
3067
|
+
/**
|
|
3068
|
+
* Get the cost for a specific tool.
|
|
3069
|
+
*/
|
|
3070
|
+
getToolCost(toolId) {
|
|
3071
|
+
if (this.config.toolCosts[toolId] !== void 0) {
|
|
3072
|
+
return this.config.toolCosts[toolId];
|
|
3073
|
+
}
|
|
3074
|
+
if (this.config.freeTier.toolOverrides?.[toolId] !== void 0) {
|
|
3075
|
+
return this.config.freeTier.toolOverrides[toolId];
|
|
3076
|
+
}
|
|
3077
|
+
return this.config.defaultToolCost;
|
|
3078
|
+
}
|
|
3079
|
+
/**
|
|
3080
|
+
* Set cost for a specific tool.
|
|
3081
|
+
*/
|
|
3082
|
+
setToolCost(toolId, cost) {
|
|
3083
|
+
this.config.toolCosts[toolId] = cost;
|
|
3084
|
+
}
|
|
3085
|
+
// ===========================================================================
|
|
3086
|
+
// AGGREGATION
|
|
3087
|
+
// ===========================================================================
|
|
3088
|
+
/**
|
|
3089
|
+
* Get usage summary for an agent within a time period.
|
|
3090
|
+
*/
|
|
3091
|
+
getAgentUsage(agentId, period = "monthly") {
|
|
3092
|
+
const events = this.events.get(agentId) ?? [];
|
|
3093
|
+
const { start, end } = this.getPeriodBounds(period);
|
|
3094
|
+
const filtered = events.filter((e) => e.timestamp >= start && e.timestamp < end);
|
|
3095
|
+
const byTool = /* @__PURE__ */ new Map();
|
|
3096
|
+
const totalAgg = {
|
|
3097
|
+
totalCalls: 0,
|
|
3098
|
+
totalCost: 0,
|
|
3099
|
+
freeTierCalls: 0,
|
|
3100
|
+
freeTierCost: 0,
|
|
3101
|
+
paidCalls: 0,
|
|
3102
|
+
paidCost: 0,
|
|
3103
|
+
periodStart: new Date(start).toISOString(),
|
|
3104
|
+
periodEnd: new Date(end).toISOString()
|
|
3105
|
+
};
|
|
3106
|
+
for (const event of filtered) {
|
|
3107
|
+
let toolAgg = byTool.get(event.toolId);
|
|
3108
|
+
if (!toolAgg) {
|
|
3109
|
+
toolAgg = {
|
|
3110
|
+
totalCalls: 0,
|
|
3111
|
+
totalCost: 0,
|
|
3112
|
+
freeTierCalls: 0,
|
|
3113
|
+
freeTierCost: 0,
|
|
3114
|
+
paidCalls: 0,
|
|
3115
|
+
paidCost: 0,
|
|
3116
|
+
periodStart: new Date(start).toISOString(),
|
|
3117
|
+
periodEnd: new Date(end).toISOString()
|
|
3118
|
+
};
|
|
3119
|
+
byTool.set(event.toolId, toolAgg);
|
|
3120
|
+
}
|
|
3121
|
+
toolAgg.totalCalls++;
|
|
3122
|
+
toolAgg.totalCost += event.cost;
|
|
3123
|
+
totalAgg.totalCalls++;
|
|
3124
|
+
totalAgg.totalCost += event.cost;
|
|
3125
|
+
if (event.freeTier) {
|
|
3126
|
+
toolAgg.freeTierCalls++;
|
|
3127
|
+
toolAgg.freeTierCost += event.cost;
|
|
3128
|
+
totalAgg.freeTierCalls++;
|
|
3129
|
+
totalAgg.freeTierCost += event.cost;
|
|
3130
|
+
} else {
|
|
3131
|
+
toolAgg.paidCalls++;
|
|
3132
|
+
toolAgg.paidCost += event.cost;
|
|
3133
|
+
totalAgg.paidCalls++;
|
|
3134
|
+
totalAgg.paidCost += event.cost;
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
const freeTierUsed = this.freeTierUsed.get(agentId) ?? 0;
|
|
3138
|
+
const freeTierRemaining = Math.max(0, this.config.freeTier.monthlyAllowance - freeTierUsed);
|
|
3139
|
+
return {
|
|
3140
|
+
agentId,
|
|
3141
|
+
byTool,
|
|
3142
|
+
total: totalAgg,
|
|
3143
|
+
freeTierRemaining
|
|
3144
|
+
};
|
|
3145
|
+
}
|
|
3146
|
+
/**
|
|
3147
|
+
* Get aggregated usage across all agents for a period.
|
|
3148
|
+
*/
|
|
3149
|
+
getGlobalUsage(period = "monthly") {
|
|
3150
|
+
const { start, end } = this.getPeriodBounds(period);
|
|
3151
|
+
const agg = {
|
|
3152
|
+
totalCalls: 0,
|
|
3153
|
+
totalCost: 0,
|
|
3154
|
+
freeTierCalls: 0,
|
|
3155
|
+
freeTierCost: 0,
|
|
3156
|
+
paidCalls: 0,
|
|
3157
|
+
paidCost: 0,
|
|
3158
|
+
periodStart: new Date(start).toISOString(),
|
|
3159
|
+
periodEnd: new Date(end).toISOString()
|
|
3160
|
+
};
|
|
3161
|
+
for (const events of this.events.values()) {
|
|
3162
|
+
for (const event of events) {
|
|
3163
|
+
if (event.timestamp >= start && event.timestamp < end) {
|
|
3164
|
+
agg.totalCalls++;
|
|
3165
|
+
agg.totalCost += event.cost;
|
|
3166
|
+
if (event.freeTier) {
|
|
3167
|
+
agg.freeTierCalls++;
|
|
3168
|
+
agg.freeTierCost += event.cost;
|
|
3169
|
+
} else {
|
|
3170
|
+
agg.paidCalls++;
|
|
3171
|
+
agg.paidCost += event.cost;
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
return agg;
|
|
3177
|
+
}
|
|
3178
|
+
/**
|
|
3179
|
+
* Get top tools by usage cost.
|
|
3180
|
+
*/
|
|
3181
|
+
getTopTools(period = "monthly", limit = 10) {
|
|
3182
|
+
const { start, end } = this.getPeriodBounds(period);
|
|
3183
|
+
const toolMap = /* @__PURE__ */ new Map();
|
|
3184
|
+
for (const events of this.events.values()) {
|
|
3185
|
+
for (const event of events) {
|
|
3186
|
+
if (event.timestamp >= start && event.timestamp < end) {
|
|
3187
|
+
const existing = toolMap.get(event.toolId) ?? { calls: 0, cost: 0 };
|
|
3188
|
+
existing.calls++;
|
|
3189
|
+
existing.cost += event.cost;
|
|
3190
|
+
toolMap.set(event.toolId, existing);
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
return [...toolMap.entries()].map(([toolId, data]) => ({ toolId, ...data })).sort((a, b) => b.cost - a.cost).slice(0, limit);
|
|
3195
|
+
}
|
|
3196
|
+
// ===========================================================================
|
|
3197
|
+
// FREE TIER
|
|
3198
|
+
// ===========================================================================
|
|
3199
|
+
/**
|
|
3200
|
+
* Get free-tier remaining for an agent.
|
|
3201
|
+
*/
|
|
3202
|
+
getFreeTierRemaining(agentId) {
|
|
3203
|
+
this.checkMonthReset();
|
|
3204
|
+
const used = this.freeTierUsed.get(agentId) ?? 0;
|
|
3205
|
+
return Math.max(0, this.config.freeTier.monthlyAllowance - used);
|
|
3206
|
+
}
|
|
3207
|
+
/**
|
|
3208
|
+
* Check if an agent has exceeded their free tier.
|
|
3209
|
+
*/
|
|
3210
|
+
isOverFreeTier(agentId) {
|
|
3211
|
+
return this.getFreeTierRemaining(agentId) === 0;
|
|
3212
|
+
}
|
|
3213
|
+
// ===========================================================================
|
|
3214
|
+
// QUERIES
|
|
3215
|
+
// ===========================================================================
|
|
3216
|
+
/**
|
|
3217
|
+
* Get all tracked agent IDs.
|
|
3218
|
+
*/
|
|
3219
|
+
getTrackedAgents() {
|
|
3220
|
+
return [...this.events.keys()];
|
|
3221
|
+
}
|
|
3222
|
+
/**
|
|
3223
|
+
* Get raw events for an agent.
|
|
3224
|
+
*/
|
|
3225
|
+
getEvents(agentId) {
|
|
3226
|
+
return [...this.events.get(agentId) ?? []];
|
|
3227
|
+
}
|
|
3228
|
+
/**
|
|
3229
|
+
* Get total number of recorded events.
|
|
3230
|
+
*/
|
|
3231
|
+
getTotalEventCount() {
|
|
3232
|
+
let total = 0;
|
|
3233
|
+
for (const events of this.events.values()) {
|
|
3234
|
+
total += events.length;
|
|
3235
|
+
}
|
|
3236
|
+
return total;
|
|
3237
|
+
}
|
|
3238
|
+
// ===========================================================================
|
|
3239
|
+
// INTERNALS
|
|
3240
|
+
// ===========================================================================
|
|
3241
|
+
getMonthKey(timestamp) {
|
|
3242
|
+
const d = new Date(timestamp);
|
|
3243
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
|
|
3244
|
+
}
|
|
3245
|
+
checkMonthReset() {
|
|
3246
|
+
const currentKey = this.getMonthKey(Date.now());
|
|
3247
|
+
if (currentKey !== this.currentMonthKey) {
|
|
3248
|
+
this.freeTierUsed.clear();
|
|
3249
|
+
this.currentMonthKey = currentKey;
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
getPeriodBounds(period) {
|
|
3253
|
+
const now = /* @__PURE__ */ new Date();
|
|
3254
|
+
let start;
|
|
3255
|
+
switch (period) {
|
|
3256
|
+
case "hourly":
|
|
3257
|
+
start = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours());
|
|
3258
|
+
return { start: start.getTime(), end: start.getTime() + 36e5 };
|
|
3259
|
+
case "daily":
|
|
3260
|
+
start = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
3261
|
+
return { start: start.getTime(), end: start.getTime() + 864e5 };
|
|
3262
|
+
case "monthly":
|
|
3263
|
+
start = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
3264
|
+
const nextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1);
|
|
3265
|
+
return { start: start.getTime(), end: nextMonth.getTime() };
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
emitTelemetry(type, data) {
|
|
3269
|
+
this.config.telemetry?.record({
|
|
3270
|
+
type,
|
|
3271
|
+
severity: "info",
|
|
3272
|
+
agentId: "usage-meter",
|
|
3273
|
+
data
|
|
3274
|
+
});
|
|
3275
|
+
}
|
|
3276
|
+
};
|
|
3277
|
+
|
|
3278
|
+
// src/economy/BountyManager.ts
|
|
3279
|
+
var BountyManager = class {
|
|
3280
|
+
bounties = /* @__PURE__ */ new Map();
|
|
3281
|
+
nextId = 1;
|
|
3282
|
+
config;
|
|
3283
|
+
constructor(config = {}) {
|
|
3284
|
+
this.config = config;
|
|
3285
|
+
}
|
|
3286
|
+
/** Create a bounty for a board task. */
|
|
3287
|
+
createBounty(taskId, reward, createdBy, deadline) {
|
|
3288
|
+
if (reward.amount <= 0) throw new Error("Bounty reward must be positive");
|
|
3289
|
+
const id = `bounty_${String(this.nextId++).padStart(4, "0")}`;
|
|
3290
|
+
const now = Date.now();
|
|
3291
|
+
const bounty = {
|
|
3292
|
+
id,
|
|
3293
|
+
taskId,
|
|
3294
|
+
reward,
|
|
3295
|
+
status: "open",
|
|
3296
|
+
createdBy,
|
|
3297
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3298
|
+
deadline: deadline ?? (this.config.defaultDeadlineMs ? now + this.config.defaultDeadlineMs : void 0)
|
|
3299
|
+
};
|
|
3300
|
+
this.bounties.set(id, bounty);
|
|
3301
|
+
return bounty;
|
|
3302
|
+
}
|
|
3303
|
+
/** Claim an open bounty. */
|
|
3304
|
+
claimBounty(bountyId, agentId) {
|
|
3305
|
+
const bounty = this.bounties.get(bountyId);
|
|
3306
|
+
if (!bounty) return { success: false, bountyId, error: "Bounty not found" };
|
|
3307
|
+
if (bounty.status !== "open")
|
|
3308
|
+
return { success: false, bountyId, error: `Bounty is ${bounty.status}, not open` };
|
|
3309
|
+
if (bounty.deadline && Date.now() > bounty.deadline) {
|
|
3310
|
+
bounty.status = "expired";
|
|
3311
|
+
return { success: false, bountyId, error: "Bounty has expired" };
|
|
3312
|
+
}
|
|
3313
|
+
bounty.status = "claimed";
|
|
3314
|
+
bounty.claimedBy = agentId;
|
|
3315
|
+
return { success: true, bountyId };
|
|
3316
|
+
}
|
|
3317
|
+
/** Complete a bounty with proof of work and trigger payout. */
|
|
3318
|
+
completeBounty(bountyId, proof) {
|
|
3319
|
+
const bounty = this.bounties.get(bountyId);
|
|
3320
|
+
if (!bounty)
|
|
3321
|
+
return {
|
|
3322
|
+
success: false,
|
|
3323
|
+
bountyId,
|
|
3324
|
+
amount: 0,
|
|
3325
|
+
currency: "credits",
|
|
3326
|
+
settlement: "ledger",
|
|
3327
|
+
error: "Bounty not found"
|
|
3328
|
+
};
|
|
3329
|
+
if (bounty.status !== "claimed")
|
|
3330
|
+
return {
|
|
3331
|
+
success: false,
|
|
3332
|
+
bountyId,
|
|
3333
|
+
amount: 0,
|
|
3334
|
+
currency: bounty.reward.currency,
|
|
3335
|
+
settlement: "ledger",
|
|
3336
|
+
error: `Bounty is ${bounty.status}, not claimed`
|
|
3337
|
+
};
|
|
3338
|
+
if (!proof.summary || proof.summary.trim().length === 0) {
|
|
3339
|
+
return {
|
|
3340
|
+
success: false,
|
|
3341
|
+
bountyId,
|
|
3342
|
+
amount: 0,
|
|
3343
|
+
currency: bounty.reward.currency,
|
|
3344
|
+
settlement: "ledger",
|
|
3345
|
+
error: "Completion proof requires a summary"
|
|
3346
|
+
};
|
|
3347
|
+
}
|
|
3348
|
+
bounty.status = "completed";
|
|
3349
|
+
bounty.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3350
|
+
const settlement = bounty.reward.currency === "credits" ? "ledger" : bounty.reward.amount < 0.1 ? "ledger" : "on_chain";
|
|
3351
|
+
return {
|
|
3352
|
+
success: true,
|
|
3353
|
+
bountyId,
|
|
3354
|
+
amount: bounty.reward.amount,
|
|
3355
|
+
currency: bounty.reward.currency,
|
|
3356
|
+
settlement
|
|
3357
|
+
};
|
|
3358
|
+
}
|
|
3359
|
+
/** Get a bounty by ID. */
|
|
3360
|
+
getBounty(bountyId) {
|
|
3361
|
+
return this.bounties.get(bountyId);
|
|
3362
|
+
}
|
|
3363
|
+
/** List bounties, optionally filtered by status. */
|
|
3364
|
+
list(status) {
|
|
3365
|
+
const all = Array.from(this.bounties.values());
|
|
3366
|
+
if (!status) return all;
|
|
3367
|
+
return all.filter((b) => b.status === status);
|
|
3368
|
+
}
|
|
3369
|
+
/** List bounties for a specific task. */
|
|
3370
|
+
byTask(taskId) {
|
|
3371
|
+
return Array.from(this.bounties.values()).filter((b) => b.taskId === taskId);
|
|
3372
|
+
}
|
|
3373
|
+
/** Expire bounties past their deadline. Returns count expired. */
|
|
3374
|
+
expireStale() {
|
|
3375
|
+
const now = Date.now();
|
|
3376
|
+
let count = 0;
|
|
3377
|
+
for (const bounty of this.bounties.values()) {
|
|
3378
|
+
if (bounty.deadline && now > bounty.deadline && bounty.status === "open") {
|
|
3379
|
+
bounty.status = "expired";
|
|
3380
|
+
count++;
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
return count;
|
|
3384
|
+
}
|
|
3385
|
+
/** Total open bounty value in a given currency. */
|
|
3386
|
+
totalOpen(currency) {
|
|
3387
|
+
return this.list("open").filter((b) => !currency || b.reward.currency === currency).reduce((sum, b) => sum + b.reward.amount, 0);
|
|
3388
|
+
}
|
|
3389
|
+
};
|
|
3390
|
+
|
|
3391
|
+
// src/economy/KnowledgeMarketplace.ts
|
|
3392
|
+
var DEFAULT_TYPE_WEIGHTS = {
|
|
3393
|
+
wisdom: 0.05,
|
|
3394
|
+
pattern: 0.03,
|
|
3395
|
+
gotcha: 0.02
|
|
3396
|
+
};
|
|
3397
|
+
var KnowledgeMarketplace = class {
|
|
3398
|
+
listings = /* @__PURE__ */ new Map();
|
|
3399
|
+
purchases = /* @__PURE__ */ new Map();
|
|
3400
|
+
// buyer -> purchases
|
|
3401
|
+
nextId = 1;
|
|
3402
|
+
pricingFactors;
|
|
3403
|
+
constructor(pricingFactors) {
|
|
3404
|
+
this.pricingFactors = pricingFactors ?? {};
|
|
3405
|
+
}
|
|
3406
|
+
/** Estimate the value of a knowledge entry (in USDC). */
|
|
3407
|
+
priceKnowledge(entry) {
|
|
3408
|
+
const weights = this.pricingFactors.typeWeights ?? DEFAULT_TYPE_WEIGHTS;
|
|
3409
|
+
let price = weights[entry.type] ?? 0.02;
|
|
3410
|
+
if (entry.confidence >= 0.8) {
|
|
3411
|
+
price *= this.pricingFactors.confidenceMultiplier ?? 1.5;
|
|
3412
|
+
}
|
|
3413
|
+
if (entry.reuseCount >= 5) {
|
|
3414
|
+
price *= this.pricingFactors.reuseMultiplier ?? 2;
|
|
3415
|
+
}
|
|
3416
|
+
if (entry.queryCount >= 10) {
|
|
3417
|
+
price *= 1.25;
|
|
3418
|
+
}
|
|
3419
|
+
return Math.round(price * 1e4) / 1e4;
|
|
3420
|
+
}
|
|
3421
|
+
/** List a knowledge entry for sale. */
|
|
3422
|
+
sellKnowledge(entry, price, seller, currency = "USDC") {
|
|
3423
|
+
if (price <= 0) return { success: false, listingId: "", error: "Price must be positive" };
|
|
3424
|
+
for (const listing2 of this.listings.values()) {
|
|
3425
|
+
if (listing2.entryId === entry.id && listing2.status === "active") {
|
|
3426
|
+
return { success: false, listingId: listing2.id, error: "Entry already listed" };
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
const id = `listing_${String(this.nextId++).padStart(4, "0")}`;
|
|
3430
|
+
const listing = {
|
|
3431
|
+
id,
|
|
3432
|
+
entryId: entry.id,
|
|
3433
|
+
seller,
|
|
3434
|
+
price,
|
|
3435
|
+
currency,
|
|
3436
|
+
status: "active",
|
|
3437
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3438
|
+
preview: {
|
|
3439
|
+
type: entry.type,
|
|
3440
|
+
domain: entry.domain,
|
|
3441
|
+
snippet: entry.content.slice(0, 100)
|
|
3442
|
+
}
|
|
3443
|
+
};
|
|
3444
|
+
this.listings.set(id, listing);
|
|
3445
|
+
return { success: true, listingId: id };
|
|
3446
|
+
}
|
|
3447
|
+
/** Buy a listed knowledge entry. */
|
|
3448
|
+
buyKnowledge(listingId, buyer) {
|
|
3449
|
+
const listing = this.listings.get(listingId);
|
|
3450
|
+
if (!listing) return { success: false, listingId, buyer, price: 0, error: "Listing not found" };
|
|
3451
|
+
if (listing.status !== "active")
|
|
3452
|
+
return {
|
|
3453
|
+
success: false,
|
|
3454
|
+
listingId,
|
|
3455
|
+
buyer,
|
|
3456
|
+
price: listing.price,
|
|
3457
|
+
error: `Listing is ${listing.status}`
|
|
3458
|
+
};
|
|
3459
|
+
if (listing.seller === buyer)
|
|
3460
|
+
return {
|
|
3461
|
+
success: false,
|
|
3462
|
+
listingId,
|
|
3463
|
+
buyer,
|
|
3464
|
+
price: listing.price,
|
|
3465
|
+
error: "Cannot buy your own listing"
|
|
3466
|
+
};
|
|
3467
|
+
listing.status = "sold";
|
|
3468
|
+
const result = {
|
|
3469
|
+
success: true,
|
|
3470
|
+
listingId,
|
|
3471
|
+
entryId: listing.entryId,
|
|
3472
|
+
buyer,
|
|
3473
|
+
price: listing.price
|
|
3474
|
+
};
|
|
3475
|
+
const buyerPurchases = this.purchases.get(buyer) ?? [];
|
|
3476
|
+
buyerPurchases.push(result);
|
|
3477
|
+
this.purchases.set(buyer, buyerPurchases);
|
|
3478
|
+
return result;
|
|
3479
|
+
}
|
|
3480
|
+
/** Get a listing by ID. */
|
|
3481
|
+
getListing(listingId) {
|
|
3482
|
+
return this.listings.get(listingId);
|
|
3483
|
+
}
|
|
3484
|
+
/** List all active listings. */
|
|
3485
|
+
activeListings() {
|
|
3486
|
+
return Array.from(this.listings.values()).filter((l) => l.status === "active");
|
|
3487
|
+
}
|
|
3488
|
+
/** Get purchase history for a buyer. */
|
|
3489
|
+
purchaseHistory(buyer) {
|
|
3490
|
+
return this.purchases.get(buyer) ?? [];
|
|
3491
|
+
}
|
|
3492
|
+
/** Delist an entry (only the seller can delist). */
|
|
3493
|
+
delist(listingId, seller) {
|
|
3494
|
+
const listing = this.listings.get(listingId);
|
|
3495
|
+
if (!listing || listing.seller !== seller || listing.status !== "active") return false;
|
|
3496
|
+
listing.status = "delisted";
|
|
3497
|
+
return true;
|
|
3498
|
+
}
|
|
3499
|
+
/** Total revenue for a seller across all sold listings. */
|
|
3500
|
+
sellerRevenue(seller) {
|
|
3501
|
+
return Array.from(this.listings.values()).filter((l) => l.seller === seller && l.status === "sold").reduce((sum, l) => sum + l.price, 0);
|
|
3502
|
+
}
|
|
3503
|
+
/** Total marketplace volume (all completed sales). */
|
|
3504
|
+
totalVolume() {
|
|
3505
|
+
return Array.from(this.listings.values()).filter((l) => l.status === "sold").reduce((sum, l) => sum + l.price, 0);
|
|
3506
|
+
}
|
|
3507
|
+
};
|
|
3508
|
+
|
|
3509
|
+
// src/economy/RevenueSplitter.ts
|
|
3510
|
+
var TOTAL_BASIS_POINTS = 1e4;
|
|
3511
|
+
var RevenueSplitter = class {
|
|
3512
|
+
recipients;
|
|
3513
|
+
/**
|
|
3514
|
+
* Create a revenue splitter.
|
|
3515
|
+
*
|
|
3516
|
+
* @param recipients — Array of recipients with basis points.
|
|
3517
|
+
* Basis points must sum to exactly 10000 (100%).
|
|
3518
|
+
* @throws Error if basis points don't sum to 10000 or any are negative.
|
|
3519
|
+
*/
|
|
3520
|
+
constructor(recipients) {
|
|
3521
|
+
if (recipients.length === 0) {
|
|
3522
|
+
throw new Error("At least one recipient is required");
|
|
3523
|
+
}
|
|
3524
|
+
const sum = recipients.reduce((acc, r) => acc + r.basisPoints, 0);
|
|
3525
|
+
if (sum !== TOTAL_BASIS_POINTS) {
|
|
3526
|
+
throw new Error(`Basis points must sum to ${TOTAL_BASIS_POINTS} (got ${sum})`);
|
|
3527
|
+
}
|
|
3528
|
+
for (const r of recipients) {
|
|
3529
|
+
if (r.basisPoints < 0) {
|
|
3530
|
+
throw new Error(`Negative basis points for "${r.id}": ${r.basisPoints}`);
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
const ids = new Set(recipients.map((r) => r.id));
|
|
3534
|
+
if (ids.size !== recipients.length) {
|
|
3535
|
+
throw new Error("Duplicate recipient IDs");
|
|
3536
|
+
}
|
|
3537
|
+
this.recipients = [...recipients];
|
|
3538
|
+
}
|
|
3539
|
+
/**
|
|
3540
|
+
* Split an amount among recipients.
|
|
3541
|
+
*
|
|
3542
|
+
* Uses bigint arithmetic for exact splitting.
|
|
3543
|
+
* Dust (remainder from integer division) goes to the first recipient.
|
|
3544
|
+
*
|
|
3545
|
+
* @param totalAmount — Total amount to split (in base units, e.g., USDC 6 decimals)
|
|
3546
|
+
* @returns SplitResult with exact shares summing to totalAmount
|
|
3547
|
+
*/
|
|
3548
|
+
split(totalAmount) {
|
|
3549
|
+
if (totalAmount < 0n) {
|
|
3550
|
+
throw new Error("Cannot split negative amount");
|
|
3551
|
+
}
|
|
3552
|
+
const shares = /* @__PURE__ */ new Map();
|
|
3553
|
+
const breakdown = [];
|
|
3554
|
+
let allocated = 0n;
|
|
3555
|
+
for (const recipient of this.recipients) {
|
|
3556
|
+
const share = totalAmount * BigInt(recipient.basisPoints) / BigInt(TOTAL_BASIS_POINTS);
|
|
3557
|
+
shares.set(recipient.id, share);
|
|
3558
|
+
allocated += share;
|
|
3559
|
+
breakdown.push({
|
|
3560
|
+
recipientId: recipient.id,
|
|
3561
|
+
basisPoints: recipient.basisPoints,
|
|
3562
|
+
amount: share,
|
|
3563
|
+
percentage: `${(recipient.basisPoints / 100).toFixed(2)}%`
|
|
3564
|
+
});
|
|
3565
|
+
}
|
|
3566
|
+
const dust = totalAmount - allocated;
|
|
3567
|
+
if (dust > 0n) {
|
|
3568
|
+
const firstId = this.recipients[0].id;
|
|
3569
|
+
const current = shares.get(firstId);
|
|
3570
|
+
shares.set(firstId, current + dust);
|
|
3571
|
+
breakdown[0].amount = current + dust;
|
|
3572
|
+
}
|
|
3573
|
+
return { shares, total: totalAmount, dust, breakdown };
|
|
3574
|
+
}
|
|
3575
|
+
/**
|
|
3576
|
+
* Split a numeric amount (convenience wrapper).
|
|
3577
|
+
* Converts to bigint internally.
|
|
3578
|
+
*/
|
|
3579
|
+
splitNumeric(totalAmount) {
|
|
3580
|
+
return this.split(BigInt(Math.floor(totalAmount)));
|
|
3581
|
+
}
|
|
3582
|
+
/**
|
|
3583
|
+
* Get the configured recipients.
|
|
3584
|
+
*/
|
|
3585
|
+
getRecipients() {
|
|
3586
|
+
return this.recipients;
|
|
3587
|
+
}
|
|
3588
|
+
/**
|
|
3589
|
+
* Validate that a split result sums correctly.
|
|
3590
|
+
*/
|
|
3591
|
+
static validate(result) {
|
|
3592
|
+
let sum = 0n;
|
|
3593
|
+
for (const amount of result.shares.values()) {
|
|
3594
|
+
sum += amount;
|
|
3595
|
+
}
|
|
3596
|
+
return sum === result.total;
|
|
3597
|
+
}
|
|
3598
|
+
};
|
|
3599
|
+
|
|
3600
|
+
// src/economy/InvisibleWallet.ts
|
|
3601
|
+
var InvisibleWalletStub = class _InvisibleWalletStub {
|
|
3602
|
+
address;
|
|
3603
|
+
chainId;
|
|
3604
|
+
isTestnet;
|
|
3605
|
+
constructor(address, config = {}) {
|
|
3606
|
+
this.address = address;
|
|
3607
|
+
this.isTestnet = config.testnet ?? false;
|
|
3608
|
+
this.chainId = this.isTestnet ? 84531 : 8453;
|
|
3609
|
+
}
|
|
3610
|
+
/**
|
|
3611
|
+
* Create from an address string (no private key needed for read-only).
|
|
3612
|
+
*/
|
|
3613
|
+
static fromAddress(address, config = {}) {
|
|
3614
|
+
const hex = address.startsWith("0x") ? address : `0x${address}`;
|
|
3615
|
+
return new _InvisibleWalletStub(hex, config);
|
|
3616
|
+
}
|
|
3617
|
+
/** Get the wallet address */
|
|
3618
|
+
getAddress() {
|
|
3619
|
+
return this.address;
|
|
3620
|
+
}
|
|
3621
|
+
/** Get chain ID */
|
|
3622
|
+
getChainId() {
|
|
3623
|
+
return this.chainId;
|
|
3624
|
+
}
|
|
3625
|
+
/** Get wallet info */
|
|
3626
|
+
getInfo() {
|
|
3627
|
+
return {
|
|
3628
|
+
address: this.address,
|
|
3629
|
+
chainId: this.chainId,
|
|
3630
|
+
isTestnet: this.isTestnet
|
|
3631
|
+
};
|
|
3632
|
+
}
|
|
3633
|
+
};
|
|
3634
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
3635
|
+
0 && (module.exports = {
|
|
3636
|
+
AgentBudgetEnforcer,
|
|
3637
|
+
BountyManager,
|
|
3638
|
+
CHAIN_IDS,
|
|
3639
|
+
CHAIN_ID_TO_NETWORK,
|
|
3640
|
+
CreatorRevenueAggregator,
|
|
3641
|
+
DEFAULT_COST_FLOOR,
|
|
3642
|
+
DEFAULT_LOD_SCALING,
|
|
3643
|
+
DEFAULT_TRAIT_UTILITIES,
|
|
3644
|
+
InvisibleWalletStub,
|
|
3645
|
+
KnowledgeMarketplace,
|
|
3646
|
+
MICRO_PAYMENT_THRESHOLD,
|
|
3647
|
+
MicroPaymentLedger,
|
|
3648
|
+
PLATFORM_LOD_SCALING,
|
|
3649
|
+
PaymentGateway,
|
|
3650
|
+
PaymentWebhookService,
|
|
3651
|
+
RevenueSplitter,
|
|
3652
|
+
SubscriptionManager,
|
|
3653
|
+
USDC_CONTRACTS,
|
|
3654
|
+
UnifiedBudgetOptimizer,
|
|
3655
|
+
UsageMeter,
|
|
3656
|
+
X402Facilitator,
|
|
3657
|
+
X402_VERSION,
|
|
3658
|
+
creditTraitHandler
|
|
3659
|
+
});
|