@ak--47/dungeon-master 1.2.3 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +51 -13
  3. package/dungeons/technical/ad-spend.js +2 -2
  4. package/dungeons/technical/anonymous-users.js +2 -2
  5. package/dungeons/technical/array-of-object-lookup.js +2 -4
  6. package/dungeons/technical/experiments.js +2 -2
  7. package/dungeons/technical/foobar.js +2 -2
  8. package/dungeons/technical/group-analytics.js +2 -2
  9. package/dungeons/technical/mirror-strategies.js +2 -2
  10. package/dungeons/technical/nested-objects.js +2 -2
  11. package/dungeons/technical/retention-cadence.js +2 -3
  12. package/dungeons/technical/sanity.js +2 -2
  13. package/dungeons/technical/scale-test.js +2 -2
  14. package/dungeons/technical/scd.js +2 -2
  15. package/dungeons/technical/simple.js +5 -6
  16. package/dungeons/technical/simplest-schema.json +5 -0
  17. package/dungeons/technical/simplest.js +2 -2
  18. package/dungeons/technical/text-generation.js +3 -3
  19. package/dungeons/vertical/ai-platform.js +858 -0
  20. package/dungeons/vertical/community.js +84 -40
  21. package/dungeons/vertical/crypto.js +830 -0
  22. package/dungeons/vertical/dating.js +744 -0
  23. package/dungeons/vertical/devtools.js +175 -69
  24. package/dungeons/vertical/ecommerce.js +242 -94
  25. package/dungeons/vertical/education.js +330 -313
  26. package/dungeons/vertical/fintech.js +442 -313
  27. package/dungeons/vertical/fitness.js +143 -61
  28. package/dungeons/vertical/food-delivery.js +327 -353
  29. package/dungeons/vertical/gaming.js +912 -382
  30. package/dungeons/vertical/healthcare.js +142 -63
  31. package/dungeons/vertical/insurance-application.js +170 -76
  32. package/dungeons/vertical/logistics.js +115 -20
  33. package/dungeons/vertical/marketplace.js +152 -58
  34. package/dungeons/vertical/media.js +248 -384
  35. package/dungeons/vertical/real-estate.js +781 -0
  36. package/dungeons/vertical/sass.js +255 -266
  37. package/dungeons/vertical/social.js +264 -206
  38. package/dungeons/vertical/travel.js +117 -41
  39. package/index.js +17 -17
  40. package/lib/core/config-validator.js +159 -31
  41. package/lib/core/context.js +10 -24
  42. package/lib/core/storage.js +6 -1
  43. package/lib/generators/events.js +11 -14
  44. package/lib/generators/funnels.js +12 -4
  45. package/lib/generators/mirror.js +3 -2
  46. package/lib/generators/product-names.js +1 -1
  47. package/lib/generators/scd.js +2 -1
  48. package/lib/generators/text.js +1 -1
  49. package/lib/orchestrators/user-loop.js +81 -47
  50. package/lib/templates/macro-presets.js +111 -0
  51. package/lib/templates/soup-presets.js +19 -36
  52. package/lib/utils/utils.js +71 -39
  53. package/package.json +8 -2
  54. package/scripts/smoke-test-all.mjs +162 -0
  55. package/scripts/verify-runner.mjs +72 -24
  56. package/types.d.ts +251 -51
  57. package/dungeons/technical/ad-spend-schema.json +0 -128
  58. package/dungeons/technical/anonymous-users-schema.json +0 -92
  59. package/dungeons/technical/array-of-object-lookup-schema.json +0 -191
  60. package/dungeons/technical/experiments-schema.json +0 -203
  61. package/dungeons/technical/foobar-schema.json +0 -362
  62. package/dungeons/technical/group-analytics-schema.json +0 -241
  63. package/dungeons/technical/mirror-strategies-schema.json +0 -84
  64. package/dungeons/technical/nested-objects-schema.json +0 -145
  65. package/dungeons/technical/retention-cadence-schema.json +0 -37
  66. package/dungeons/technical/sanity-schema.json +0 -185
  67. package/dungeons/technical/scale-test-schema.json +0 -70
  68. package/dungeons/technical/scd-schema.json +0 -467
  69. package/dungeons/technical/simple-schema.json +0 -362
  70. package/dungeons/technical/text-generation-schema.json +0 -1062
  71. package/dungeons/user/.gitkeep +0 -0
  72. package/dungeons/vertical/community-schema.json +0 -579
  73. package/dungeons/vertical/devtools-schema.json +0 -601
  74. package/dungeons/vertical/ecommerce-schema.json +0 -604
  75. package/dungeons/vertical/education-schema.json +0 -5686
  76. package/dungeons/vertical/fintech-schema.json +0 -630
  77. package/dungeons/vertical/fitness-schema.json +0 -530
  78. package/dungeons/vertical/food-delivery-schema.json +0 -36728
  79. package/dungeons/vertical/gaming-schema.json +0 -438
  80. package/dungeons/vertical/healthcare-schema.json +0 -549
  81. package/dungeons/vertical/insurance-application-schema.json +0 -485
  82. package/dungeons/vertical/logistics-schema.json +0 -574
  83. package/dungeons/vertical/marketplace-schema.json +0 -533
  84. package/dungeons/vertical/media-schema.json +0 -4749
  85. package/dungeons/vertical/rpg-schema.json +0 -2491
  86. package/dungeons/vertical/rpg.js +0 -976
  87. package/dungeons/vertical/sass-schema.json +0 -3128
  88. package/dungeons/vertical/social-schema.json +0 -620
  89. package/dungeons/vertical/travel-schema.json +0 -580
