@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.
- package/.claude-plugin/plugin.json +22 -0
- package/LICENSE +21 -0
- package/README.md +162 -0
- package/agents/launch-tracker-agent.md +338 -0
- package/package.json +43 -0
- package/skills/skill-adapter/assets/README.md +5 -0
- package/skills/skill-adapter/assets/config-template.json +32 -0
- package/skills/skill-adapter/assets/skill-schema.json +28 -0
- package/skills/skill-adapter/assets/test-data.json +27 -0
- package/skills/skill-adapter/references/README.md +4 -0
- package/skills/skill-adapter/references/best-practices.md +69 -0
- package/skills/skill-adapter/references/examples.md +73 -0
- package/skills/skill-adapter/scripts/README.md +8 -0
- package/skills/skill-adapter/scripts/helper-template.sh +42 -0
- package/skills/skill-adapter/scripts/validation.sh +32 -0
- package/skills/tracking-token-launches/ARD.md +183 -0
- package/skills/tracking-token-launches/PRD.md +66 -0
- package/skills/tracking-token-launches/SKILL.md +161 -0
- package/skills/tracking-token-launches/config/settings.yaml +166 -0
- package/skills/tracking-token-launches/references/errors.md +167 -0
- package/skills/tracking-token-launches/references/examples.md +292 -0
- package/skills/tracking-token-launches/references/implementation.md +36 -0
- package/skills/tracking-token-launches/scripts/dex_sources.py +270 -0
- package/skills/tracking-token-launches/scripts/event_monitor.py +345 -0
- package/skills/tracking-token-launches/scripts/formatters.py +279 -0
- package/skills/tracking-token-launches/scripts/launch_tracker.py +572 -0
- package/skills/tracking-token-launches/scripts/token_analyzer.py +417 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Token Launch Formatters
|
|
4
|
+
|
|
5
|
+
Format launch data for various outputs.
|
|
6
|
+
|
|
7
|
+
Author: Jeremy Longshore <jeremy@intentsolutions.io>
|
|
8
|
+
Version: 1.0.0
|
|
9
|
+
License: MIT
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from typing import List, Dict, Any
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def format_timestamp(ts: int) -> str:
|
|
18
|
+
"""Format Unix timestamp."""
|
|
19
|
+
dt = datetime.fromtimestamp(ts)
|
|
20
|
+
return dt.strftime("%Y-%m-%d %H:%M")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def format_age(ts: int) -> str:
|
|
24
|
+
"""Format time ago."""
|
|
25
|
+
now = int(datetime.now().timestamp())
|
|
26
|
+
diff = now - ts
|
|
27
|
+
|
|
28
|
+
if diff < 60:
|
|
29
|
+
return f"{diff}s ago"
|
|
30
|
+
elif diff < 3600:
|
|
31
|
+
return f"{diff // 60}m ago"
|
|
32
|
+
elif diff < 86400:
|
|
33
|
+
return f"{diff // 3600}h ago"
|
|
34
|
+
else:
|
|
35
|
+
return f"{diff // 86400}d ago"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def format_address(address: str, length: int = 10) -> str:
|
|
39
|
+
"""Format address with ellipsis."""
|
|
40
|
+
if not address:
|
|
41
|
+
return "N/A"
|
|
42
|
+
if len(address) <= length * 2:
|
|
43
|
+
return address
|
|
44
|
+
return f"{address[:length]}...{address[-4:]}"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def format_risk_badge(score: int) -> str:
|
|
48
|
+
"""Format risk score as badge."""
|
|
49
|
+
if score >= 70:
|
|
50
|
+
return f"[HIGH RISK: {score}]"
|
|
51
|
+
elif score >= 40:
|
|
52
|
+
return f"[MEDIUM: {score}]"
|
|
53
|
+
elif score >= 20:
|
|
54
|
+
return f"[LOW: {score}]"
|
|
55
|
+
else:
|
|
56
|
+
return f"[OK: {score}]"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def format_supply(supply: int, decimals: int) -> str:
|
|
60
|
+
"""Format token supply."""
|
|
61
|
+
value = supply / (10 ** decimals)
|
|
62
|
+
|
|
63
|
+
if value >= 1e12:
|
|
64
|
+
return f"{value / 1e12:.2f}T"
|
|
65
|
+
elif value >= 1e9:
|
|
66
|
+
return f"{value / 1e9:.2f}B"
|
|
67
|
+
elif value >= 1e6:
|
|
68
|
+
return f"{value / 1e6:.2f}M"
|
|
69
|
+
elif value >= 1e3:
|
|
70
|
+
return f"{value / 1e3:.2f}K"
|
|
71
|
+
else:
|
|
72
|
+
return f"{value:.2f}"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def format_new_pairs_table(
|
|
76
|
+
pairs: List[Any],
|
|
77
|
+
token_infos: Dict[str, Any],
|
|
78
|
+
analyses: Dict[str, Any]
|
|
79
|
+
) -> str:
|
|
80
|
+
"""Format new pairs as table.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
pairs: List of PairCreated events
|
|
84
|
+
token_infos: Dict of address -> TokenInfo
|
|
85
|
+
analyses: Dict of address -> ContractAnalysis
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Formatted table string
|
|
89
|
+
"""
|
|
90
|
+
lines = [
|
|
91
|
+
"",
|
|
92
|
+
"NEW TOKEN LAUNCHES",
|
|
93
|
+
"=" * 90,
|
|
94
|
+
f"{'Time':<12} {'Token':<20} {'DEX':<15} {'Chain':<10} {'Risk':<15} {'Pair':<18}",
|
|
95
|
+
"-" * 90,
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
for pair in pairs:
|
|
99
|
+
# Get new token address (not the base token)
|
|
100
|
+
new_token = pair.token0 # Simplified - should use identify_new_token
|
|
101
|
+
|
|
102
|
+
info = token_infos.get(new_token)
|
|
103
|
+
analysis = analyses.get(new_token)
|
|
104
|
+
|
|
105
|
+
token_str = f"{info.symbol if info else '???'}" if info else "Unknown"
|
|
106
|
+
if len(token_str) > 18:
|
|
107
|
+
token_str = token_str[:15] + "..."
|
|
108
|
+
|
|
109
|
+
risk_str = format_risk_badge(analysis.risk_score) if analysis else "[N/A]"
|
|
110
|
+
pair_str = format_address(pair.pair_address, 8)
|
|
111
|
+
time_str = format_age(pair.timestamp)
|
|
112
|
+
|
|
113
|
+
lines.append(
|
|
114
|
+
f"{time_str:<12} {token_str:<20} {pair.dex:<15} "
|
|
115
|
+
f"{pair.chain:<10} {risk_str:<15} {pair_str:<18}"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
lines.append("=" * 90)
|
|
119
|
+
lines.append(f"Total: {len(pairs)} new pairs")
|
|
120
|
+
|
|
121
|
+
return "\n".join(lines)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def format_launch_detail(
|
|
125
|
+
pair: Any,
|
|
126
|
+
token_info: Any,
|
|
127
|
+
analysis: Any,
|
|
128
|
+
chain_config: Any
|
|
129
|
+
) -> str:
|
|
130
|
+
"""Format detailed launch info.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
pair: PairCreated event
|
|
134
|
+
token_info: TokenInfo object
|
|
135
|
+
analysis: ContractAnalysis object
|
|
136
|
+
chain_config: ChainConfig object
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Formatted detail string
|
|
140
|
+
"""
|
|
141
|
+
lines = [
|
|
142
|
+
"",
|
|
143
|
+
f"TOKEN LAUNCH: {token_info.symbol if token_info else 'Unknown'}",
|
|
144
|
+
"=" * 60,
|
|
145
|
+
f"Name: {token_info.name if token_info else 'Unknown'}",
|
|
146
|
+
f"Symbol: {token_info.symbol if token_info else '???'}",
|
|
147
|
+
f"Address: {pair.token0}",
|
|
148
|
+
f"Pair: {pair.pair_address}",
|
|
149
|
+
"",
|
|
150
|
+
"LAUNCH INFO",
|
|
151
|
+
"-" * 60,
|
|
152
|
+
f"DEX: {pair.dex}",
|
|
153
|
+
f"Chain: {pair.chain.upper()}",
|
|
154
|
+
f"Block: {pair.block_number:,}",
|
|
155
|
+
f"Time: {format_timestamp(pair.timestamp)} ({format_age(pair.timestamp)})",
|
|
156
|
+
f"Tx: {pair.tx_hash}",
|
|
157
|
+
"",
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
if token_info:
|
|
161
|
+
lines.extend([
|
|
162
|
+
"TOKEN INFO",
|
|
163
|
+
"-" * 60,
|
|
164
|
+
f"Decimals: {token_info.decimals}",
|
|
165
|
+
f"Total Supply: {format_supply(token_info.total_supply, token_info.decimals)}",
|
|
166
|
+
f"Owner: {format_address(token_info.owner) if token_info.owner else 'None'}",
|
|
167
|
+
f"Verified: {'Yes' if token_info.is_verified else 'No'}",
|
|
168
|
+
"",
|
|
169
|
+
])
|
|
170
|
+
|
|
171
|
+
if analysis:
|
|
172
|
+
lines.extend([
|
|
173
|
+
"RISK ANALYSIS",
|
|
174
|
+
"-" * 60,
|
|
175
|
+
f"Risk Score: {analysis.risk_score}/100 {format_risk_badge(analysis.risk_score)}",
|
|
176
|
+
f"Is Proxy: {'Yes' if analysis.is_proxy else 'No'}",
|
|
177
|
+
f"Ownership: {'Renounced' if analysis.ownership_renounced else 'Active'}",
|
|
178
|
+
"",
|
|
179
|
+
"Indicators:",
|
|
180
|
+
])
|
|
181
|
+
|
|
182
|
+
for ind in analysis.indicators:
|
|
183
|
+
severity_marker = {
|
|
184
|
+
"high": "!!",
|
|
185
|
+
"medium": "! ",
|
|
186
|
+
"low": ". ",
|
|
187
|
+
"info": " ",
|
|
188
|
+
}.get(ind.severity, " ")
|
|
189
|
+
lines.append(f" {severity_marker} {ind.name}: {ind.description}")
|
|
190
|
+
|
|
191
|
+
lines.append("")
|
|
192
|
+
lines.append("LINKS")
|
|
193
|
+
lines.append("-" * 60)
|
|
194
|
+
lines.append(f"Explorer: {chain_config.explorer_url}/address/{pair.token0}")
|
|
195
|
+
lines.append(f"DEXScreener: https://dexscreener.com/{pair.chain}/{pair.pair_address}")
|
|
196
|
+
|
|
197
|
+
lines.append("=" * 60)
|
|
198
|
+
|
|
199
|
+
return "\n".join(lines)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def format_chain_summary(pairs_by_chain: Dict[str, int]) -> str:
|
|
203
|
+
"""Format summary by chain.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
pairs_by_chain: Dict of chain -> count
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Formatted summary
|
|
210
|
+
"""
|
|
211
|
+
lines = [
|
|
212
|
+
"",
|
|
213
|
+
"LAUNCHES BY CHAIN",
|
|
214
|
+
"=" * 40,
|
|
215
|
+
]
|
|
216
|
+
|
|
217
|
+
total = 0
|
|
218
|
+
for chain, count in sorted(pairs_by_chain.items(), key=lambda x: -x[1]):
|
|
219
|
+
lines.append(f" {chain.upper():<15} {count:>10}")
|
|
220
|
+
total += count
|
|
221
|
+
|
|
222
|
+
lines.append("-" * 40)
|
|
223
|
+
lines.append(f" {'TOTAL':<15} {total:>10}")
|
|
224
|
+
lines.append("=" * 40)
|
|
225
|
+
|
|
226
|
+
return "\n".join(lines)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def format_dex_summary(pairs_by_dex: Dict[str, int]) -> str:
|
|
230
|
+
"""Format summary by DEX.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
pairs_by_dex: Dict of dex -> count
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Formatted summary
|
|
237
|
+
"""
|
|
238
|
+
lines = [
|
|
239
|
+
"",
|
|
240
|
+
"LAUNCHES BY DEX",
|
|
241
|
+
"=" * 40,
|
|
242
|
+
]
|
|
243
|
+
|
|
244
|
+
for dex, count in sorted(pairs_by_dex.items(), key=lambda x: -x[1]):
|
|
245
|
+
lines.append(f" {dex:<25} {count:>10}")
|
|
246
|
+
|
|
247
|
+
lines.append("=" * 40)
|
|
248
|
+
|
|
249
|
+
return "\n".join(lines)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def format_json(data: Any) -> str:
|
|
253
|
+
"""Format data as JSON."""
|
|
254
|
+
if hasattr(data, "__dict__"):
|
|
255
|
+
return json.dumps(vars(data), indent=2, default=str)
|
|
256
|
+
elif isinstance(data, list):
|
|
257
|
+
return json.dumps(
|
|
258
|
+
[vars(x) if hasattr(x, "__dict__") else x for x in data],
|
|
259
|
+
indent=2,
|
|
260
|
+
default=str
|
|
261
|
+
)
|
|
262
|
+
else:
|
|
263
|
+
return json.dumps(data, indent=2, default=str)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def main():
|
|
267
|
+
"""CLI entry point for testing."""
|
|
268
|
+
print("=== Formatter Tests ===")
|
|
269
|
+
print(f"Timestamp: {format_timestamp(1705784400)}")
|
|
270
|
+
print(f"Age: {format_age(1705784400)}")
|
|
271
|
+
print(f"Address: {format_address('0x1234567890abcdef1234567890abcdef12345678')}")
|
|
272
|
+
print(f"Risk: {format_risk_badge(75)}")
|
|
273
|
+
print(f"Risk: {format_risk_badge(45)}")
|
|
274
|
+
print(f"Risk: {format_risk_badge(15)}")
|
|
275
|
+
print(f"Supply: {format_supply(1000000000000000000000000, 18)}")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
if __name__ == "__main__":
|
|
279
|
+
main()
|