@intentsolutionsio/token-launch-tracker 1.0.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.
Files changed (27) hide show
  1. package/.claude-plugin/plugin.json +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +162 -0
  4. package/agents/launch-tracker-agent.md +338 -0
  5. package/package.json +43 -0
  6. package/skills/skill-adapter/assets/README.md +5 -0
  7. package/skills/skill-adapter/assets/config-template.json +32 -0
  8. package/skills/skill-adapter/assets/skill-schema.json +28 -0
  9. package/skills/skill-adapter/assets/test-data.json +27 -0
  10. package/skills/skill-adapter/references/README.md +4 -0
  11. package/skills/skill-adapter/references/best-practices.md +69 -0
  12. package/skills/skill-adapter/references/examples.md +73 -0
  13. package/skills/skill-adapter/scripts/README.md +8 -0
  14. package/skills/skill-adapter/scripts/helper-template.sh +42 -0
  15. package/skills/skill-adapter/scripts/validation.sh +32 -0
  16. package/skills/tracking-token-launches/ARD.md +183 -0
  17. package/skills/tracking-token-launches/PRD.md +66 -0
  18. package/skills/tracking-token-launches/SKILL.md +161 -0
  19. package/skills/tracking-token-launches/config/settings.yaml +166 -0
  20. package/skills/tracking-token-launches/references/errors.md +167 -0
  21. package/skills/tracking-token-launches/references/examples.md +292 -0
  22. package/skills/tracking-token-launches/references/implementation.md +36 -0
  23. package/skills/tracking-token-launches/scripts/dex_sources.py +270 -0
  24. package/skills/tracking-token-launches/scripts/event_monitor.py +345 -0
  25. package/skills/tracking-token-launches/scripts/formatters.py +279 -0
  26. package/skills/tracking-token-launches/scripts/launch_tracker.py +572 -0
  27. package/skills/tracking-token-launches/scripts/token_analyzer.py +417 -0