@@ -0,0 +1,830 @@
1
+ // ── TWEAK THESE ──
2
+ const SEED = "coinnest";
3
+ const num_days = 120;
4
+ const num_users = 6_000;
5
+ const avg_events_per_user_per_day = 0.83;
6
+ let token = "your-mixpanel-token";
7
+
8
+ // ── env overrides ──
9
+ if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
10
+
11
+ import dayjs from "dayjs";
12
+ import utc from "dayjs/plugin/utc.js";
13
+ import "dotenv/config";
14
+ import * as u from "../../lib/utils/utils.js";
15
+ import * as v from "ak-tools";
16
+
17
+ dayjs.extend(utc);
18
+ const chance = u.initChance(SEED);
19
+
20
+ /** @typedef {import("../../types").Dungeon} Config */
21
+
22
+ /**
23
+ * ===================================================================
24
+ * DATASET OVERVIEW
25
+ * ===================================================================
26
+ *
27
+ * CoinNest — a DeFi cryptocurrency exchange platform. Users connect
28
+ * wallets, swap tokens, stake for yield, mint NFTs, claim airdrops,
29
+ * and trade. Features KYC verification, pro trading tiers, and
30
+ * portfolio tracking.
31
+ *
32
+ * Scale: 6,000 users · 600K events · 120 days · 16 event types
33
+ *
34
+ * Core loop: wallet connected → KYC → deposit → swap → stake/unstake
35
+ * → portfolio viewed → withdrawal
36
+ *
37
+ * Funnels:
38
+ * - Onboarding: wallet connected → kyc started → deposit (60%)
39
+ * - Trading: deposit → swap → withdrawal (70%)
40
+ * - DeFi: swap → stake → claim airdrop (35%)
41
+ *
42
+ * Tiers: Standard (75%) / Pro (25%)
43
+ * Chains: Ethereum, Solana, Base, Arbitrum, Polygon
44
+ * Wallets: MetaMask, Phantom, Coinbase Wallet, Rainbow, WalletConnect
45
+ */
46
+
47
+ /**
48
+ * ===================================================================
49
+ * ANALYTICS HOOKS (10 hooks)
50
+ * ===================================================================
51
+ *
52
+ * NOTE: All cohort effects are HIDDEN — no flag stamping. Discoverable
53
+ * via behavioral cohorts (count event per user, observe trade_amount
54
+ * distribution) or raw-prop breakdowns (token_pair, day, trading_tier).
55
+ *
56
+ * 1. WHALE WALLETS (everything)
57
+ *
58
+ * PATTERN: 2% of wallets (deterministic via id hash) get swap
59
+ * trade_amount_usd boosted 50x. No flag — analyst sees long-tail
60
+ * trade-amount distribution.
61
+ *
62
+ * HOW TO FIND IT IN MIXPANEL:
63
+ *
64
+ * Report 1: Trade Amount Distribution
65
+ * - Report type: Insights
66
+ * - Event: "swap"
67
+ * - Measure: Distribution of "trade_amount_usd"
68
+ * - Expected: heavy long tail; top 2% of users dominate volume
69
+ *
70
+ * Report 2: Volume Share by Top Traders Cohort
71
+ * - Cohort A: top 5% by total swap volume per user
72
+ * - Event: "swap"
73
+ * - Measure: Total of "trade_amount_usd"
74
+ * - Expected: cohort A drives ~ 60%+ of total volume
75
+ *
76
+ * REAL-WORLD ANALOGUE: Whale wallets dominate exchange volume.
77
+ *
78
+ * ---------------------------------------------------------------
79
+ * 2. GAS PRICE SPIKE (event + everything)
80
+ *
81
+ * PATTERN: Days 35-37, gas_fee_usd 10x on swap + transfer events.
82
+ * In window, 40% of swaps get swap_status flipped to "failed". No flag.
83
+ *
84
+ * HOW TO FIND IT IN MIXPANEL:
85
+ *
86
+ * Report 1: Avg Gas Fee Over Time
87
+ * - Report type: Insights
88
+ * - Event: "swap"
89
+ * - Measure: Average of "gas_fee_usd"
90
+ * - Line chart by day
91
+ * - Expected: spike days 35-37 (~10x baseline)
92
+ *
93
+ * Report 2: Swap Failure Rate Over Time
94
+ * - Report type: Insights
95
+ * - Event: "swap"
96
+ * - Measure: Total
97
+ * - Filter: swap_status = "failed"
98
+ * - Line chart by day
99
+ * - Expected: failure rate spikes during gas window
100
+ *
101
+ * REAL-WORLD ANALOGUE: Network congestion breaks transactions.
102
+ *
103
+ * ---------------------------------------------------------------
104
+ * 3. TOKEN LAUNCH SURGE (event + everything)
105
+ *
106
+ * PATTERN: Day 50+, 25% of swaps get token_pair flipped to a MOON
107
+ * pair. Each MOON swap clones 4 extra swap events with unique offset.
108
+ * No flag — discover via token_pair breakdown over time.
109
+ *
110
+ * HOW TO FIND IT IN MIXPANEL:
111
+ *
112
+ * Report 1: Swap Volume by Token Pair Over Time
113
+ * - Report type: Insights
114
+ * - Event: "swap"
115
+ * - Measure: Total
116
+ * - Filter: token_pair contains "MOON"
117
+ * - Line chart by day
118
+ * - Expected: appears at day 50, surges thereafter
119
+ *
120
+ * REAL-WORLD ANALOGUE: Meme/altcoin listings drive frenzied volume.
121
+ *
122
+ * ---------------------------------------------------------------
123
+ * 4. AIRDROP HUNTER CHURN (everything)
124
+ *
125
+ * PATTERN: 4% of users (deterministic via charCodeAt(1) % 25 === 0)
126
+ * are airdrop-farming bots. After their first "claim airdrop" event,
127
+ * 95% of subsequent events are removed.
128
+ *
129
+ * HOW TO FIND IT IN MIXPANEL:
130
+ *
131
+ * Report 1: Retention by Airdrop Claim
132
+ * - Report type: Retention
133
+ * - Event A: "claim airdrop"
134
+ * - Event B: any event
135
+ * - Expected: claimers retain dramatically worse than overall users
136
+ *
137
+ * Report 2: Airdrop Hunters vs Real Traders
138
+ * - Report type: Insights
139
+ * - Events: "claim airdrop" unique users vs "swap" unique users
140
+ * - Expected: large overlap gap — many claimers never swap again
141
+ *
142
+ * REAL-WORLD ANALOGUE: Airdrop farms are a well-known DeFi growth
143
+ * tax — bots claim free tokens and immediately abandon the platform.
144
+ *
145
+ * ---------------------------------------------------------------
146
+ * 5. KYC FUNNEL COMPLETION (everything)
147
+ *
148
+ * PATTERN: Users with kyc-completed get post-KYC deposit_amount_usd
149
+ * boosted 4x and 7 extra cloned swaps per existing post-KYC swap.
150
+ * No flag — discover via cohort builder.
151
+ *
152
+ * HOW TO FIND IT IN MIXPANEL:
153
+ *
154
+ * Report 1: Avg Deposit by KYC Cohort
155
+ * - Report type: Insights (with cohort)
156
+ * - Cohort A: users with >= 1 "kyc completed"
157
+ * - Cohort B: rest
158
+ * - Event: "deposit"
159
+ * - Measure: Average of "deposit_amount_usd"
160
+ * - Expected: A ~ 4x B
161
+ *
162
+ * REAL-WORLD ANALOGUE: KYC unlocks higher limits.
163
+ *
164
+ * ---------------------------------------------------------------
165
+ * 6. STAKE-TO-RETAIN (everything)
166
+ *
167
+ * PATTERN: Users who stake any token within first 14 days get extra
168
+ * cloned swap + portfolio events past day 60. Non-stakers lose 85% of
169
+ * post-day-60 events. No flag — discover via retention cohort.
170
+ *
171
+ * HOW TO FIND IT IN MIXPANEL:
172
+ *
173
+ * Report 1: Retention by Early Stake Cohort
174
+ * - Report type: Retention
175
+ * - Cohort A: users with >= 1 "stake" in first 14 days
176
+ * - Cohort B: rest
177
+ * - Expected: A ~ 70% D60 vs B ~ 15%
178
+ *
179
+ * REAL-WORLD ANALOGUE: Staking creates skin in the game.
180
+ *
181
+ * ---------------------------------------------------------------
182
+ * 7. PRO TIER MAKER FEES (everything)
183
+ *
184
+ * PATTERN: Pro-tier users (profile.trading_tier) get maker_fee_pct
185
+ * pinned to 0.05 (vs Standard 0.30) on swap events plus 5 extra
186
+ * cloned swaps per existing. Mutates raw prop. Discover via
187
+ * trading_tier breakdown on maker_fee_pct.
188
+ *
189
+ * HOW TO FIND IT IN MIXPANEL:
190
+ *
191
+ * Report 1: Avg Maker Fee by Tier
192
+ * - Report type: Insights
193
+ * - Event: "swap"
194
+ * - Measure: Average of "maker_fee_pct"
195
+ * - Breakdown: "trading_tier"
196
+ * - Expected: Pro ~ 0.05, Standard ~ 0.30
197
+ *
198
+ * REAL-WORLD ANALOGUE: Tiered fees retain volume traders.
199
+ *
200
+ * ---------------------------------------------------------------
201
+ * 8. RUG-PULL AFTERMATH (everything)
202
+ *
203
+ * PATTERN: ~8% of pre-day-70 swaps get token_pair flipped to a SCAM
204
+ * pair. Users who held SCAM lose 80% of post-day-70 events. No flag.
205
+ *
206
+ * HOW TO FIND IT IN MIXPANEL:
207
+ *
208
+ * Report 1: Retention by SCAM Holder Cohort
209
+ * - Report type: Retention
210
+ * - Cohort A: users with >= 1 swap where token_pair contains "SCAM"
211
+ * - Cohort B: rest
212
+ * - Expected: A retention drops sharply at day 70
213
+ *
214
+ * REAL-WORLD ANALOGUE: Rug-pulls collapse trust.
215
+ *
216
+ * ---------------------------------------------------------------
217
+ * 9. TRADING FUNNEL TIME-TO-CONVERT (funnel-post)
218
+ *
219
+ * PATTERN: Pro tier users complete deposit→swap→withdrawal funnel
220
+ * 1.4x faster than baseline (factor 0.7); Standard 1.3x slower
221
+ * (factor 1.3). Mutates funnel event timestamps.
222
+ *
223
+ * HOW TO FIND IT IN MIXPANEL:
224
+ *
225
+ * Report 1: Trading Funnel Median Time-to-Convert by Tier
226
+ * - Report type: Funnels
227
+ * - Steps: "deposit" -> "swap" -> "withdrawal"
228
+ * - Measure: Median time to convert
229
+ * - Breakdown: "trading_tier"
230
+ * - Expected: Pro ~ 0.7x; Standard ~ 1.3x
231
+ *
232
+ * NOTE (funnel-post measurement): visible only via Mixpanel funnel
233
+ * median TTC. Cross-event MIN→MIN SQL queries on raw events do NOT
234
+ * show this — funnel-post adjusts gaps within funnel instances, not
235
+ * across the user's full event history.
236
+ *
237
+ * REAL-WORLD ANALOGUE: Pro traders execute faster end-to-end.
238
+ *
239
+ * ---------------------------------------------------------------
240
+ * 10. SWAP-COUNT MAGIC NUMBER (everything)
241
+ *
242
+ * PATTERN: Users with 8-20 swaps in dataset get +30% on stake
243
+ * amount_usd plus 1-2 extra cloned stake events per existing.
244
+ * Users with 21+ swaps drop 40% of portfolio-viewed events
245
+ * (over-active traders ignore portfolio review). No flag.
246
+ *
247
+ * HOW TO FIND IT IN MIXPANEL:
248
+ *
249
+ * Report 1: Avg Stake Amount by Swap-Count Bucket
250
+ * - Report type: Insights (with cohort)
251
+ * - Cohort A: users with 8-20 "swap"
252
+ * - Cohort B: users with 0-7
253
+ * - Event: "stake"
254
+ * - Measure: Average of "amount_usd"
255
+ * - Expected: A ~ 1.3x B
256
+ *
257
+ * Report 2: Portfolio Views per User on Heavy Swappers
258
+ * - Report type: Insights (with cohort)
259
+ * - Cohort C: users with >= 21 "swap"
260
+ * - Cohort A: users with 8-20
261
+ * - Event: "portfolio viewed"
262
+ * - Measure: Total per user
263
+ * - Expected: C ~ 40% fewer portfolio views per user
264
+ *
265
+ * REAL-WORLD ANALOGUE: Engaged swappers grow stake; over-active
266
+ * day-traders ignore long-term portfolio review.
267
+ *
268
+ * ===================================================================
269
+ * EXPECTED METRICS SUMMARY
270
+ * ===================================================================
271
+ *
272
+ * Hook | Metric | Baseline | Effect | Ratio
273
+ * ----------------------|-----------------------|----------|---------|------
274
+ * Whale Wallets | Top 5% trade share | 5% | ~ 60% | n/a
275
+ * Gas Price Spike | gas_fee_usd days 35-7 | 1x | 10x | 10x
276
+ * Token Launch Surge | MOON pair share post-D50 | 0% | ~ 25%+ | new
277
+ * Airdrop Hunter Churn | post-airdrop events | 1x | 0.05x | -95%
278
+ * KYC Completion | deposit_amount_usd | 1x | 4x | 4x
279
+ * Stake-to-Retain | D60 retention | 15% | 70% | ~ 5x
280
+ * Pro Tier Fees | maker_fee_pct | 0.30 | 0.05 | -83%
281
+ * Rug-Pull Aftermath | victim post-D70 | 1x | 0.2x | -80%
282
+ * Trading T2C | median min by tier | 1x | 0.7x/1.3x | 1.86x range
283
+ * Swap Magic Number | sweet stake amount | 1x | 1.3x | 1.3x
284
+ * Swap Magic Number | over portfolio/user | 1x | 0.6x | -40%
285
+ */
286
+
287
+ // Token pairs used in swap events
288
+ const TOKEN_PAIRS = [
289
+ "ETH/USDC", "ETH/USDT", "BTC/USDC", "SOL/USDC",
290
+ "MATIC/ETH", "ARB/ETH", "OP/USDC", "AVAX/USDC",
291
+ "LINK/ETH", "UNI/USDC", "AAVE/ETH", "CRV/USDC",
292
+ "DOGE/USDT", "PEPE/ETH", "APE/USDC"
293
+ ];
294
+
295
+ const AIRDROP_NAMES = [
296
+ "LayerZero Season 1", "zkSync Drop", "Starknet STRK",
297
+ "Arbitrum ARB", "Optimism OP", "Blur Season 3",
298
+ "EigenLayer Points", "Celestia TIA", "Jupiter JUP",
299
+ "Wormhole W"
300
+ ];
301
+
302
+ const NFT_COLLECTIONS = [
303
+ "Bored Apes", "Azuki", "DeGods", "Pudgy Penguins",
304
+ "Milady", "Doodles", "CloneX", "Moonbirds",
305
+ "CryptoPunks", "Art Blocks"
306
+ ];
307
+
308
+ /** @type {Config} */
309
+ const config = {
310
+ token,
311
+ seed: SEED,
312
+ datasetStart: "2026-01-01T00:00:00Z",
313
+ datasetEnd: "2026-04-28T23:59:59Z",
314
+ // numDays: num_days,
315
+ avgEventsPerUserPerDay: avg_events_per_user_per_day,
316
+ numUsers: num_users,
317
+ hasAnonIds: false,
318
+ hasSessionIds: true,
319
+ format: "json",
320
+ gzip: true,
321
+ alsoInferFunnels: false,
322
+ hasLocation: true,
323
+ hasAndroidDevices: false,
324
+ hasIOSDevices: true,
325
+ hasDesktopDevices: true,
326
+ hasBrowser: true,
327
+ hasCampaigns: false,
328
+ isAnonymous: false,
329
+ hasAdSpend: false,
330
+ hasAvatar: true,
331
+
332
+ concurrency: 1,
333
+ writeToDisk: false,
334
+
335
+ soup: "growth",
336
+
337
+ scdProps: {
338
+ trading_tier: {
339
+ values: ["Standard", "Pro"],
340
+ frequency: "month",
341
+ timing: "fuzzy",
342
+ max: 4
343
+ }
344
+ },
345
+
346
+ funnels: [
347
+ {
348
+ sequence: ["wallet connected", "kyc started", "deposit"],
349
+ isFirstFunnel: true,
350
+ conversionRate: 60,
351
+ timeToConvert: 1,
352
+ },
353
+ {
354
+ sequence: ["deposit", "swap", "withdrawal"],
355
+ conversionRate: 70,
356
+ timeToConvert: 2,
357
+ weight: 5,
358
+ },
359
+ {
360
+ sequence: ["swap", "stake", "claim airdrop"],
361
+ conversionRate: 35,
362
+ timeToConvert: 7,
363
+ weight: 3,
364
+ },
365
+ ],
366
+
367
+ events: [
368
+ {
369
+ event: "wallet connected",
370
+ weight: 1,
371
+ isFirstEvent: true,
372
+ properties: {
373
+ wallet_type: ["MetaMask", "Phantom", "Coinbase Wallet", "Rainbow", "WalletConnect"],
374
+ chain: ["Ethereum", "Solana", "Base", "Arbitrum", "Polygon"],
375
+ }
376
+ },
377
+ {
378
+ event: "kyc started",
379
+ weight: 2,
380
+ properties: {
381
+ document_type: ["passport", "drivers_license", "national_id"],
382
+ verification_method: ["selfie", "video", "document_scan"],
383
+ }
384
+ },
385
+ {
386
+ event: "kyc completed",
387
+ weight: 1,
388
+ properties: {
389
+ document_type: ["passport", "drivers_license", "national_id"],
390
+ verification_method: ["selfie", "video", "document_scan"],
391
+ }
392
+ },
393
+ {
394
+ event: "deposit",
395
+ weight: 5,
396
+ properties: {
397
+ token: ["ETH", "USDC", "USDT", "SOL", "BTC"],
398
+ deposit_amount_usd: u.weighNumRange(10, 5000, 0.3, 500),
399
+ chain: ["Ethereum", "Solana", "Base", "Arbitrum", "Polygon"],
400
+ deposit_method: ["wallet_transfer", "bridge", "on_ramp", "exchange_transfer"],
401
+ }
402
+ },
403
+ {
404
+ event: "withdrawal",
405
+ weight: 3,
406
+ properties: {
407
+ token: ["ETH", "USDC", "USDT", "SOL", "BTC"],
408
+ amount_usd: u.weighNumRange(10, 5000, 0.3, 400),
409
+ chain: ["Ethereum", "Solana", "Base", "Arbitrum", "Polygon"],
410
+ withdrawal_method: ["wallet_transfer", "bridge", "off_ramp", "exchange_transfer"],
411
+ }
412
+ },
413
+ {
414
+ event: "swap",
415
+ weight: 10,
416
+ properties: {
417
+ token_pair: TOKEN_PAIRS,
418
+ trade_amount_usd: u.weighNumRange(10, 10000, 0.3, 200),
419
+ gas_fee_usd: u.weighNumRange(0.5, 30, 0.3, 5),
420
+ swap_status: ["completed", "completed", "completed", "completed", "pending", "failed"],
421
+ slippage_pct: u.weighNumRange(0.01, 5, 0.3, 0.5),
422
+ maker_fee_pct: [0.30],
423
+ }
424
+ },
425
+ {
426
+ event: "stake",
427
+ weight: 4,
428
+ properties: {
429
+ token: ["ETH", "SOL", "MATIC", "AVAX", "ATOM", "DOT"],
430
+ amount_usd: u.weighNumRange(50, 10000, 0.3, 500),
431
+ apy_pct: u.weighNumRange(2, 25, 0.5, 8),
432
+ lock_period_days: [7, 14, 30, 60, 90, 180],
433
+ }
434
+ },
435
+ {
436
+ event: "unstake",
437
+ weight: 2,
438
+ properties: {
439
+ token: ["ETH", "SOL", "MATIC", "AVAX", "ATOM", "DOT"],
440
+ amount_usd: u.weighNumRange(50, 10000, 0.3, 500),
441
+ apy_pct: u.weighNumRange(2, 25, 0.5, 8),
442
+ lock_period_days: [7, 14, 30, 60, 90, 180],
443
+ }
444
+ },
445
+ {
446
+ event: "claim airdrop",
447
+ weight: 2,
448
+ properties: {
449
+ airdrop_name: AIRDROP_NAMES,
450
+ token_amount: u.weighNumRange(10, 10000, 0.3, 500),
451
+ airdrop_value_usd: u.weighNumRange(5, 2000, 0.3, 100),
452
+ }
453
+ },
454
+ {
455
+ event: "nft mint",
456
+ weight: 2,
457
+ properties: {
458
+ collection: NFT_COLLECTIONS,
459
+ mint_price_usd: u.weighNumRange(10, 2000, 0.3, 150),
460
+ chain: ["Ethereum", "Solana", "Base", "Polygon"],
461
+ }
462
+ },
463
+ {
464
+ event: "portfolio viewed",
465
+ weight: 8,
466
+ properties: {
467
+ total_value_usd: u.weighNumRange(100, 100000, 0.3, 5000),
468
+ }
469
+ },
470
+ {
471
+ event: "price alert set",
472
+ weight: 3,
473
+ properties: {
474
+ token: ["ETH", "BTC", "SOL", "MATIC", "AVAX", "LINK", "UNI", "AAVE"],
475
+ alert_type: ["above", "below", "percent_change"],
476
+ }
477
+ },
478
+ {
479
+ event: "referral sent",
480
+ weight: 2,
481
+ properties: {
482
+ referral_method: ["link", "email", "twitter", "discord", "telegram"],
483
+ }
484
+ },
485
+ {
486
+ event: "limit order placed",
487
+ weight: 3,
488
+ properties: {
489
+ token_pair: TOKEN_PAIRS,
490
+ order_type: ["limit_buy", "limit_sell", "stop_loss", "take_profit"],
491
+ price_usd: u.weighNumRange(10, 50000, 0.3, 1000),
492
+ }
493
+ },
494
+ {
495
+ event: "transfer",
496
+ weight: 3,
497
+ properties: {
498
+ token: ["ETH", "USDC", "USDT", "SOL", "BTC"],
499
+ amount_usd: u.weighNumRange(10, 5000, 0.3, 300),
500
+ gas_fee_usd: u.weighNumRange(0.5, 30, 0.3, 5),
501
+ destination_type: ["wallet", "exchange", "contract", "ens_name"],
502
+ }
503
+ },
504
+ ],
505
+
506
+ superProps: {
507
+ trading_tier: ["Standard", "Standard", "Standard", "Pro"],
508
+ preferred_chain: ["Ethereum", "Solana", "Base", "Arbitrum", "Polygon"],
509
+ wallet_type: ["MetaMask", "Phantom", "Coinbase Wallet", "Rainbow", "WalletConnect"],
510
+ },
511
+
512
+ userProps: {
513
+ trading_tier: ["Standard", "Standard", "Standard", "Pro"],
514
+ preferred_chain: ["Ethereum", "Solana", "Base", "Arbitrum", "Polygon"],
515
+ wallet_type: ["MetaMask", "Phantom", "Coinbase Wallet", "Rainbow", "WalletConnect"],
516
+ total_trade_volume: u.weighNumRange(0, 500000, 0.3, 5000),
517
+ portfolio_value: u.weighNumRange(0, 200000, 0.3, 3000),
518
+ kyc_status: ["pending"],
519
+ },
520
+
521
+ groupKeys: [],
522
+ groupProps: {},
523
+ lookupTables: [],
524
+
525
+ hook: function (record, type, meta) {
526
+ // HOOKS 2 + 3: GAS PRICE SPIKE and TOKEN LAUNCH SURGE moved to everything
527
+ // hook below — event hook fires before bunchIntoSessions reshuffles timestamps,
528
+ // causing day-window tags to leak across the boundary.
529
+
530
+ // HOOK 9 (T2C): TRADING FUNNEL TIME-TO-CONVERT (funnel-post)
531
+ // Pro tier users complete deposit→swap→withdrawal funnel 1.4x faster
532
+ // (factor 0.7 on inter-event gaps); Standard 1.3x slower (factor 1.3).
533
+ if (type === "funnel-post") {
534
+ const segment = meta?.profile?.trading_tier;
535
+ if (Array.isArray(record) && record.length > 1) {
536
+ const factor = (
537
+ segment === "Pro" ? 0.7 :
538
+ segment === "Standard" ? 1.3 :
539
+ 1.0
540
+ );
541
+ if (factor !== 1.0) {
542
+ for (let i = 1; i < record.length; i++) {
543
+ const prev = dayjs(record[i - 1].time);
544
+ const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
545
+ record[i].time = prev.add(newGap, "milliseconds").toISOString();
546
+ }
547
+ }
548
+ }
549
+ }
550
+
551
+ if (type === "everything") {
552
+ const datasetStart = dayjs.unix(meta.datasetStart);
553
+ const userEvents = record;
554
+ const profile = meta.profile;
555
+
556
+ // Stamp superProps from profile for consistency
557
+ userEvents.forEach(e => {
558
+ e.trading_tier = profile.trading_tier;
559
+ e.preferred_chain = profile.preferred_chain;
560
+ e.wallet_type = profile.wallet_type;
561
+ });
562
+
563
+ const firstEventTime = userEvents.length > 0 ? dayjs(userEvents[0].time) : null;
564
+
565
+ // HOOK 1: WHALE WALLETS — 2% of users (charCodeAt(0) % 50 === 0)
566
+ // get swap trade_amount_usd boosted 50x. No flag.
567
+ const userId = userEvents.length > 0 ? (userEvents[0].user_id || userEvents[0].distinct_id || "") : "";
568
+ const isWhale = userId.length > 0 && userId.charCodeAt(0) % 50 === 0;
569
+
570
+ if (isWhale) {
571
+ userEvents.forEach(e => {
572
+ if (e.event === "swap") {
573
+ e.trade_amount_usd = Math.round((e.trade_amount_usd || 200) * 50);
574
+ }
575
+ });
576
+ }
577
+
578
+ // HOOK 2: GAS PRICE SPIKE — days 35-37 swap+transfer gas_fee_usd 10x,
579
+ // 40% of swaps get swap_status=failed. Runs in everything hook so
580
+ // timestamp checks see post-bunchIntoSessions times.
581
+ const day35 = datasetStart.add(35, "days");
582
+ const day38 = datasetStart.add(38, "days");
583
+ userEvents.forEach(e => {
584
+ if (e.event !== "swap" && e.event !== "transfer") return;
585
+ const t = dayjs(e.time);
586
+ if (t.isAfter(day35) && t.isBefore(day38)) {
587
+ e.gas_fee_usd = Math.round((e.gas_fee_usd || 5) * 10);
588
+ if (e.event === "swap" && chance.bool({ likelihood: 40 })) {
589
+ e.swap_status = "failed";
590
+ }
591
+ }
592
+ });
593
+
594
+ // HOOK 3: TOKEN LAUNCH SURGE — after d50, 25% of swaps flip token_pair to MOON.
595
+ const day50 = datasetStart.add(50, "days");
596
+ userEvents.forEach(e => {
597
+ if (e.event === "swap" && dayjs(e.time).isAfter(day50) && chance.bool({ likelihood: 25 })) {
598
+ e.token_pair = chance.pickone(["MOON/USDC", "MOON/ETH", "ETH/MOON"]);
599
+ }
600
+ });
601
+
602
+ // HOOK 3 (cont): TOKEN LAUNCH SURGE — clone 4 extra MOON-pair
603
+ // swap events per existing MOON swap. Cloned with unique offset.
604
+ const moonSwaps = userEvents.filter(e =>
605
+ e.event === "swap" &&
606
+ typeof e.token_pair === "string" &&
607
+ e.token_pair.includes("MOON")
608
+ );
609
+ if (moonSwaps.length > 0) {
610
+ const clonedMoonEvents = [];
611
+ moonSwaps.forEach(moonEvt => {
612
+ const moonTime = dayjs(moonEvt.time);
613
+ for (let i = 0; i < 4; i++) {
614
+ clonedMoonEvents.push({
615
+ ...moonEvt,
616
+ time: moonTime.add(chance.integer({ min: 1, max: 72 }), "hours").toISOString(),
617
+ user_id: moonEvt.user_id,
618
+ trade_amount_usd: Math.round((moonEvt.trade_amount_usd || 200) * chance.floating({ min: 0.5, max: 2.0 })),
619
+ });
620
+ }
621
+ });
622
+ userEvents.push(...clonedMoonEvents);
623
+ }
624
+
625
+ // -----------------------------------------------------------
626
+ // Hook #4: AIRDROP HUNTER CHURN
627
+ // 4% of users are deterministically tagged as airdrop-farming bots
628
+ // via charCodeAt(1) % 25 === 0. Bots that have ever claimed an airdrop
629
+ // lose 95% of events after their first claim. (Original cohort
630
+ // "claimed-but-never-swapped" was empty because every claimer also
631
+ // has organic swap events at this scale.)
632
+ // -----------------------------------------------------------
633
+ const isAirdropBot = userId.length > 1 && userId.charCodeAt(1) % 25 === 0;
634
+ const hasAirdropClaim = userEvents.some(e => e.event === "claim airdrop");
635
+
636
+ if (isAirdropBot && hasAirdropClaim) {
637
+ const firstClaim = userEvents.find(e => e.event === "claim airdrop");
638
+ if (firstClaim) {
639
+ const claimTime = dayjs(firstClaim.time);
640
+ for (let i = userEvents.length - 1; i >= 0; i--) {
641
+ const evt = userEvents[i];
642
+ if (dayjs(evt.time).isAfter(claimTime) && evt.event !== "claim airdrop") {
643
+ if (chance.bool({ likelihood: 95 })) {
644
+ userEvents.splice(i, 1);
645
+ }
646
+ }
647
+ }
648
+ }
649
+ }
650
+
651
+ // HOOK 5: KYC FUNNEL COMPLETION — users with kyc-completed
652
+ // get post-KYC deposit_amount_usd boosted 4x and 7 extra cloned
653
+ // swaps per existing post-KYC swap. No flag.
654
+ const kycCompleted = userEvents.find(e => e.event === "kyc completed");
655
+ if (kycCompleted) {
656
+ const kycTime = dayjs(kycCompleted.time);
657
+
658
+ userEvents.forEach(e => {
659
+ if (dayjs(e.time).isAfter(kycTime) && e.event === "deposit") {
660
+ e.deposit_amount_usd = Math.round((e.deposit_amount_usd || 500) * 4);
661
+ }
662
+ });
663
+
664
+ const postKycSwaps = userEvents.filter(e =>
665
+ e.event === "swap" && dayjs(e.time).isAfter(kycTime)
666
+ );
667
+ const clonedKycSwaps = [];
668
+ postKycSwaps.forEach(swapEvt => {
669
+ const swapTime = dayjs(swapEvt.time);
670
+ for (let i = 0; i < 7; i++) {
671
+ clonedKycSwaps.push({
672
+ ...swapEvt,
673
+ time: swapTime.add(chance.integer({ min: 1, max: 48 }), "hours").toISOString(),
674
+ user_id: swapEvt.user_id,
675
+ trade_amount_usd: Math.round((swapEvt.trade_amount_usd || 200) * chance.floating({ min: 0.8, max: 1.5 })),
676
+ });
677
+ }
678
+ });
679
+ userEvents.push(...clonedKycSwaps);
680
+ }
681
+
682
+ // -----------------------------------------------------------
683
+ // Hook #6: STAKE-TO-RETAIN
684
+ // Users who stake any token within the first 14 days of the
685
+ // dataset have 70% D60 retention (events injected beyond D60).
686
+ // Non-stakers lose events after day 60 to simulate 15% retention.
687
+ // -----------------------------------------------------------
688
+ const day14 = datasetStart.add(14, "days");
689
+ const day60 = datasetStart.add(60, "days");
690
+
691
+ const stakedEarly = userEvents.some(e =>
692
+ e.event === "stake" && dayjs(e.time).isBefore(day14)
693
+ );
694
+
695
+ if (stakedEarly) {
696
+ // 70% retention: inject late-stage events for stakers (no flag)
697
+ const postD60Events = userEvents.filter(e => dayjs(e.time).isAfter(day60));
698
+ if (postD60Events.length < 5) {
699
+ const swapTemplate = userEvents.find(e => e.event === "swap");
700
+ const portfolioTemplate = userEvents.find(e => e.event === "portfolio viewed");
701
+ if (swapTemplate) {
702
+ for (let i = 0; i < 8; i++) {
703
+ userEvents.push({
704
+ ...swapTemplate,
705
+ time: day60.add(chance.integer({ min: 1, max: 55 }), "days").toISOString(),
706
+ user_id: swapTemplate.user_id,
707
+ trade_amount_usd: Math.round((swapTemplate.trade_amount_usd || 200) * chance.floating({ min: 0.5, max: 2.0 })),
708
+ });
709
+ }
710
+ }
711
+ if (portfolioTemplate) {
712
+ for (let i = 0; i < 4; i++) {
713
+ userEvents.push({
714
+ ...portfolioTemplate,
715
+ time: day60.add(chance.integer({ min: 1, max: 55 }), "days").toISOString(),
716
+ user_id: portfolioTemplate.user_id,
717
+ });
718
+ }
719
+ }
720
+ }
721
+ } else {
722
+ // Non-stakers: 15% retention past D60 — remove 85% of post-D60 events
723
+ for (let i = userEvents.length - 1; i >= 0; i--) {
724
+ const evt = userEvents[i];
725
+ if (dayjs(evt.time).isAfter(day60)) {
726
+ if (chance.bool({ likelihood: 85 })) {
727
+ userEvents.splice(i, 1);
728
+ }
729
+ }
730
+ }
731
+ }
732
+
733
+ // HOOK 7: PRO TIER MAKER FEES — Pro users pay maker_fee_pct=0.05
734
+ // (vs Standard 0.30) and get 5 extra cloned swaps. Mutates raw
735
+ // maker_fee_pct prop. Reads trading_tier from profile.
736
+ if (profile.trading_tier === "Pro") {
737
+ userEvents.forEach(e => {
738
+ if (e.event === "swap") e.maker_fee_pct = 0.05;
739
+ });
740
+
741
+ const proSwaps = userEvents.filter(e => e.event === "swap");
742
+ const clonedProSwaps = [];
743
+ proSwaps.forEach(swapEvt => {
744
+ const swapTime = dayjs(swapEvt.time);
745
+ for (let i = 0; i < 5; i++) {
746
+ clonedProSwaps.push({
747
+ ...swapEvt,
748
+ time: swapTime.add(chance.integer({ min: 1, max: 96 }), "hours").toISOString(),
749
+ user_id: swapEvt.user_id,
750
+ trade_amount_usd: Math.round((swapEvt.trade_amount_usd || 200) * chance.floating({ min: 0.5, max: 3.0 })),
751
+ maker_fee_pct: 0.05,
752
+ });
753
+ }
754
+ });
755
+ userEvents.push(...clonedProSwaps);
756
+ } else {
757
+ userEvents.forEach(e => {
758
+ if (e.event === "swap") e.maker_fee_pct = 0.30;
759
+ });
760
+ }
761
+
762
+ // -----------------------------------------------------------
763
+ // Hook #8: RUG-PULL AFTERMATH
764
+ // Day 70, "SCAM" token rugs. Users who swapped SCAM token
765
+ // before day 70 lose 80% of events after day 70. No flag —
766
+ // derive cohort by detecting users with token_pair containing "SCAM".
767
+ // -----------------------------------------------------------
768
+ const day70 = datasetStart.add(70, "days");
769
+
770
+ // Check if user traded SCAM before day 70
771
+ // ~10% of pre-day-70 swaps naturally get SCAM token pair
772
+ // (injected here since SCAM isn't in the default TOKEN_PAIRS list)
773
+ let hadScam = false;
774
+ userEvents.forEach(e => {
775
+ if (e.event === "swap") {
776
+ const swapDay = dayjs(e.time).diff(datasetStart, "day");
777
+ // Before day 70, ~8% of swaps randomly become SCAM trades
778
+ if (swapDay >= 10 && swapDay < 70 && chance.bool({ likelihood: 8 })) {
779
+ e.token_pair = chance.pickone(["SCAM/USDC", "SCAM/ETH", "ETH/SCAM"]);
780
+ hadScam = true;
781
+ }
782
+ }
783
+ });
784
+
785
+ if (hadScam) {
786
+ for (let i = userEvents.length - 1; i >= 0; i--) {
787
+ const evt = userEvents[i];
788
+ if (dayjs(evt.time).isAfter(day70) && chance.bool({ likelihood: 80 })) {
789
+ userEvents.splice(i, 1);
790
+ }
791
+ }
792
+ }
793
+
794
+ // HOOK 10: SWAP-COUNT MAGIC NUMBER (no flags)
795
+ // Sweet 8-20 swaps → +30% on stake amount_usd; clone 1-2 extra
796
+ // stake events per existing. Over 21+ → drop 40% of portfolio
797
+ // viewed events (over-active traders ignore their portfolio).
798
+ const swapCount = userEvents.filter(e => e.event === "swap").length;
799
+ if (swapCount >= 8 && swapCount <= 20) {
800
+ const stakeTemplate = userEvents.find(e => e.event === "stake");
801
+ if (stakeTemplate) {
802
+ const stakes = userEvents.filter(e => e.event === "stake");
803
+ stakes.forEach(s => {
804
+ if (typeof s.amount_usd === "number") s.amount_usd = Math.round(s.amount_usd * 1.3);
805
+ const extras = chance.integer({ min: 1, max: 2 });
806
+ for (let k = 0; k < extras; k++) {
807
+ userEvents.push({
808
+ ...s,
809
+ time: dayjs(s.time).add(chance.integer({ min: 5, max: 360 }), "minutes").toISOString(),
810
+ user_id: s.user_id,
811
+ });
812
+ }
813
+ });
814
+ }
815
+ } else if (swapCount >= 21) {
816
+ for (let i = userEvents.length - 1; i >= 0; i--) {
817
+ if (userEvents[i].event === "portfolio viewed" && chance.bool({ likelihood: 40 })) {
818
+ userEvents.splice(i, 1);
819
+ }
820
+ }
821
+ }
822
+
823
+ userEvents.sort((a, b) => new Date(a.time) - new Date(b.time));
824
+ }
825
+
826
+ return record;
827
+ }
828
+ };
829
+
830
+ export default config;