@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.
- package/CHANGELOG.md +42 -0
- package/README.md +51 -13
- package/dungeons/technical/ad-spend.js +2 -2
- package/dungeons/technical/anonymous-users.js +2 -2
- package/dungeons/technical/array-of-object-lookup.js +2 -4
- package/dungeons/technical/experiments.js +2 -2
- package/dungeons/technical/foobar.js +2 -2
- package/dungeons/technical/group-analytics.js +2 -2
- package/dungeons/technical/mirror-strategies.js +2 -2
- package/dungeons/technical/nested-objects.js +2 -2
- package/dungeons/technical/retention-cadence.js +2 -3
- package/dungeons/technical/sanity.js +2 -2
- package/dungeons/technical/scale-test.js +2 -2
- package/dungeons/technical/scd.js +2 -2
- package/dungeons/technical/simple.js +5 -6
- package/dungeons/technical/simplest-schema.json +5 -0
- package/dungeons/technical/simplest.js +2 -2
- package/dungeons/technical/text-generation.js +3 -3
- package/dungeons/vertical/ai-platform.js +858 -0
- package/dungeons/vertical/community.js +84 -40
- package/dungeons/vertical/crypto.js +830 -0
- package/dungeons/vertical/dating.js +744 -0
- package/dungeons/vertical/devtools.js +175 -69
- package/dungeons/vertical/ecommerce.js +242 -94
- package/dungeons/vertical/education.js +330 -313
- package/dungeons/vertical/fintech.js +442 -313
- package/dungeons/vertical/fitness.js +143 -61
- package/dungeons/vertical/food-delivery.js +327 -353
- package/dungeons/vertical/gaming.js +912 -382
- package/dungeons/vertical/healthcare.js +142 -63
- package/dungeons/vertical/insurance-application.js +170 -76
- package/dungeons/vertical/logistics.js +115 -20
- package/dungeons/vertical/marketplace.js +152 -58
- package/dungeons/vertical/media.js +248 -384
- package/dungeons/vertical/real-estate.js +781 -0
- package/dungeons/vertical/sass.js +255 -266
- package/dungeons/vertical/social.js +264 -206
- package/dungeons/vertical/travel.js +117 -41
- package/index.js +17 -17
- package/lib/core/config-validator.js +159 -31
- package/lib/core/context.js +10 -24
- package/lib/core/storage.js +6 -1
- package/lib/generators/events.js +11 -14
- package/lib/generators/funnels.js +12 -4
- package/lib/generators/mirror.js +3 -2
- package/lib/generators/product-names.js +1 -1
- package/lib/generators/scd.js +2 -1
- package/lib/generators/text.js +1 -1
- package/lib/orchestrators/user-loop.js +81 -47
- package/lib/templates/macro-presets.js +111 -0
- package/lib/templates/soup-presets.js +19 -36
- package/lib/utils/utils.js +71 -39
- package/package.json +8 -2
- package/scripts/smoke-test-all.mjs +162 -0
- package/scripts/verify-runner.mjs +72 -24
- package/types.d.ts +251 -51
- package/dungeons/technical/ad-spend-schema.json +0 -128
- package/dungeons/technical/anonymous-users-schema.json +0 -92
- package/dungeons/technical/array-of-object-lookup-schema.json +0 -191
- package/dungeons/technical/experiments-schema.json +0 -203
- package/dungeons/technical/foobar-schema.json +0 -362
- package/dungeons/technical/group-analytics-schema.json +0 -241
- package/dungeons/technical/mirror-strategies-schema.json +0 -84
- package/dungeons/technical/nested-objects-schema.json +0 -145
- package/dungeons/technical/retention-cadence-schema.json +0 -37
- package/dungeons/technical/sanity-schema.json +0 -185
- package/dungeons/technical/scale-test-schema.json +0 -70
- package/dungeons/technical/scd-schema.json +0 -467
- package/dungeons/technical/simple-schema.json +0 -362
- package/dungeons/technical/text-generation-schema.json +0 -1062
- package/dungeons/user/.gitkeep +0 -0
- package/dungeons/vertical/community-schema.json +0 -579
- package/dungeons/vertical/devtools-schema.json +0 -601
- package/dungeons/vertical/ecommerce-schema.json +0 -604
- package/dungeons/vertical/education-schema.json +0 -5686
- package/dungeons/vertical/fintech-schema.json +0 -630
- package/dungeons/vertical/fitness-schema.json +0 -530
- package/dungeons/vertical/food-delivery-schema.json +0 -36728
- package/dungeons/vertical/gaming-schema.json +0 -438
- package/dungeons/vertical/healthcare-schema.json +0 -549
- package/dungeons/vertical/insurance-application-schema.json +0 -485
- package/dungeons/vertical/logistics-schema.json +0 -574
- package/dungeons/vertical/marketplace-schema.json +0 -533
- package/dungeons/vertical/media-schema.json +0 -4749
- package/dungeons/vertical/rpg-schema.json +0 -2491
- package/dungeons/vertical/rpg.js +0 -976
- package/dungeons/vertical/sass-schema.json +0 -3128
- package/dungeons/vertical/social-schema.json +0 -620
- 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;
|