@agent-e/core 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -197,9 +197,9 @@ var DEFAULT_TICK_CONFIG = {
197
197
  var Observer = class {
198
198
  constructor(tickConfig) {
199
199
  this.previousMetrics = null;
200
- this.previousPrices = {};
200
+ this.previousPricesByCurrency = {};
201
201
  this.customMetricFns = {};
202
- this.anchorBaseline = null;
202
+ this.anchorBaselineByCurrency = {};
203
203
  this.tickConfig = { ...DEFAULT_TICK_CONFIG, ...tickConfig };
204
204
  }
205
205
  registerCustomMetric(name, fn) {
@@ -207,23 +207,28 @@ var Observer = class {
207
207
  }
208
208
  compute(state, recentEvents) {
209
209
  const tick = state.tick;
210
- const balances = Object.values(state.agentBalances);
211
210
  const roles = Object.values(state.agentRoles);
212
- const totalAgents = balances.length;
213
- let faucetVolume = 0;
214
- let sinkVolume = 0;
211
+ const totalAgents = Object.keys(state.agentBalances).length;
212
+ let productionAmount = 0;
213
+ const faucetVolumeByCurrency = {};
214
+ const sinkVolumeByCurrency = {};
215
215
  const tradeEvents = [];
216
216
  const roleChangeEvents = [];
217
217
  let churnCount = 0;
218
+ const defaultCurrency = state.currencies[0] ?? "default";
218
219
  for (const e of recentEvents) {
220
+ const curr = e.currency ?? defaultCurrency;
219
221
  switch (e.type) {
220
222
  case "mint":
221
223
  case "spawn":
222
- faucetVolume += e.amount ?? 0;
224
+ faucetVolumeByCurrency[curr] = (faucetVolumeByCurrency[curr] ?? 0) + (e.amount ?? 0);
223
225
  break;
224
226
  case "burn":
225
227
  case "consume":
226
- sinkVolume += e.amount ?? 0;
228
+ sinkVolumeByCurrency[curr] = (sinkVolumeByCurrency[curr] ?? 0) + (e.amount ?? 0);
229
+ break;
230
+ case "produce":
231
+ productionAmount += e.amount ?? 1;
227
232
  break;
228
233
  case "trade":
229
234
  tradeEvents.push(e);
@@ -237,20 +242,68 @@ var Observer = class {
237
242
  break;
238
243
  }
239
244
  }
240
- const totalSupply = balances.reduce((s, b) => s + b, 0);
245
+ const currencies = state.currencies;
246
+ const totalSupplyByCurrency = {};
247
+ const balancesByCurrency = {};
248
+ for (const [_agentId, balances] of Object.entries(state.agentBalances)) {
249
+ for (const [curr, bal] of Object.entries(balances)) {
250
+ totalSupplyByCurrency[curr] = (totalSupplyByCurrency[curr] ?? 0) + bal;
251
+ if (!balancesByCurrency[curr]) balancesByCurrency[curr] = [];
252
+ balancesByCurrency[curr].push(bal);
253
+ }
254
+ }
255
+ const netFlowByCurrency = {};
256
+ const tapSinkRatioByCurrency = {};
257
+ const inflationRateByCurrency = {};
258
+ const velocityByCurrency = {};
259
+ for (const curr of currencies) {
260
+ const faucet = faucetVolumeByCurrency[curr] ?? 0;
261
+ const sink = sinkVolumeByCurrency[curr] ?? 0;
262
+ netFlowByCurrency[curr] = faucet - sink;
263
+ tapSinkRatioByCurrency[curr] = sink > 0 ? faucet / sink : faucet > 0 ? Infinity : 1;
264
+ const prevSupply = this.previousMetrics?.totalSupplyByCurrency?.[curr] ?? totalSupplyByCurrency[curr] ?? 0;
265
+ const currSupply = totalSupplyByCurrency[curr] ?? 0;
266
+ inflationRateByCurrency[curr] = prevSupply > 0 ? (currSupply - prevSupply) / prevSupply : 0;
267
+ const currTrades = tradeEvents.filter((e) => (e.currency ?? defaultCurrency) === curr);
268
+ velocityByCurrency[curr] = currSupply > 0 ? currTrades.length / currSupply : 0;
269
+ }
270
+ const giniCoefficientByCurrency = {};
271
+ const medianBalanceByCurrency = {};
272
+ const meanBalanceByCurrency = {};
273
+ const top10PctShareByCurrency = {};
274
+ const meanMedianDivergenceByCurrency = {};
275
+ for (const curr of currencies) {
276
+ const bals = balancesByCurrency[curr] ?? [];
277
+ const sorted = [...bals].sort((a, b) => a - b);
278
+ const supply = totalSupplyByCurrency[curr] ?? 0;
279
+ const count = sorted.length;
280
+ const median = computeMedian(sorted);
281
+ const mean = count > 0 ? supply / count : 0;
282
+ const top10Idx = Math.floor(count * 0.9);
283
+ const top10Sum = sorted.slice(top10Idx).reduce((s, b) => s + b, 0);
284
+ giniCoefficientByCurrency[curr] = computeGini(sorted);
285
+ medianBalanceByCurrency[curr] = median;
286
+ meanBalanceByCurrency[curr] = mean;
287
+ top10PctShareByCurrency[curr] = supply > 0 ? top10Sum / supply : 0;
288
+ meanMedianDivergenceByCurrency[curr] = median > 0 ? Math.abs(mean - median) / median : 0;
289
+ }
290
+ const avgOf = (rec) => {
291
+ const vals = Object.values(rec);
292
+ return vals.length > 0 ? vals.reduce((s, v) => s + v, 0) / vals.length : 0;
293
+ };
294
+ const totalSupply = Object.values(totalSupplyByCurrency).reduce((s, v) => s + v, 0);
295
+ const faucetVolume = Object.values(faucetVolumeByCurrency).reduce((s, v) => s + v, 0);
296
+ const sinkVolume = Object.values(sinkVolumeByCurrency).reduce((s, v) => s + v, 0);
241
297
  const netFlow = faucetVolume - sinkVolume;
242
298
  const tapSinkRatio = sinkVolume > 0 ? faucetVolume / sinkVolume : faucetVolume > 0 ? Infinity : 1;
243
- const prevSupply = this.previousMetrics?.totalSupply ?? totalSupply;
244
- const inflationRate = prevSupply > 0 ? (totalSupply - prevSupply) / prevSupply : 0;
245
299
  const velocity = totalSupply > 0 ? tradeEvents.length / totalSupply : 0;
246
- const sortedBalances = [...balances].sort((a, b) => a - b);
300
+ const prevTotalSupply = this.previousMetrics?.totalSupply ?? totalSupply;
301
+ const inflationRate = prevTotalSupply > 0 ? (totalSupply - prevTotalSupply) / prevTotalSupply : 0;
302
+ const giniCoefficient = avgOf(giniCoefficientByCurrency);
303
+ const medianBalance = avgOf(medianBalanceByCurrency);
247
304
  const meanBalance = totalAgents > 0 ? totalSupply / totalAgents : 0;
248
- const medianBalance = computeMedian(sortedBalances);
249
- const top10Idx = Math.floor(totalAgents * 0.9);
250
- const top10Sum = sortedBalances.slice(top10Idx).reduce((s, b) => s + b, 0);
251
- const top10PctShare = totalSupply > 0 ? top10Sum / totalSupply : 0;
252
- const giniCoefficient = computeGini(sortedBalances);
253
- const meanMedianDivergence = medianBalance > 0 ? Math.abs(meanBalance - medianBalance) / medianBalance : 0;
305
+ const top10PctShare = avgOf(top10PctShareByCurrency);
306
+ const meanMedianDivergence = avgOf(meanMedianDivergenceByCurrency);
254
307
  const populationByRole = {};
255
308
  const roleShares = {};
256
309
  for (const role of roles) {
@@ -265,15 +318,25 @@ var Observer = class {
265
318
  churnByRole[role] = (churnByRole[role] ?? 0) + 1;
266
319
  }
267
320
  const churnRate = churnCount / Math.max(1, totalAgents);
268
- const prices = { ...state.marketPrices };
269
- const priceVolatility = {};
270
- for (const [resource, price] of Object.entries(prices)) {
271
- const prev = this.previousPrices[resource] ?? price;
272
- priceVolatility[resource] = prev > 0 ? Math.abs(price - prev) / prev : 0;
273
- }
274
- this.previousPrices = { ...prices };
275
- const priceValues = Object.values(prices);
276
- const priceIndex = priceValues.length > 0 ? priceValues.reduce((s, p) => s + p, 0) / priceValues.length : 0;
321
+ const pricesByCurrency = {};
322
+ const priceVolatilityByCurrency = {};
323
+ const priceIndexByCurrency = {};
324
+ for (const [curr, resourcePrices] of Object.entries(state.marketPrices)) {
325
+ pricesByCurrency[curr] = { ...resourcePrices };
326
+ const pricePrev = this.previousPricesByCurrency?.[curr] ?? {};
327
+ const volMap = {};
328
+ for (const [resource, price] of Object.entries(resourcePrices)) {
329
+ const prev = pricePrev[resource] ?? price;
330
+ volMap[resource] = prev > 0 ? Math.abs(price - prev) / prev : 0;
331
+ }
332
+ priceVolatilityByCurrency[curr] = volMap;
333
+ const pVals = Object.values(resourcePrices);
334
+ priceIndexByCurrency[curr] = pVals.length > 0 ? pVals.reduce((s, p) => s + p, 0) / pVals.length : 0;
335
+ }
336
+ this.previousPricesByCurrency = JSON.parse(JSON.stringify(pricesByCurrency));
337
+ const prices = pricesByCurrency[defaultCurrency] ?? {};
338
+ const priceVolatility = priceVolatilityByCurrency[defaultCurrency] ?? {};
339
+ const priceIndex = priceIndexByCurrency[defaultCurrency] ?? 0;
277
340
  const supplyByResource = {};
278
341
  for (const inv of Object.values(state.agentInventories)) {
279
342
  for (const [resource, qty] of Object.entries(inv)) {
@@ -298,70 +361,92 @@ var Observer = class {
298
361
  pinchPoints[resource] = "optimal";
299
362
  }
300
363
  }
301
- const productionIndex = recentEvents.filter((e) => e.type === "produce").reduce((s, e) => s + (e.amount ?? 1), 0);
364
+ const productionIndex = productionAmount;
302
365
  const maxPossibleProduction = productionIndex + sinkVolume;
303
366
  const capacityUsage = maxPossibleProduction > 0 ? productionIndex / maxPossibleProduction : 0;
304
367
  const satisfactions = Object.values(state.agentSatisfaction ?? {});
305
368
  const avgSatisfaction = satisfactions.length > 0 ? satisfactions.reduce((s, v) => s + v, 0) / satisfactions.length : 80;
306
369
  const blockedAgentCount = satisfactions.filter((s) => s < 20).length;
307
370
  const timeToValue = totalAgents > 0 ? blockedAgentCount / totalAgents * 100 : 0;
308
- const poolSizes = { ...state.poolSizes ?? {} };
309
- if (!this.anchorBaseline && tick === 1 && totalSupply > 0) {
310
- this.anchorBaseline = {
311
- currencyPerPeriod: totalSupply / Math.max(1, totalAgents),
312
- itemsPerCurrency: priceIndex > 0 ? 1 / priceIndex : 0
313
- };
371
+ const poolSizesByCurrency = {};
372
+ const poolSizesAggregate = {};
373
+ if (state.poolSizes) {
374
+ for (const [pool, currencyAmounts] of Object.entries(state.poolSizes)) {
375
+ poolSizesByCurrency[pool] = { ...currencyAmounts };
376
+ poolSizesAggregate[pool] = Object.values(currencyAmounts).reduce((s, v) => s + v, 0);
377
+ }
314
378
  }
315
- let anchorRatioDrift = 0;
316
- if (this.anchorBaseline && totalAgents > 0) {
317
- const currentCurrencyPerPeriod = totalSupply / totalAgents;
318
- anchorRatioDrift = this.anchorBaseline.currencyPerPeriod > 0 ? (currentCurrencyPerPeriod - this.anchorBaseline.currencyPerPeriod) / this.anchorBaseline.currencyPerPeriod : 0;
319
- }
320
- let arbitrageIndex = 0;
321
- const priceKeys = Object.keys(prices).filter((k) => prices[k] > 0);
322
- if (priceKeys.length >= 2) {
323
- let pairCount = 0;
324
- let totalDivergence = 0;
325
- for (let i = 0; i < priceKeys.length; i++) {
326
- for (let j = i + 1; j < priceKeys.length; j++) {
327
- const pA = prices[priceKeys[i]];
328
- const pB = prices[priceKeys[j]];
329
- let ratio = pA / pB;
330
- ratio = Math.max(1e-3, Math.min(1e3, ratio));
331
- totalDivergence += Math.abs(Math.log(ratio));
332
- pairCount++;
379
+ const anchorRatioDriftByCurrency = {};
380
+ if (tick === 1) {
381
+ for (const curr of currencies) {
382
+ const supply = totalSupplyByCurrency[curr] ?? 0;
383
+ if (supply > 0) {
384
+ this.anchorBaselineByCurrency[curr] = {
385
+ currencyPerPeriod: supply / Math.max(1, totalAgents),
386
+ itemsPerCurrency: (priceIndexByCurrency[curr] ?? 0) > 0 ? 1 / priceIndexByCurrency[curr] : 0
387
+ };
333
388
  }
334
389
  }
335
- arbitrageIndex = pairCount > 0 ? Math.min(1, totalDivergence / pairCount) : 0;
336
390
  }
391
+ for (const curr of currencies) {
392
+ const baseline = this.anchorBaselineByCurrency[curr];
393
+ if (baseline && totalAgents > 0) {
394
+ const currentCPP = (totalSupplyByCurrency[curr] ?? 0) / totalAgents;
395
+ anchorRatioDriftByCurrency[curr] = baseline.currencyPerPeriod > 0 ? (currentCPP - baseline.currencyPerPeriod) / baseline.currencyPerPeriod : 0;
396
+ } else {
397
+ anchorRatioDriftByCurrency[curr] = 0;
398
+ }
399
+ }
400
+ const anchorRatioDrift = avgOf(anchorRatioDriftByCurrency);
401
+ const arbitrageIndexByCurrency = {};
402
+ for (const curr of currencies) {
403
+ const cPrices = pricesByCurrency[curr] ?? {};
404
+ const priceKeys = Object.keys(cPrices).filter((k) => cPrices[k] > 0);
405
+ if (priceKeys.length >= 2) {
406
+ let pairCount = 0;
407
+ let totalDivergence = 0;
408
+ for (let i = 0; i < priceKeys.length; i++) {
409
+ for (let j = i + 1; j < priceKeys.length; j++) {
410
+ const pA = cPrices[priceKeys[i]];
411
+ const pB = cPrices[priceKeys[j]];
412
+ let ratio = pA / pB;
413
+ ratio = Math.max(1e-3, Math.min(1e3, ratio));
414
+ totalDivergence += Math.abs(Math.log(ratio));
415
+ pairCount++;
416
+ }
417
+ }
418
+ arbitrageIndexByCurrency[curr] = pairCount > 0 ? Math.min(1, totalDivergence / pairCount) : 0;
419
+ } else {
420
+ arbitrageIndexByCurrency[curr] = 0;
421
+ }
422
+ }
423
+ const arbitrageIndex = avgOf(arbitrageIndexByCurrency);
337
424
  const contentDropEvents = recentEvents.filter(
338
425
  (e) => e.metadata?.["contentDrop"] === true
339
426
  );
340
427
  const contentDropAge = contentDropEvents.length > 0 ? tick - Math.max(...contentDropEvents.map((e) => e.timestamp)) : (this.previousMetrics?.contentDropAge ?? 0) + 1;
341
- let giftTrades = 0;
342
- if (tradeEvents.length > 0) {
343
- for (const e of tradeEvents) {
344
- const marketPrice = prices[e.resource ?? ""] ?? 0;
428
+ const giftTradeRatioByCurrency = {};
429
+ const disposalTradeRatioByCurrency = {};
430
+ for (const curr of currencies) {
431
+ const currTrades = tradeEvents.filter((e) => (e.currency ?? defaultCurrency) === curr);
432
+ const cPrices = pricesByCurrency[curr] ?? {};
433
+ let gifts = 0;
434
+ let disposals = 0;
435
+ for (const e of currTrades) {
436
+ const marketPrice = cPrices[e.resource ?? ""] ?? 0;
345
437
  const tradePrice = e.price ?? 0;
346
- if (tradePrice === 0 || marketPrice > 0 && tradePrice < marketPrice * 0.3) {
347
- giftTrades++;
348
- }
349
- }
350
- }
351
- const giftTradeRatio = tradeEvents.length > 0 ? giftTrades / tradeEvents.length : 0;
352
- let disposalTrades = 0;
353
- if (tradeEvents.length > 0) {
354
- for (const e of tradeEvents) {
438
+ if (tradePrice === 0 || marketPrice > 0 && tradePrice < marketPrice * 0.3) gifts++;
355
439
  if (e.from && e.resource) {
356
440
  const sellerInv = state.agentInventories[e.from]?.[e.resource] ?? 0;
357
441
  const avgInv = (supplyByResource[e.resource] ?? 0) / Math.max(1, totalAgents);
358
- if (sellerInv > avgInv * 3) {
359
- disposalTrades++;
360
- }
442
+ if (sellerInv > avgInv * 3) disposals++;
361
443
  }
362
444
  }
445
+ giftTradeRatioByCurrency[curr] = currTrades.length > 0 ? gifts / currTrades.length : 0;
446
+ disposalTradeRatioByCurrency[curr] = currTrades.length > 0 ? disposals / currTrades.length : 0;
363
447
  }
364
- const disposalTradeRatio = tradeEvents.length > 0 ? disposalTrades / tradeEvents.length : 0;
448
+ const giftTradeRatio = avgOf(giftTradeRatioByCurrency);
449
+ const disposalTradeRatio = avgOf(disposalTradeRatioByCurrency);
365
450
  const custom = {};
366
451
  for (const [name, fn] of Object.entries(this.customMetricFns)) {
367
452
  try {
@@ -373,10 +458,57 @@ var Observer = class {
373
458
  const metrics = {
374
459
  tick,
375
460
  timestamp: Date.now(),
461
+ currencies,
462
+ // Per-currency
463
+ totalSupplyByCurrency,
464
+ netFlowByCurrency,
465
+ velocityByCurrency,
466
+ inflationRateByCurrency,
467
+ faucetVolumeByCurrency,
468
+ sinkVolumeByCurrency,
469
+ tapSinkRatioByCurrency,
470
+ anchorRatioDriftByCurrency,
471
+ giniCoefficientByCurrency,
472
+ medianBalanceByCurrency,
473
+ meanBalanceByCurrency,
474
+ top10PctShareByCurrency,
475
+ meanMedianDivergenceByCurrency,
476
+ priceIndexByCurrency,
477
+ pricesByCurrency,
478
+ priceVolatilityByCurrency,
479
+ poolSizesByCurrency,
480
+ extractionRatioByCurrency: {},
481
+ newUserDependencyByCurrency: {},
482
+ currencyInsulationByCurrency: {},
483
+ arbitrageIndexByCurrency,
484
+ giftTradeRatioByCurrency,
485
+ disposalTradeRatioByCurrency,
486
+ // Aggregates
376
487
  totalSupply,
377
488
  netFlow,
378
489
  velocity,
379
490
  inflationRate,
491
+ faucetVolume,
492
+ sinkVolume,
493
+ tapSinkRatio,
494
+ anchorRatioDrift,
495
+ giniCoefficient,
496
+ medianBalance,
497
+ meanBalance,
498
+ top10PctShare,
499
+ meanMedianDivergence,
500
+ priceIndex,
501
+ prices,
502
+ priceVolatility,
503
+ poolSizes: poolSizesAggregate,
504
+ extractionRatio: NaN,
505
+ newUserDependency: NaN,
506
+ smokeTestRatio: NaN,
507
+ currencyInsulation: NaN,
508
+ arbitrageIndex,
509
+ giftTradeRatio,
510
+ disposalTradeRatio,
511
+ // Unchanged
380
512
  populationByRole,
381
513
  roleShares,
382
514
  totalAgents,
@@ -384,38 +516,18 @@ var Observer = class {
384
516
  churnByRole,
385
517
  personaDistribution: {},
386
518
  // populated by PersonaTracker
387
- giniCoefficient,
388
- medianBalance,
389
- meanBalance,
390
- top10PctShare,
391
- meanMedianDivergence,
392
- priceIndex,
393
519
  productionIndex,
394
520
  capacityUsage,
395
- prices,
396
- priceVolatility,
397
521
  supplyByResource,
398
522
  demandSignals,
399
523
  pinchPoints,
400
524
  avgSatisfaction,
401
525
  blockedAgentCount,
402
526
  timeToValue,
403
- faucetVolume,
404
- sinkVolume,
405
- tapSinkRatio,
406
- poolSizes,
407
- anchorRatioDrift,
408
- extractionRatio: NaN,
409
- newUserDependency: NaN,
410
- smokeTestRatio: NaN,
411
- currencyInsulation: NaN,
412
527
  sharkToothPeaks: this.previousMetrics?.sharkToothPeaks ?? [],
413
528
  sharkToothValleys: this.previousMetrics?.sharkToothValleys ?? [],
414
529
  eventCompletionRate: NaN,
415
- arbitrageIndex,
416
530
  contentDropAge,
417
- giftTradeRatio,
418
- disposalTradeRatio,
419
531
  custom
420
532
  };
421
533
  this.previousMetrics = metrics;
@@ -489,48 +601,75 @@ function emptyMetrics(tick = 0) {
489
601
  return {
490
602
  tick,
491
603
  timestamp: Date.now(),
604
+ currencies: [],
605
+ // Per-currency
606
+ totalSupplyByCurrency: {},
607
+ netFlowByCurrency: {},
608
+ velocityByCurrency: {},
609
+ inflationRateByCurrency: {},
610
+ faucetVolumeByCurrency: {},
611
+ sinkVolumeByCurrency: {},
612
+ tapSinkRatioByCurrency: {},
613
+ anchorRatioDriftByCurrency: {},
614
+ giniCoefficientByCurrency: {},
615
+ medianBalanceByCurrency: {},
616
+ meanBalanceByCurrency: {},
617
+ top10PctShareByCurrency: {},
618
+ meanMedianDivergenceByCurrency: {},
619
+ priceIndexByCurrency: {},
620
+ pricesByCurrency: {},
621
+ priceVolatilityByCurrency: {},
622
+ poolSizesByCurrency: {},
623
+ extractionRatioByCurrency: {},
624
+ newUserDependencyByCurrency: {},
625
+ currencyInsulationByCurrency: {},
626
+ arbitrageIndexByCurrency: {},
627
+ giftTradeRatioByCurrency: {},
628
+ disposalTradeRatioByCurrency: {},
629
+ // Aggregates
492
630
  totalSupply: 0,
493
631
  netFlow: 0,
494
632
  velocity: 0,
495
633
  inflationRate: 0,
496
- populationByRole: {},
497
- roleShares: {},
498
- totalAgents: 0,
499
- churnRate: 0,
500
- churnByRole: {},
501
- personaDistribution: {},
634
+ faucetVolume: 0,
635
+ sinkVolume: 0,
636
+ tapSinkRatio: 1,
637
+ anchorRatioDrift: 0,
502
638
  giniCoefficient: 0,
503
639
  medianBalance: 0,
504
640
  meanBalance: 0,
505
641
  top10PctShare: 0,
506
642
  meanMedianDivergence: 0,
507
643
  priceIndex: 0,
508
- productionIndex: 0,
509
- capacityUsage: 0,
510
644
  prices: {},
511
645
  priceVolatility: {},
646
+ poolSizes: {},
647
+ extractionRatio: NaN,
648
+ newUserDependency: NaN,
649
+ smokeTestRatio: NaN,
650
+ currencyInsulation: NaN,
651
+ arbitrageIndex: 0,
652
+ giftTradeRatio: 0,
653
+ disposalTradeRatio: 0,
654
+ // Unchanged
655
+ populationByRole: {},
656
+ roleShares: {},
657
+ totalAgents: 0,
658
+ churnRate: 0,
659
+ churnByRole: {},
660
+ personaDistribution: {},
661
+ productionIndex: 0,
662
+ capacityUsage: 0,
512
663
  supplyByResource: {},
513
664
  demandSignals: {},
514
665
  pinchPoints: {},
515
666
  avgSatisfaction: 100,
516
667
  blockedAgentCount: 0,
517
668
  timeToValue: 0,
518
- faucetVolume: 0,
519
- sinkVolume: 0,
520
- tapSinkRatio: 1,
521
- poolSizes: {},
522
- anchorRatioDrift: 0,
523
- extractionRatio: NaN,
524
- newUserDependency: NaN,
525
- smokeTestRatio: NaN,
526
- currencyInsulation: NaN,
527
669
  sharkToothPeaks: [],
528
670
  sharkToothValleys: [],
529
671
  eventCompletionRate: NaN,
530
- arbitrageIndex: 0,
531
672
  contentDropAge: 0,
532
- giftTradeRatio: 0,
533
- disposalTradeRatio: 0,
534
673
  custom: {}
535
674
  };
536
675
  }
@@ -996,38 +1135,44 @@ var P12_OnePrimaryFaucet = {
996
1135
  id: "P12",
997
1136
  name: "One Primary Faucet",
998
1137
  category: "currency",
999
- description: "Multiple independent currency sources (gathering + crafting + quests) each creating gold causes uncontrolled inflation. One clear primary faucet makes the economy predictable and auditable.",
1138
+ description: "Multiple independent currency sources (gathering + production + quests) each creating currency causes uncontrolled inflation. One clear primary faucet makes the economy predictable and auditable.",
1000
1139
  check(metrics, thresholds) {
1001
- const { netFlow, faucetVolume, sinkVolume } = metrics;
1002
- if (netFlow > thresholds.netFlowWarnThreshold) {
1003
- return {
1004
- violated: true,
1005
- severity: 5,
1006
- evidence: { netFlow, faucetVolume, sinkVolume },
1007
- suggestedAction: {
1008
- parameter: "productionCost",
1009
- direction: "increase",
1010
- magnitude: 0.15,
1011
- reasoning: `Net flow +${netFlow.toFixed(1)} g/t. Inflationary. Increase crafting cost (primary sink) to balance faucet output.`
1012
- },
1013
- confidence: 0.8,
1014
- estimatedLag: 8
1015
- };
1016
- }
1017
- if (netFlow < -thresholds.netFlowWarnThreshold) {
1018
- return {
1019
- violated: true,
1020
- severity: 4,
1021
- evidence: { netFlow, faucetVolume, sinkVolume },
1022
- suggestedAction: {
1023
- parameter: "productionCost",
1024
- direction: "decrease",
1025
- magnitude: 0.15,
1026
- reasoning: `Net flow ${netFlow.toFixed(1)} g/t. Deflationary. Decrease crafting cost to ease sink pressure.`
1027
- },
1028
- confidence: 0.8,
1029
- estimatedLag: 8
1030
- };
1140
+ for (const curr of metrics.currencies) {
1141
+ const netFlow = metrics.netFlowByCurrency[curr] ?? 0;
1142
+ const faucetVolume = metrics.faucetVolumeByCurrency[curr] ?? 0;
1143
+ const sinkVolume = metrics.sinkVolumeByCurrency[curr] ?? 0;
1144
+ if (netFlow > thresholds.netFlowWarnThreshold) {
1145
+ return {
1146
+ violated: true,
1147
+ severity: 5,
1148
+ evidence: { currency: curr, netFlow, faucetVolume, sinkVolume },
1149
+ suggestedAction: {
1150
+ parameter: "productionCost",
1151
+ direction: "increase",
1152
+ currency: curr,
1153
+ magnitude: 0.15,
1154
+ reasoning: `[${curr}] Net flow +${netFlow.toFixed(1)}/tick. Inflationary. Increase production cost (primary sink) to balance faucet output.`
1155
+ },
1156
+ confidence: 0.8,
1157
+ estimatedLag: 8
1158
+ };
1159
+ }
1160
+ if (netFlow < -thresholds.netFlowWarnThreshold) {
1161
+ return {
1162
+ violated: true,
1163
+ severity: 4,
1164
+ evidence: { currency: curr, netFlow, faucetVolume, sinkVolume },
1165
+ suggestedAction: {
1166
+ parameter: "productionCost",
1167
+ direction: "decrease",
1168
+ currency: curr,
1169
+ magnitude: 0.15,
1170
+ reasoning: `[${curr}] Net flow ${netFlow.toFixed(1)}/tick. Deflationary. Decrease production cost to ease sink pressure.`
1171
+ },
1172
+ confidence: 0.8,
1173
+ estimatedLag: 8
1174
+ };
1175
+ }
1031
1176
  }
1032
1177
  return { violated: false };
1033
1178
  }
@@ -1038,28 +1183,30 @@ var P13_PotsAreZeroSumAndSelfRegulate = {
1038
1183
  category: "currency",
1039
1184
  description: "Competitive pot math: winRate \xD7 multiplier > (1 - houseCut) drains the pot. At 65% win rate, multiplier must be \u2264 1.38. We use 1.5 for slight surplus buffer.",
1040
1185
  check(metrics, thresholds) {
1041
- const { poolSizes, populationByRole } = metrics;
1042
- const totalAgents = metrics.totalAgents;
1186
+ const { populationByRole } = metrics;
1043
1187
  const roleEntries = Object.entries(populationByRole).sort((a, b) => b[1] - a[1]);
1044
- const dominantRole = roleEntries[0]?.[0];
1045
1188
  const dominantCount = roleEntries[0]?.[1] ?? 0;
1046
- for (const [poolName, poolSize] of Object.entries(poolSizes)) {
1047
- if (dominantCount > 5 && poolSize < 50) {
1048
- const { poolWinRate, poolHouseCut } = thresholds;
1049
- const maxSustainableMultiplier = (1 - poolHouseCut) / poolWinRate;
1050
- return {
1051
- violated: true,
1052
- severity: 7,
1053
- evidence: { pool: poolName, poolSize, participants: dominantCount, maxSustainableMultiplier },
1054
- suggestedAction: {
1055
- parameter: "rewardRate",
1056
- direction: "decrease",
1057
- magnitude: 0.15,
1058
- reasoning: `${poolName} pool at ${poolSize.toFixed(0)} currency with ${dominantCount} active participants. Sustainable multiplier \u2264 ${maxSustainableMultiplier.toFixed(2)}. Reduce reward multiplier to prevent pool drain.`
1059
- },
1060
- confidence: 0.85,
1061
- estimatedLag: 3
1062
- };
1189
+ for (const [poolName, currencyAmounts] of Object.entries(metrics.poolSizesByCurrency)) {
1190
+ for (const curr of metrics.currencies) {
1191
+ const poolSize = currencyAmounts[curr] ?? 0;
1192
+ if (dominantCount > 5 && poolSize < 50) {
1193
+ const { poolWinRate, poolHouseCut } = thresholds;
1194
+ const maxSustainableMultiplier = (1 - poolHouseCut) / poolWinRate;
1195
+ return {
1196
+ violated: true,
1197
+ severity: 7,
1198
+ evidence: { currency: curr, pool: poolName, poolSize, participants: dominantCount, maxSustainableMultiplier },
1199
+ suggestedAction: {
1200
+ parameter: "rewardRate",
1201
+ direction: "decrease",
1202
+ currency: curr,
1203
+ magnitude: 0.15,
1204
+ reasoning: `[${curr}] ${poolName} pool at ${poolSize.toFixed(0)} currency with ${dominantCount} active participants. Sustainable multiplier \u2264 ${maxSustainableMultiplier.toFixed(2)}. Reduce reward multiplier to prevent pool drain.`
1205
+ },
1206
+ confidence: 0.85,
1207
+ estimatedLag: 3
1208
+ };
1209
+ }
1063
1210
  }
1064
1211
  }
1065
1212
  return { violated: false };
@@ -1071,22 +1218,27 @@ var P14_TrackActualInjection = {
1071
1218
  category: "currency",
1072
1219
  description: 'Counting resource gathering as "currency injected" is misleading. Currency enters through faucet mechanisms (spawning, rewards). Fake metrics break every downstream decision.',
1073
1220
  check(metrics, _thresholds) {
1074
- const { faucetVolume, netFlow, totalSupply } = metrics;
1075
- const supplyGrowthRate = Math.abs(netFlow) / Math.max(1, totalSupply);
1076
- if (supplyGrowthRate > 0.1) {
1077
- return {
1078
- violated: true,
1079
- severity: 4,
1080
- evidence: { faucetVolume, netFlow, supplyGrowthRate },
1081
- suggestedAction: {
1082
- parameter: "yieldRate",
1083
- direction: "decrease",
1084
- magnitude: 0.1,
1085
- reasoning: `Supply growing at ${(supplyGrowthRate * 100).toFixed(1)}%/tick. Verify gold injection tracking. Resources should not create gold directly.`
1086
- },
1087
- confidence: 0.55,
1088
- estimatedLag: 5
1089
- };
1221
+ for (const curr of metrics.currencies) {
1222
+ const faucetVolume = metrics.faucetVolumeByCurrency[curr] ?? 0;
1223
+ const netFlow = metrics.netFlowByCurrency[curr] ?? 0;
1224
+ const totalSupply = metrics.totalSupplyByCurrency[curr] ?? 0;
1225
+ const supplyGrowthRate = Math.abs(netFlow) / Math.max(1, totalSupply);
1226
+ if (supplyGrowthRate > 0.1) {
1227
+ return {
1228
+ violated: true,
1229
+ severity: 4,
1230
+ evidence: { currency: curr, faucetVolume, netFlow, supplyGrowthRate },
1231
+ suggestedAction: {
1232
+ parameter: "yieldRate",
1233
+ direction: "decrease",
1234
+ currency: curr,
1235
+ magnitude: 0.1,
1236
+ reasoning: `[${curr}] Supply growing at ${(supplyGrowthRate * 100).toFixed(1)}%/tick. Verify currency injection tracking. Resources should not create currency directly.`
1237
+ },
1238
+ confidence: 0.55,
1239
+ estimatedLag: 5
1240
+ };
1241
+ }
1090
1242
  }
1091
1243
  return { violated: false };
1092
1244
  }
@@ -1095,26 +1247,30 @@ var P15_PoolsNeedCapAndDecay = {
1095
1247
  id: "P15",
1096
1248
  name: "Pools Need Cap + Decay",
1097
1249
  category: "currency",
1098
- description: "Any pool (bank, reward pool) without a cap accumulates infinitely. Bank pool at 42% of gold supply means 42% of the economy is frozen. Cap at 5%, decay at 2%/tick.",
1250
+ description: "Any pool (bank, reward pool) without a cap accumulates infinitely. A pool at 42% of total supply means 42% of the economy is frozen. Cap at 5%, decay at 2%/tick.",
1099
1251
  check(metrics, thresholds) {
1100
- const { poolSizes, totalSupply } = metrics;
1101
1252
  const { poolCapPercent } = thresholds;
1102
- for (const [pool, size] of Object.entries(poolSizes)) {
1103
- const shareOfSupply = size / Math.max(1, totalSupply);
1104
- if (shareOfSupply > poolCapPercent * 2) {
1105
- return {
1106
- violated: true,
1107
- severity: 6,
1108
- evidence: { pool, size, shareOfSupply, cap: poolCapPercent },
1109
- suggestedAction: {
1110
- parameter: "transactionFee",
1111
- direction: "decrease",
1112
- magnitude: 0.1,
1113
- reasoning: `${pool} pool at ${(shareOfSupply * 100).toFixed(1)}% of supply (cap: ${(poolCapPercent * 100).toFixed(0)}%). Gold frozen. Lower fees to encourage circulation over accumulation.`
1114
- },
1115
- confidence: 0.85,
1116
- estimatedLag: 5
1117
- };
1253
+ for (const [pool, currencyAmounts] of Object.entries(metrics.poolSizesByCurrency)) {
1254
+ for (const curr of metrics.currencies) {
1255
+ const size = currencyAmounts[curr] ?? 0;
1256
+ const totalSupply = metrics.totalSupplyByCurrency[curr] ?? 0;
1257
+ const shareOfSupply = size / Math.max(1, totalSupply);
1258
+ if (shareOfSupply > poolCapPercent * 2) {
1259
+ return {
1260
+ violated: true,
1261
+ severity: 6,
1262
+ evidence: { currency: curr, pool, size, shareOfSupply, cap: poolCapPercent },
1263
+ suggestedAction: {
1264
+ parameter: "transactionFee",
1265
+ direction: "decrease",
1266
+ currency: curr,
1267
+ magnitude: 0.1,
1268
+ reasoning: `[${curr}] ${pool} pool at ${(shareOfSupply * 100).toFixed(1)}% of supply (cap: ${(poolCapPercent * 100).toFixed(0)}%). Currency frozen. Lower fees to encourage circulation over accumulation.`
1269
+ },
1270
+ confidence: 0.85,
1271
+ estimatedLag: 5
1272
+ };
1273
+ }
1118
1274
  }
1119
1275
  }
1120
1276
  return { violated: false };
@@ -1126,23 +1282,27 @@ var P16_WithdrawalPenaltyScales = {
1126
1282
  category: "currency",
1127
1283
  description: "A 50-tick lock period with a penalty calculated as /100 means agents can exit after 1 tick and keep 99% of accrued yield. Penalty must scale linearly: (1 - ticksStaked/lockDuration) \xD7 yield.",
1128
1284
  check(metrics, _thresholds) {
1129
- const { poolSizes, totalSupply } = metrics;
1130
- const stakedEstimate = totalSupply * 0.15;
1131
- for (const [poolName, poolSize] of Object.entries(poolSizes)) {
1132
- if (poolSize < 10 && stakedEstimate > 100) {
1133
- return {
1134
- violated: true,
1135
- severity: 3,
1136
- evidence: { pool: poolName, poolSize, estimatedStaked: stakedEstimate },
1137
- suggestedAction: {
1138
- parameter: "transactionFee",
1139
- direction: "increase",
1140
- magnitude: 0.05,
1141
- reasoning: `${poolName} pool depleted while significant currency should be locked. Early withdrawals may be draining the pool. Ensure withdrawal penalty scales with lock duration.`
1142
- },
1143
- confidence: 0.45,
1144
- estimatedLag: 10
1145
- };
1285
+ for (const [poolName, currencyAmounts] of Object.entries(metrics.poolSizesByCurrency)) {
1286
+ for (const curr of metrics.currencies) {
1287
+ const poolSize = currencyAmounts[curr] ?? 0;
1288
+ const totalSupply = metrics.totalSupplyByCurrency[curr] ?? 0;
1289
+ const stakedEstimate = totalSupply * 0.15;
1290
+ if (poolSize < 10 && stakedEstimate > 100) {
1291
+ return {
1292
+ violated: true,
1293
+ severity: 3,
1294
+ evidence: { currency: curr, pool: poolName, poolSize, estimatedStaked: stakedEstimate },
1295
+ suggestedAction: {
1296
+ parameter: "transactionFee",
1297
+ direction: "increase",
1298
+ currency: curr,
1299
+ magnitude: 0.05,
1300
+ reasoning: `[${curr}] ${poolName} pool depleted while significant currency should be locked. Early withdrawals may be draining the pool. Ensure withdrawal penalty scales with lock duration.`
1301
+ },
1302
+ confidence: 0.45,
1303
+ estimatedLag: 10
1304
+ };
1305
+ }
1146
1306
  }
1147
1307
  }
1148
1308
  return { violated: false };
@@ -1154,22 +1314,27 @@ var P32_VelocityAboveSupply = {
1154
1314
  category: "currency",
1155
1315
  description: "Low transactions despite adequate supply means liquidity is trapped. High supply with low velocity = stagnation, not abundance.",
1156
1316
  check(metrics, _thresholds) {
1157
- const { velocity, totalSupply, supplyByResource } = metrics;
1317
+ const { supplyByResource } = metrics;
1158
1318
  const totalResources = Object.values(supplyByResource).reduce((s, v) => s + v, 0);
1159
- if (velocity < 3 && totalSupply > 100 && totalResources > 20) {
1160
- return {
1161
- violated: true,
1162
- severity: 4,
1163
- evidence: { velocity, totalSupply, totalResources },
1164
- suggestedAction: {
1165
- parameter: "transactionFee",
1166
- direction: "decrease",
1167
- magnitude: 0.2,
1168
- reasoning: `Velocity ${velocity}/t with ${totalResources} resources in system. Economy stagnant despite available supply. Lower trading friction.`
1169
- },
1170
- confidence: 0.75,
1171
- estimatedLag: 5
1172
- };
1319
+ for (const curr of metrics.currencies) {
1320
+ const velocity = metrics.velocityByCurrency[curr] ?? 0;
1321
+ const totalSupply = metrics.totalSupplyByCurrency[curr] ?? 0;
1322
+ if (velocity < 3 && totalSupply > 100 && totalResources > 20) {
1323
+ return {
1324
+ violated: true,
1325
+ severity: 4,
1326
+ evidence: { currency: curr, velocity, totalSupply, totalResources },
1327
+ suggestedAction: {
1328
+ parameter: "transactionFee",
1329
+ direction: "decrease",
1330
+ currency: curr,
1331
+ magnitude: 0.2,
1332
+ reasoning: `[${curr}] Velocity ${velocity}/t with ${totalResources} resources in system. Economy stagnant despite available supply. Lower trading friction.`
1333
+ },
1334
+ confidence: 0.75,
1335
+ estimatedLag: 5
1336
+ };
1337
+ }
1173
1338
  }
1174
1339
  return { violated: false };
1175
1340
  }
@@ -1180,32 +1345,38 @@ var P58_NoNaturalNumeraire = {
1180
1345
  category: "currency",
1181
1346
  description: "No single commodity naturally stabilizes as currency in barter-heavy economies. Multiple items rotate as de facto units of account, but none locks in. If a num\xE9raire is needed, design and enforce it \u2014 emergence alone will not produce one.",
1182
1347
  check(metrics, _thresholds) {
1183
- const { prices, velocity, totalSupply } = metrics;
1184
- const priceValues = Object.values(prices).filter((p) => p > 0);
1185
- if (priceValues.length < 3) return { violated: false };
1186
- const mean = priceValues.reduce((s, p) => s + p, 0) / priceValues.length;
1187
- const coeffOfVariation = mean > 0 ? Math.sqrt(
1188
- priceValues.reduce((s, p) => s + (p - mean) ** 2, 0) / priceValues.length
1189
- ) / mean : 0;
1190
- if (coeffOfVariation < 0.25 && velocity > 5 && totalSupply > 100) {
1191
- return {
1192
- violated: true,
1193
- severity: 3,
1194
- evidence: {
1195
- coeffOfVariation,
1196
- velocity,
1197
- numResources: priceValues.length,
1198
- meanPrice: mean
1199
- },
1200
- suggestedAction: {
1201
- parameter: "productionCost",
1202
- direction: "increase",
1203
- magnitude: 0.1,
1204
- reasoning: `Price coefficient of variation ${coeffOfVariation.toFixed(2)} with velocity ${velocity.toFixed(1)}. All items priced similarly in an active economy \u2014 no natural num\xE9raire emerging. If a designated currency exists, increase its sink demand to differentiate it.`
1205
- },
1206
- confidence: 0.5,
1207
- estimatedLag: 20
1208
- };
1348
+ for (const curr of metrics.currencies) {
1349
+ const currPrices = metrics.pricesByCurrency[curr] ?? {};
1350
+ const velocity = metrics.velocityByCurrency[curr] ?? 0;
1351
+ const totalSupply = metrics.totalSupplyByCurrency[curr] ?? 0;
1352
+ const priceValues = Object.values(currPrices).filter((p) => p > 0);
1353
+ if (priceValues.length < 3) continue;
1354
+ const mean = priceValues.reduce((s, p) => s + p, 0) / priceValues.length;
1355
+ const coeffOfVariation = mean > 0 ? Math.sqrt(
1356
+ priceValues.reduce((s, p) => s + (p - mean) ** 2, 0) / priceValues.length
1357
+ ) / mean : 0;
1358
+ if (coeffOfVariation < 0.25 && velocity > 5 && totalSupply > 100) {
1359
+ return {
1360
+ violated: true,
1361
+ severity: 3,
1362
+ evidence: {
1363
+ currency: curr,
1364
+ coeffOfVariation,
1365
+ velocity,
1366
+ numResources: priceValues.length,
1367
+ meanPrice: mean
1368
+ },
1369
+ suggestedAction: {
1370
+ parameter: "productionCost",
1371
+ direction: "increase",
1372
+ currency: curr,
1373
+ magnitude: 0.1,
1374
+ reasoning: `[${curr}] Price coefficient of variation ${coeffOfVariation.toFixed(2)} with velocity ${velocity.toFixed(1)}. All items priced similarly in an active economy \u2014 no natural num\xE9raire emerging. If a designated currency exists, increase its sink demand to differentiate it.`
1375
+ },
1376
+ confidence: 0.5,
1377
+ estimatedLag: 20
1378
+ };
1379
+ }
1209
1380
  }
1210
1381
  return { violated: false };
1211
1382
  }
@@ -1252,25 +1423,22 @@ var P18_FirstProducerNeedsStartingInventory = {
1252
1423
  description: "A producer with 0 resources and 0 currency must sell nothing to get currency before they can buy raw materials. This creates a chicken-and-egg freeze. Starting inventory (2 goods + 4 raw materials + 40 currency) breaks the deadlock.",
1253
1424
  check(metrics, _thresholds) {
1254
1425
  if (metrics.tick > 20) return { violated: false };
1426
+ const hasAgents = metrics.totalAgents > 0;
1255
1427
  for (const [resource, supply] of Object.entries(metrics.supplyByResource)) {
1256
- if (supply === 0) {
1257
- for (const [role, population] of Object.entries(metrics.populationByRole)) {
1258
- if (population > 0) {
1259
- return {
1260
- violated: true,
1261
- severity: 8,
1262
- evidence: { tick: metrics.tick, resource, supply, role, population },
1263
- suggestedAction: {
1264
- parameter: "productionCost",
1265
- direction: "decrease",
1266
- magnitude: 0.5,
1267
- reasoning: `Bootstrap failure: ${role} exists but ${resource} supply is 0 on tick 1-20. Drastically reduce production cost to allow immediate output.`
1268
- },
1269
- confidence: 0.9,
1270
- estimatedLag: 2
1271
- };
1272
- }
1273
- }
1428
+ if (supply === 0 && hasAgents) {
1429
+ return {
1430
+ violated: true,
1431
+ severity: 8,
1432
+ evidence: { tick: metrics.tick, resource, supply, totalAgents: metrics.totalAgents },
1433
+ suggestedAction: {
1434
+ parameter: "productionCost",
1435
+ direction: "decrease",
1436
+ magnitude: 0.5,
1437
+ reasoning: `Bootstrap failure: ${resource} supply is 0 at tick ${metrics.tick} with ${metrics.totalAgents} agents. Drastically reduce production cost to allow immediate output.`
1438
+ },
1439
+ confidence: 0.9,
1440
+ estimatedLag: 2
1441
+ };
1274
1442
  }
1275
1443
  }
1276
1444
  return { violated: false };
@@ -1896,26 +2064,33 @@ var P42_TheMedianPrinciple = {
1896
2064
  category: "statistical",
1897
2065
  description: "When (mean - median) / median > 0.3, mean is a lie. A few high-balance agents raise the mean while most agents have low balances. Always balance to median when divergence exceeds 30%.",
1898
2066
  check(metrics, thresholds) {
1899
- const { meanMedianDivergence, giniCoefficient } = metrics;
1900
- if (meanMedianDivergence > thresholds.meanMedianDivergenceMax) {
1901
- return {
1902
- violated: true,
1903
- severity: 5,
1904
- evidence: {
1905
- meanMedianDivergence,
1906
- giniCoefficient,
1907
- meanBalance: metrics.meanBalance,
1908
- medianBalance: metrics.medianBalance
1909
- },
1910
- suggestedAction: {
1911
- parameter: "transactionFee",
1912
- direction: "increase",
1913
- magnitude: 0.15,
1914
- reasoning: `Mean/median divergence ${(meanMedianDivergence * 100).toFixed(0)}% (threshold: ${(thresholds.meanMedianDivergenceMax * 100).toFixed(0)}%). Economy has outliers skewing metrics. Use median for decisions. Raise auction fees to redistribute wealth.`
1915
- },
1916
- confidence: 0.85,
1917
- estimatedLag: 15
1918
- };
2067
+ for (const curr of metrics.currencies) {
2068
+ const meanMedianDivergence = metrics.meanMedianDivergenceByCurrency[curr] ?? 0;
2069
+ const giniCoefficient = metrics.giniCoefficientByCurrency[curr] ?? 0;
2070
+ const meanBalance = metrics.meanBalanceByCurrency[curr] ?? 0;
2071
+ const medianBalance = metrics.medianBalanceByCurrency[curr] ?? 0;
2072
+ if (meanMedianDivergence > thresholds.meanMedianDivergenceMax) {
2073
+ return {
2074
+ violated: true,
2075
+ severity: 5,
2076
+ evidence: {
2077
+ currency: curr,
2078
+ meanMedianDivergence,
2079
+ giniCoefficient,
2080
+ meanBalance,
2081
+ medianBalance
2082
+ },
2083
+ suggestedAction: {
2084
+ parameter: "transactionFee",
2085
+ direction: "increase",
2086
+ currency: curr,
2087
+ magnitude: 0.15,
2088
+ reasoning: `[${curr}] Mean/median divergence ${(meanMedianDivergence * 100).toFixed(0)}% (threshold: ${(thresholds.meanMedianDivergenceMax * 100).toFixed(0)}%). Economy has outliers skewing metrics. Use median for decisions. Raise transaction fees to redistribute wealth.`
2089
+ },
2090
+ confidence: 0.85,
2091
+ estimatedLag: 15
2092
+ };
2093
+ }
1919
2094
  }
1920
2095
  return { violated: false };
1921
2096
  }
@@ -2123,51 +2298,56 @@ var P33_FairNotEqual = {
2123
2298
  category: "player_experience",
2124
2299
  description: "Gini = 0 is boring \u2014 everyone has the same and there is nothing to strive for. Healthy inequality from skill/effort is fine. Inequality from money (pay-to-win) is toxic. Target Gini 0.3-0.45: meaningful spread, not oligarchy.",
2125
2300
  check(metrics, thresholds) {
2126
- const { giniCoefficient } = metrics;
2127
- if (giniCoefficient < 0.1) {
2128
- return {
2129
- violated: true,
2130
- severity: 3,
2131
- evidence: { giniCoefficient },
2132
- suggestedAction: {
2133
- parameter: "rewardRate",
2134
- direction: "increase",
2135
- magnitude: 0.1,
2136
- reasoning: `Gini ${giniCoefficient.toFixed(2)} \u2014 near-perfect equality. Economy lacks stakes. Increase winner rewards to create meaningful spread.`
2137
- },
2138
- confidence: 0.6,
2139
- estimatedLag: 20
2140
- };
2141
- }
2142
- if (giniCoefficient > thresholds.giniRedThreshold) {
2143
- return {
2144
- violated: true,
2145
- severity: 7,
2146
- evidence: { giniCoefficient },
2147
- suggestedAction: {
2148
- parameter: "transactionFee",
2149
- direction: "increase",
2150
- magnitude: 0.2,
2151
- reasoning: `Gini ${giniCoefficient.toFixed(2)} \u2014 oligarchy level. Toxic inequality. Raise transaction fees to redistribute wealth from rich to pool.`
2152
- },
2153
- confidence: 0.85,
2154
- estimatedLag: 10
2155
- };
2156
- }
2157
- if (giniCoefficient > thresholds.giniWarnThreshold) {
2158
- return {
2159
- violated: true,
2160
- severity: 4,
2161
- evidence: { giniCoefficient },
2162
- suggestedAction: {
2163
- parameter: "transactionFee",
2164
- direction: "increase",
2165
- magnitude: 0.1,
2166
- reasoning: `Gini ${giniCoefficient.toFixed(2)} \u2014 high inequality warning. Gently raise fees to slow wealth concentration.`
2167
- },
2168
- confidence: 0.75,
2169
- estimatedLag: 15
2170
- };
2301
+ for (const curr of metrics.currencies) {
2302
+ const giniCoefficient = metrics.giniCoefficientByCurrency[curr] ?? 0;
2303
+ if (giniCoefficient < 0.1) {
2304
+ return {
2305
+ violated: true,
2306
+ severity: 3,
2307
+ evidence: { currency: curr, giniCoefficient },
2308
+ suggestedAction: {
2309
+ parameter: "rewardRate",
2310
+ direction: "increase",
2311
+ currency: curr,
2312
+ magnitude: 0.1,
2313
+ reasoning: `[${curr}] Gini ${giniCoefficient.toFixed(2)} \u2014 near-perfect equality. Economy lacks stakes. Increase winner rewards to create meaningful spread.`
2314
+ },
2315
+ confidence: 0.6,
2316
+ estimatedLag: 20
2317
+ };
2318
+ }
2319
+ if (giniCoefficient > thresholds.giniRedThreshold) {
2320
+ return {
2321
+ violated: true,
2322
+ severity: 7,
2323
+ evidence: { currency: curr, giniCoefficient },
2324
+ suggestedAction: {
2325
+ parameter: "transactionFee",
2326
+ direction: "increase",
2327
+ currency: curr,
2328
+ magnitude: 0.2,
2329
+ reasoning: `[${curr}] Gini ${giniCoefficient.toFixed(2)} \u2014 oligarchy level. Toxic inequality. Raise transaction fees to redistribute wealth from rich to pool.`
2330
+ },
2331
+ confidence: 0.85,
2332
+ estimatedLag: 10
2333
+ };
2334
+ }
2335
+ if (giniCoefficient > thresholds.giniWarnThreshold) {
2336
+ return {
2337
+ violated: true,
2338
+ severity: 4,
2339
+ evidence: { currency: curr, giniCoefficient },
2340
+ suggestedAction: {
2341
+ parameter: "transactionFee",
2342
+ direction: "increase",
2343
+ currency: curr,
2344
+ magnitude: 0.1,
2345
+ reasoning: `[${curr}] Gini ${giniCoefficient.toFixed(2)} \u2014 high inequality warning. Gently raise fees to slow wealth concentration.`
2346
+ },
2347
+ confidence: 0.75,
2348
+ estimatedLag: 15
2349
+ };
2350
+ }
2171
2351
  }
2172
2352
  return { violated: false };
2173
2353
  }
@@ -2700,33 +2880,41 @@ var Simulator = class {
2700
2880
  runForward(metrics, action, ticks, _thresholds) {
2701
2881
  const multiplier = this.actionMultiplier(action);
2702
2882
  const noise = () => 1 + (Math.random() - 0.5) * 0.1;
2703
- let supply = metrics.totalSupply;
2883
+ const currencies = metrics.currencies;
2884
+ const targetCurrency = action.currency;
2885
+ const supplies = { ...metrics.totalSupplyByCurrency };
2886
+ const netFlows = { ...metrics.netFlowByCurrency };
2887
+ const ginis = { ...metrics.giniCoefficientByCurrency };
2888
+ const velocities = { ...metrics.velocityByCurrency };
2704
2889
  let satisfaction = metrics.avgSatisfaction;
2705
- let gini = metrics.giniCoefficient;
2706
- let velocity = metrics.velocity;
2707
- let netFlow = metrics.netFlow;
2708
- const churnRate = metrics.churnRate;
2709
2890
  for (let t = 0; t < ticks; t++) {
2710
- const effectOnFlow = this.flowEffect(action, metrics) * multiplier * noise();
2711
- netFlow = netFlow * 0.9 + effectOnFlow * 0.1;
2712
- supply += netFlow * noise();
2713
- supply = Math.max(0, supply);
2714
- const satDelta = netFlow > 0 && netFlow < 20 ? 0.5 : netFlow < 0 ? -1 : 0;
2891
+ for (const curr of currencies) {
2892
+ const isTarget = !targetCurrency || targetCurrency === curr;
2893
+ const effectOnFlow = isTarget ? this.flowEffect(action, metrics, curr) * multiplier * noise() : 0;
2894
+ netFlows[curr] = (netFlows[curr] ?? 0) * 0.9 + effectOnFlow * 0.1;
2895
+ supplies[curr] = Math.max(0, (supplies[curr] ?? 0) + (netFlows[curr] ?? 0) * noise());
2896
+ ginis[curr] = (ginis[curr] ?? 0) * 0.99 + 0.35 * 0.01 * noise();
2897
+ velocities[curr] = (supplies[curr] ?? 0) / Math.max(1, metrics.totalAgents) * 0.01 * noise();
2898
+ }
2899
+ const avgNetFlow = currencies.length > 0 ? Object.values(netFlows).reduce((s, v) => s + v, 0) / currencies.length : 0;
2900
+ const satDelta = avgNetFlow > 0 && avgNetFlow < 20 ? 0.5 : avgNetFlow < 0 ? -1 : 0;
2715
2901
  satisfaction = Math.min(100, Math.max(0, satisfaction + satDelta * noise()));
2716
- gini = gini * 0.99 + 0.35 * 0.01 * noise();
2717
- velocity = supply / Math.max(1, metrics.totalAgents) * 0.01 * noise();
2718
- const agentLoss = metrics.totalAgents * churnRate * noise();
2719
- void agentLoss;
2720
2902
  }
2903
+ const totalSupply = Object.values(supplies).reduce((s, v) => s + v, 0);
2721
2904
  const projected = {
2722
2905
  ...metrics,
2723
2906
  tick: metrics.tick + ticks,
2724
- totalSupply: supply,
2725
- netFlow,
2726
- velocity,
2727
- giniCoefficient: Math.max(0, Math.min(1, gini)),
2907
+ currencies,
2908
+ totalSupplyByCurrency: supplies,
2909
+ netFlowByCurrency: netFlows,
2910
+ velocityByCurrency: velocities,
2911
+ giniCoefficientByCurrency: ginis,
2912
+ totalSupply,
2913
+ netFlow: Object.values(netFlows).reduce((s, v) => s + v, 0),
2914
+ velocity: totalSupply > 0 && currencies.length > 0 ? Object.values(velocities).reduce((s, v) => s + v, 0) / currencies.length : 0,
2915
+ giniCoefficient: currencies.length > 0 ? Object.values(ginis).reduce((s, v) => s + v, 0) / currencies.length : 0,
2728
2916
  avgSatisfaction: satisfaction,
2729
- inflationRate: metrics.totalSupply > 0 ? (supply - metrics.totalSupply) / metrics.totalSupply : 0
2917
+ inflationRate: metrics.totalSupply > 0 ? (totalSupply - metrics.totalSupply) / metrics.totalSupply : 0
2730
2918
  };
2731
2919
  return projected;
2732
2920
  }
@@ -2734,16 +2922,16 @@ var Simulator = class {
2734
2922
  const base = action.magnitude ?? 0.1;
2735
2923
  return action.direction === "increase" ? 1 + base : 1 - base;
2736
2924
  }
2737
- flowEffect(action, metrics) {
2925
+ flowEffect(action, metrics, currency) {
2738
2926
  const { parameter, direction } = action;
2739
2927
  const sign = direction === "increase" ? -1 : 1;
2740
2928
  const roleEntries = Object.entries(metrics.populationByRole).sort((a, b) => b[1] - a[1]);
2741
2929
  const dominantRoleCount = roleEntries[0]?.[1] ?? 0;
2742
2930
  if (parameter === "productionCost") {
2743
- return sign * metrics.netFlow * 0.2;
2931
+ return sign * (metrics.netFlowByCurrency[currency] ?? 0) * 0.2;
2744
2932
  }
2745
2933
  if (parameter === "transactionFee") {
2746
- return sign * metrics.velocity * 10 * 0.1;
2934
+ return sign * (metrics.velocityByCurrency[currency] ?? 0) * 10 * 0.1;
2747
2935
  }
2748
2936
  if (parameter === "entryFee") {
2749
2937
  return sign * dominantRoleCount * 0.5;
@@ -2752,14 +2940,22 @@ var Simulator = class {
2752
2940
  return -sign * dominantRoleCount * 0.3;
2753
2941
  }
2754
2942
  if (parameter === "yieldRate") {
2755
- return sign * metrics.faucetVolume * 0.15;
2943
+ return sign * (metrics.faucetVolumeByCurrency[currency] ?? 0) * 0.15;
2756
2944
  }
2757
- return sign * metrics.netFlow * 0.1;
2945
+ return sign * (metrics.netFlowByCurrency[currency] ?? 0) * 0.1;
2758
2946
  }
2759
2947
  checkImprovement(before, after, action) {
2760
2948
  const satisfactionImproved = after.avgSatisfaction >= before.avgSatisfaction - 2;
2761
- const flowMoreBalanced = Math.abs(after.netFlow) <= Math.abs(before.netFlow) * 1.2;
2762
- const notWorseGini = after.giniCoefficient <= before.giniCoefficient + 0.05;
2949
+ const flowMoreBalanced = before.currencies.every((curr) => {
2950
+ const afterFlow = Math.abs(after.netFlowByCurrency[curr] ?? 0);
2951
+ const beforeFlow = Math.abs(before.netFlowByCurrency[curr] ?? 0);
2952
+ return afterFlow <= beforeFlow * 1.2 || afterFlow < 1;
2953
+ });
2954
+ const notWorseGini = before.currencies.every((curr) => {
2955
+ const afterGini = after.giniCoefficientByCurrency[curr] ?? 0;
2956
+ const beforeGini = before.giniCoefficientByCurrency[curr] ?? 0;
2957
+ return afterGini <= beforeGini + 0.05;
2958
+ });
2763
2959
  void action;
2764
2960
  return satisfactionImproved && flowMoreBalanced && notWorseGini;
2765
2961
  }
@@ -2770,6 +2966,21 @@ var Simulator = class {
2770
2966
  const vals = outcomes.map((o) => o[key]).filter((v) => typeof v === "number" && !isNaN(v));
2771
2967
  return vals.length > 0 ? vals.reduce((a, b) => a + b, 0) / vals.length : 0;
2772
2968
  };
2969
+ const avgRecord = (key) => {
2970
+ const allKeys = /* @__PURE__ */ new Set();
2971
+ for (const o of outcomes) {
2972
+ const rec = o[key];
2973
+ if (rec && typeof rec === "object" && !Array.isArray(rec)) {
2974
+ Object.keys(rec).forEach((k) => allKeys.add(k));
2975
+ }
2976
+ }
2977
+ const result = {};
2978
+ for (const k of allKeys) {
2979
+ const vals = outcomes.map((o) => o[key]?.[k]).filter((v) => typeof v === "number" && !isNaN(v));
2980
+ result[k] = vals.length > 0 ? vals.reduce((a, b) => a + b, 0) / vals.length : 0;
2981
+ }
2982
+ return result;
2983
+ };
2773
2984
  return {
2774
2985
  ...base,
2775
2986
  totalSupply: avg("totalSupply"),
@@ -2777,7 +2988,11 @@ var Simulator = class {
2777
2988
  velocity: avg("velocity"),
2778
2989
  giniCoefficient: avg("giniCoefficient"),
2779
2990
  avgSatisfaction: avg("avgSatisfaction"),
2780
- inflationRate: avg("inflationRate")
2991
+ inflationRate: avg("inflationRate"),
2992
+ totalSupplyByCurrency: avgRecord("totalSupplyByCurrency"),
2993
+ netFlowByCurrency: avgRecord("netFlowByCurrency"),
2994
+ velocityByCurrency: avgRecord("velocityByCurrency"),
2995
+ giniCoefficientByCurrency: avgRecord("giniCoefficientByCurrency")
2781
2996
  };
2782
2997
  }
2783
2998
  };
@@ -2836,6 +3051,7 @@ var Planner = class {
2836
3051
  id: `plan_${metrics.tick}_${param}`,
2837
3052
  diagnosis,
2838
3053
  parameter: param,
3054
+ ...action.currency !== void 0 ? { currency: action.currency } : {},
2839
3055
  currentValue,
2840
3056
  targetValue,
2841
3057
  maxChangePercent: thresholds.maxAdjustmentPercent,
@@ -2877,7 +3093,7 @@ var Executor = class {
2877
3093
  }
2878
3094
  async apply(plan, adapter, currentParams) {
2879
3095
  const originalValue = currentParams[plan.parameter] ?? plan.currentValue;
2880
- await adapter.setParam(plan.parameter, plan.targetValue);
3096
+ await adapter.setParam(plan.parameter, plan.targetValue, plan.diagnosis.violation.suggestedAction.currency);
2881
3097
  plan.appliedAt = plan.diagnosis.tick;
2882
3098
  this.activePlans.push({ plan, originalValue });
2883
3099
  }
@@ -2899,7 +3115,7 @@ var Executor = class {
2899
3115
  const metricValue = this.getMetricValue(metrics, rc.metric);
2900
3116
  const shouldRollback = rc.direction === "below" ? metricValue < rc.threshold : metricValue > rc.threshold;
2901
3117
  if (shouldRollback) {
2902
- await adapter.setParam(plan.parameter, originalValue);
3118
+ await adapter.setParam(plan.parameter, originalValue, plan.diagnosis.violation.suggestedAction.currency);
2903
3119
  rolledBack.push(plan);
2904
3120
  } else {
2905
3121
  const settledTick = rc.checkAfterTick + 10;
@@ -3032,6 +3248,18 @@ var DecisionLog = class {
3032
3248
  };
3033
3249
 
3034
3250
  // src/MetricStore.ts
3251
+ function getNestedValue(obj, path) {
3252
+ const parts = path.split(".");
3253
+ let val = obj;
3254
+ for (const part of parts) {
3255
+ if (val !== null && typeof val === "object") {
3256
+ val = val[part];
3257
+ } else {
3258
+ return NaN;
3259
+ }
3260
+ }
3261
+ return typeof val === "number" ? val : NaN;
3262
+ }
3035
3263
  var RingBuffer = class {
3036
3264
  constructor(capacity) {
3037
3265
  this.capacity = capacity;
@@ -3109,10 +3337,9 @@ var MetricStore = class {
3109
3337
  if (q.to !== void 0 && m.tick > q.to) return false;
3110
3338
  return true;
3111
3339
  });
3112
- const metricKey = q.metric;
3113
3340
  const points = filtered.map((m) => ({
3114
3341
  tick: m.tick,
3115
- value: typeof m[metricKey] === "number" ? m[metricKey] : NaN
3342
+ value: getNestedValue(m, q.metric)
3116
3343
  }));
3117
3344
  return { metric: q.metric, resolution, points };
3118
3345
  }
@@ -3137,6 +3364,21 @@ var MetricStore = class {
3137
3364
  const vals = snapshots.map((s) => s[key]).filter((v) => !isNaN(v));
3138
3365
  return vals.length > 0 ? vals.reduce((a, b) => a + b, 0) / vals.length : 0;
3139
3366
  };
3367
+ const avgRecord = (key) => {
3368
+ const allKeys = /* @__PURE__ */ new Set();
3369
+ for (const s of snapshots) {
3370
+ const rec = s[key];
3371
+ if (rec && typeof rec === "object" && !Array.isArray(rec)) {
3372
+ Object.keys(rec).forEach((k) => allKeys.add(k));
3373
+ }
3374
+ }
3375
+ const result = {};
3376
+ for (const k of allKeys) {
3377
+ const vals = snapshots.map((s) => s[key]?.[k]).filter((v) => typeof v === "number" && !isNaN(v));
3378
+ result[k] = vals.length > 0 ? vals.reduce((a, b) => a + b, 0) / vals.length : 0;
3379
+ }
3380
+ return result;
3381
+ };
3140
3382
  return {
3141
3383
  ...last,
3142
3384
  totalSupply: avg("totalSupply"),
@@ -3156,7 +3398,21 @@ var MetricStore = class {
3156
3398
  tapSinkRatio: avg("tapSinkRatio"),
3157
3399
  productionIndex: avg("productionIndex"),
3158
3400
  capacityUsage: avg("capacityUsage"),
3159
- anchorRatioDrift: avg("anchorRatioDrift")
3401
+ anchorRatioDrift: avg("anchorRatioDrift"),
3402
+ // Per-currency averages
3403
+ totalSupplyByCurrency: avgRecord("totalSupplyByCurrency"),
3404
+ netFlowByCurrency: avgRecord("netFlowByCurrency"),
3405
+ velocityByCurrency: avgRecord("velocityByCurrency"),
3406
+ inflationRateByCurrency: avgRecord("inflationRateByCurrency"),
3407
+ faucetVolumeByCurrency: avgRecord("faucetVolumeByCurrency"),
3408
+ sinkVolumeByCurrency: avgRecord("sinkVolumeByCurrency"),
3409
+ tapSinkRatioByCurrency: avgRecord("tapSinkRatioByCurrency"),
3410
+ anchorRatioDriftByCurrency: avgRecord("anchorRatioDriftByCurrency"),
3411
+ giniCoefficientByCurrency: avgRecord("giniCoefficientByCurrency"),
3412
+ medianBalanceByCurrency: avgRecord("medianBalanceByCurrency"),
3413
+ meanBalanceByCurrency: avgRecord("meanBalanceByCurrency"),
3414
+ top10PctShareByCurrency: avgRecord("top10PctShareByCurrency"),
3415
+ meanMedianDivergenceByCurrency: avgRecord("meanMedianDivergenceByCurrency")
3160
3416
  };
3161
3417
  }
3162
3418
  };