@@ -0,0 +1,572 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Token Launch Tracker
4
+
5
+ Track new token launches across DEXes with risk analysis.
6
+
7
+ Author: Jeremy Longshore <jeremy@intentsolutions.io>
8
+ Version: 1.0.0
9
+ License: MIT
10
+ """
11
+
12
+ import argparse
13
+ import json
14
+ import sys
15
+ from datetime import datetime
16
+ from typing import Dict, List
17
+
18
+ # Local imports
19
+ from dex_sources import (
20
+ get_chain_config,
21
+ get_dex_factories,
22
+ CHAINS,
23
+ )
24
+ from event_monitor import EventMonitor, PairCreated
25
+ from token_analyzer import TokenAnalyzer, TokenInfo, ContractAnalysis
26
+ from formatters import (
27
+ format_new_pairs_table,
28
+ format_launch_detail,
29
+ format_chain_summary,
30
+ format_dex_summary,
31
+ format_risk_badge,
32
+ )
33
+
34
+
35
+ def cmd_recent(args) -> int:
36
+ """Show recently launched tokens."""
37
+ print(f"\nScanning {args.chain.upper()} for new launches...")
38
+ print(f"Looking back {args.hours} hours")
39
+ if args.dex:
40
+ print(f"Filtering by DEX: {args.dex}")
41
+ print()
42
+
43
+ try:
44
+ monitor = EventMonitor(
45
+ chain=args.chain,
46
+ rpc_url=args.rpc_url,
47
+ verbose=args.verbose
48
+ )
49
+
50
+ pairs = monitor.get_recent_pairs(hours=args.hours, dex=args.dex)
51
+
52
+ if not pairs:
53
+ print("No new pairs found in the specified timeframe.")
54
+ return 0
55
+
56
+ # Limit results
57
+ pairs = pairs[:args.limit]
58
+
59
+ # Enrich with token info and analysis if requested
60
+ token_infos: Dict[str, TokenInfo] = {}
61
+ analyses: Dict[str, ContractAnalysis] = {}
62
+
63
+ if args.analyze:
64
+ analyzer = TokenAnalyzer(
65
+ chain=args.chain,
66
+ rpc_url=args.rpc_url,
67
+ verbose=args.verbose
68
+ )
69
+
70
+ print(f"Analyzing {len(pairs)} pairs...")
71
+ for i, pair in enumerate(pairs):
72
+ new_token = monitor.identify_new_token(pair)
73
+
74
+ # Get token info
75
+ info = analyzer.get_token_info(new_token)
76
+ if info:
77
+ token_infos[new_token] = info
78
+
79
+ # Analyze contract
80
+ if not args.skip_analysis:
81
+ analysis = analyzer.analyze_contract(new_token)
82
+ analyses[new_token] = analysis
83
+
84
+ if args.verbose:
85
+ print(f" [{i+1}/{len(pairs)}] {info.symbol if info else 'Unknown'}")
86
+
87
+ # Output
88
+ if args.format == "json":
89
+ output = []
90
+ for pair in pairs:
91
+ new_token = monitor.identify_new_token(pair)
92
+ entry = {
93
+ "pair": vars(pair),
94
+ "token_info": vars(token_infos.get(new_token)) if new_token in token_infos else None,
95
+ "analysis": vars(analyses.get(new_token)) if new_token in analyses else None,
96
+ }
97
+ output.append(entry)
98
+ print(json.dumps(output, indent=2, default=str))
99
+ else:
100
+ print(format_new_pairs_table(pairs, token_infos, analyses))
101
+
102
+ return 0
103
+
104
+ except Exception as e:
105
+ print(f"Error: {e}")
106
+ if args.verbose:
107
+ import traceback
108
+ traceback.print_exc()
109
+ return 1
110
+
111
+
112
+ def cmd_detail(args) -> int:
113
+ """Show detailed launch information."""
114
+ print(f"\nFetching details for {args.address[:20]}...")
115
+
116
+ try:
117
+ monitor = EventMonitor(
118
+ chain=args.chain,
119
+ rpc_url=args.rpc_url,
120
+ verbose=args.verbose
121
+ )
122
+
123
+ analyzer = TokenAnalyzer(
124
+ chain=args.chain,
125
+ rpc_url=args.rpc_url,
126
+ etherscan_api_key=args.etherscan_key,
127
+ verbose=args.verbose
128
+ )
129
+
130
+ # Get token info
131
+ token_info = analyzer.get_token_info(args.address)
132
+
133
+ # Analyze contract
134
+ analysis = analyzer.analyze_contract(args.address)
135
+
136
+ # Get chain config
137
+ chain_config = get_chain_config(args.chain)
138
+
139
+ # Create a mock pair for display
140
+ mock_pair = PairCreated(
141
+ block_number=0,
142
+ tx_hash="",
143
+ timestamp=int(datetime.now().timestamp()),
144
+ pair_address=args.pair or "Unknown",
145
+ token0=args.address,
146
+ token1="",
147
+ dex=args.dex or "Unknown",
148
+ chain=args.chain,
149
+ factory_address="",
150
+ )
151
+
152
+ if args.format == "json":
153
+ output = {
154
+ "token_info": vars(token_info) if token_info else None,
155
+ "analysis": vars(analysis),
156
+ "risk_summary": analyzer.get_risk_summary(analysis),
157
+ }
158
+ print(json.dumps(output, indent=2, default=str))
159
+ else:
160
+ print(format_launch_detail(mock_pair, token_info, analysis, chain_config))
161
+
162
+ return 0
163
+
164
+ except Exception as e:
165
+ print(f"Error: {e}")
166
+ if args.verbose:
167
+ import traceback
168
+ traceback.print_exc()
169
+ return 1
170
+
171
+
172
+ def cmd_risk(args) -> int:
173
+ """Analyze token contract for risks."""
174
+ print(f"\nAnalyzing contract: {args.address[:20]}...")
175
+
176
+ try:
177
+ analyzer = TokenAnalyzer(
178
+ chain=args.chain,
179
+ rpc_url=args.rpc_url,
180
+ etherscan_api_key=args.etherscan_key,
181
+ verbose=args.verbose
182
+ )
183
+
184
+ analysis = analyzer.analyze_contract(args.address)
185
+
186
+ if args.format == "json":
187
+ output = {
188
+ "address": args.address,
189
+ "risk_score": analysis.risk_score,
190
+ "risk_level": analyzer.get_risk_summary(analysis),
191
+ "is_proxy": analysis.is_proxy,
192
+ "ownership_renounced": analysis.ownership_renounced,
193
+ "bytecode_size": analysis.bytecode_size,
194
+ "indicators": [vars(ind) for ind in analysis.indicators],
195
+ }
196
+ print(json.dumps(output, indent=2, default=str))
197
+ else:
198
+ print()
199
+ print("RISK ANALYSIS")
200
+ print("=" * 60)
201
+ print(f"Address: {args.address}")
202
+ print(f"Chain: {args.chain.upper()}")
203
+ print()
204
+ print(f"Risk Score: {analysis.risk_score}/100 {format_risk_badge(analysis.risk_score)}")
205
+ print(f"Risk Level: {analyzer.get_risk_summary(analysis)}")
206
+ print()
207
+ print("CONTRACT INFO")
208
+ print("-" * 60)
209
+ print(f"Bytecode: {analysis.bytecode_size:,} bytes")
210
+ print(f"Is Proxy: {'Yes' if analysis.is_proxy else 'No'}")
211
+ print(f"Ownership: {'Renounced' if analysis.ownership_renounced else 'Active'}")
212
+ print()
213
+ print("RISK INDICATORS")
214
+ print("-" * 60)
215
+
216
+ if analysis.indicators:
217
+ for ind in sorted(analysis.indicators, key=lambda x: {"high": 0, "medium": 1, "low": 2, "info": 3}.get(x.severity, 4)):
218
+ severity_marker = {
219
+ "high": "!!",
220
+ "medium": "! ",
221
+ "low": ". ",
222
+ "info": " ",
223
+ }.get(ind.severity, " ")
224
+ print(f" {severity_marker} [{ind.severity.upper():6}] {ind.name}")
225
+ print(f" {ind.description}")
226
+ else:
227
+ print(" No risk indicators detected")
228
+
229
+ print()
230
+ print("=" * 60)
231
+
232
+ return 0
233
+
234
+ except Exception as e:
235
+ print(f"Error: {e}")
236
+ if args.verbose:
237
+ import traceback
238
+ traceback.print_exc()
239
+ return 1
240
+
241
+
242
+ def cmd_summary(args) -> int:
243
+ """Show launch summary statistics."""
244
+ chains = args.chains.split(",") if args.chains else list(CHAINS.keys())
245
+
246
+ print(f"\nGenerating launch summary...")
247
+ print(f"Chains: {', '.join(chains)}")
248
+ print(f"Period: Last {args.hours} hours")
249
+ print()
250
+
251
+ try:
252
+ pairs_by_chain: Dict[str, int] = {}
253
+ pairs_by_dex: Dict[str, int] = {}
254
+ all_pairs: List[PairCreated] = []
255
+
256
+ for chain in chains:
257
+ if chain not in CHAINS:
258
+ print(f"Warning: Skipping unsupported chain: {chain}")
259
+ continue
260
+
261
+ if args.verbose:
262
+ print(f"Scanning {chain}...")
263
+
264
+ try:
265
+ monitor = EventMonitor(chain=chain, verbose=args.verbose)
266
+ pairs = monitor.get_recent_pairs(hours=args.hours)
267
+
268
+ pairs_by_chain[chain] = len(pairs)
269
+ all_pairs.extend(pairs)
270
+
271
+ for pair in pairs:
272
+ dex_key = f"{chain}:{pair.dex}"
273
+ pairs_by_dex[dex_key] = pairs_by_dex.get(dex_key, 0) + 1
274
+
275
+ except Exception as e:
276
+ print(f"Error scanning {chain}: {e}")
277
+ pairs_by_chain[chain] = 0
278
+
279
+ if args.format == "json":
280
+ output = {
281
+ "period_hours": args.hours,
282
+ "chains": chains,
283
+ "by_chain": pairs_by_chain,
284
+ "by_dex": pairs_by_dex,
285
+ "total": sum(pairs_by_chain.values()),
286
+ }
287
+ print(json.dumps(output, indent=2))
288
+ else:
289
+ print(format_chain_summary(pairs_by_chain))
290
+ print(format_dex_summary(pairs_by_dex))
291
+
292
+ return 0
293
+
294
+ except Exception as e:
295
+ print(f"Error: {e}")
296
+ if args.verbose:
297
+ import traceback
298
+ traceback.print_exc()
299
+ return 1
300
+
301
+
302
+ def cmd_dexes(args) -> int:
303
+ """List supported DEXes."""
304
+ chain = args.chain
305
+
306
+ if chain:
307
+ if chain not in CHAINS:
308
+ print(f"Error: Unsupported chain: {chain}")
309
+ print(f"Supported chains: {', '.join(CHAINS.keys())}")
310
+ return 1
311
+
312
+ factories = get_dex_factories(chain)
313
+
314
+ if args.format == "json":
315
+ output = {
316
+ "chain": chain,
317
+ "dexes": [
318
+ {
319
+ "name": f.name,
320
+ "address": f.address,
321
+ "version": f.version,
322
+ }
323
+ for f in factories.values()
324
+ ]
325
+ }
326
+ print(json.dumps(output, indent=2))
327
+ else:
328
+ print()
329
+ print(f"SUPPORTED DEXES ON {chain.upper()}")
330
+ print("=" * 60)
331
+ for key, factory in factories.items():
332
+ print(f" {factory.name:<25} ({factory.version})")
333
+ print(f" Factory: {factory.address}")
334
+ print("=" * 60)
335
+ else:
336
+ # List all
337
+ if args.format == "json":
338
+ output = {}
339
+ for chain_id in CHAINS:
340
+ factories = get_dex_factories(chain_id)
341
+ output[chain_id] = [
342
+ {"name": f.name, "address": f.address, "version": f.version}
343
+ for f in factories.values()
344
+ ]
345
+ print(json.dumps(output, indent=2))
346
+ else:
347
+ print()
348
+ print("SUPPORTED DEXES BY CHAIN")
349
+ print("=" * 60)
350
+ for chain_id, config in CHAINS.items():
351
+ factories = get_dex_factories(chain_id)
352
+ print(f"\n{config.name} ({chain_id})")
353
+ print("-" * 40)
354
+ for factory in factories.values():
355
+ print(f" {factory.name:<25} {factory.version}")
356
+ print()
357
+ print("=" * 60)
358
+
359
+ return 0
360
+
361
+
362
+ def cmd_chains(args) -> int:
363
+ """List supported chains."""
364
+ if args.format == "json":
365
+ output = {
366
+ chain_id: {
367
+ "name": config.name,
368
+ "chain_id": config.chain_id,
369
+ "native_symbol": config.native_symbol,
370
+ "block_time": config.block_time,
371
+ "explorer_url": config.explorer_url,
372
+ }
373
+ for chain_id, config in CHAINS.items()
374
+ }
375
+ print(json.dumps(output, indent=2))
376
+ else:
377
+ print()
378
+ print("SUPPORTED CHAINS")
379
+ print("=" * 70)
380
+ print(f"{'Chain':<15} {'Name':<20} {'ID':<10} {'Symbol':<8} {'Block':<8}")
381
+ print("-" * 70)
382
+ for chain_id, config in CHAINS.items():
383
+ print(f"{chain_id:<15} {config.name:<20} {config.chain_id:<10} {config.native_symbol:<8} {config.block_time}s")
384
+ print("=" * 70)
385
+
386
+ return 0
387
+
388
+
389
+ def main():
390
+ """Main entry point."""
391
+ parser = argparse.ArgumentParser(
392
+ description="Track new token launches across DEXes",
393
+ formatter_class=argparse.RawDescriptionHelpFormatter,
394
+ epilog="""
395
+ Examples:
396
+ # Show recent launches on Ethereum
397
+ %(prog)s recent --chain ethereum --hours 24
398
+
399
+ # Get detailed info for a specific token
400
+ %(prog)s detail --address 0x... --chain ethereum
401
+
402
+ # Analyze token risk
403
+ %(prog)s risk --address 0x... --chain base
404
+
405
+ # Show launch summary across all chains
406
+ %(prog)s summary --hours 24
407
+
408
+ # List supported DEXes
409
+ %(prog)s dexes --chain bsc
410
+ """
411
+ )
412
+
413
+ parser.add_argument(
414
+ "-v", "--verbose",
415
+ action="store_true",
416
+ help="Enable verbose output"
417
+ )
418
+ parser.add_argument(
419
+ "-f", "--format",
420
+ choices=["text", "json"],
421
+ default="text",
422
+ help="Output format (default: text)"
423
+ )
424
+
425
+ subparsers = parser.add_subparsers(dest="command", help="Command")
426
+
427
+ # recent command
428
+ recent_parser = subparsers.add_parser(
429
+ "recent",
430
+ help="Show recently launched tokens"
431
+ )
432
+ recent_parser.add_argument(
433
+ "--chain", "-c",
434
+ default="ethereum",
435
+ help="Chain to scan (default: ethereum)"
436
+ )
437
+ recent_parser.add_argument(
438
+ "--hours", "-H",
439
+ type=int,
440
+ default=24,
441
+ help="Hours to look back (default: 24)"
442
+ )
443
+ recent_parser.add_argument(
444
+ "--dex", "-d",
445
+ help="Filter by DEX name"
446
+ )
447
+ recent_parser.add_argument(
448
+ "--limit", "-l",
449
+ type=int,
450
+ default=50,
451
+ help="Maximum results (default: 50)"
452
+ )
453
+ recent_parser.add_argument(
454
+ "--analyze", "-a",
455
+ action="store_true",
456
+ help="Include token analysis"
457
+ )
458
+ recent_parser.add_argument(
459
+ "--skip-analysis",
460
+ action="store_true",
461
+ help="Skip contract analysis (faster)"
462
+ )
463
+ recent_parser.add_argument(
464
+ "--rpc-url",
465
+ help="Custom RPC URL"
466
+ )
467
+ recent_parser.set_defaults(func=cmd_recent)
468
+
469
+ # detail command
470
+ detail_parser = subparsers.add_parser(
471
+ "detail",
472
+ help="Show detailed launch information"
473
+ )
474
+ detail_parser.add_argument(
475
+ "--address", "-a",
476
+ required=True,
477
+ help="Token contract address"
478
+ )
479
+ detail_parser.add_argument(
480
+ "--chain", "-c",
481
+ default="ethereum",
482
+ help="Chain (default: ethereum)"
483
+ )
484
+ detail_parser.add_argument(
485
+ "--pair", "-p",
486
+ help="Pair address (optional)"
487
+ )
488
+ detail_parser.add_argument(
489
+ "--dex", "-d",
490
+ help="DEX name (optional)"
491
+ )
492
+ detail_parser.add_argument(
493
+ "--etherscan-key",
494
+ help="Etherscan API key for verification check"
495
+ )
496
+ detail_parser.add_argument(
497
+ "--rpc-url",
498
+ help="Custom RPC URL"
499
+ )
500
+ detail_parser.set_defaults(func=cmd_detail)
501
+
502
+ # risk command
503
+ risk_parser = subparsers.add_parser(
504
+ "risk",
505
+ help="Analyze token contract for risks"
506
+ )
507
+ risk_parser.add_argument(
508
+ "--address", "-a",
509
+ required=True,
510
+ help="Token contract address"
511
+ )
512
+ risk_parser.add_argument(
513
+ "--chain", "-c",
514
+ default="ethereum",
515
+ help="Chain (default: ethereum)"
516
+ )
517
+ risk_parser.add_argument(
518
+ "--etherscan-key",
519
+ help="Etherscan API key for verification check"
520
+ )
521
+ risk_parser.add_argument(
522
+ "--rpc-url",
523
+ help="Custom RPC URL"
524
+ )
525
+ risk_parser.set_defaults(func=cmd_risk)
526
+
527
+ # summary command
528
+ summary_parser = subparsers.add_parser(
529
+ "summary",
530
+ help="Show launch summary statistics"
531
+ )
532
+ summary_parser.add_argument(
533
+ "--chains",
534
+ help="Comma-separated chains (default: all)"
535
+ )
536
+ summary_parser.add_argument(
537
+ "--hours", "-H",
538
+ type=int,
539
+ default=24,
540
+ help="Hours to look back (default: 24)"
541
+ )
542
+ summary_parser.set_defaults(func=cmd_summary)
543
+
544
+ # dexes command
545
+ dexes_parser = subparsers.add_parser(
546
+ "dexes",
547
+ help="List supported DEXes"
548
+ )
549
+ dexes_parser.add_argument(
550
+ "--chain", "-c",
551
+ help="Show DEXes for specific chain"
552
+ )
553
+ dexes_parser.set_defaults(func=cmd_dexes)
554
+
555
+ # chains command
556
+ chains_parser = subparsers.add_parser(
557
+ "chains",
558
+ help="List supported chains"
559
+ )
560
+ chains_parser.set_defaults(func=cmd_chains)
561
+
562
+ args = parser.parse_args()
563
+
564
+ if not args.command:
565
+ parser.print_help()
566
+ return 1
567
+
568
+ return args.func(args)
569
+
570
+
571
+ if __name__ == "__main__":
572
+ sys.exit(main())