@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,270 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ DEX Factory Addresses
4
+
5
+ Factory contract addresses for detecting new pairs.
6
+
7
+ Author: Jeremy Longshore <jeremy@intentsolutions.io>
8
+ Version: 1.0.0
9
+ License: MIT
10
+ """
11
+
12
+ from dataclasses import dataclass
13
+ from typing import Dict, List
14
+
15
+ # PairCreated event signature for Uniswap V2 style
16
+ PAIR_CREATED_TOPIC = "0x0d3648bd0f6ba80134a33ba9275ac585d9d315f0ad8355cddefde31afa28d0e9"
17
+
18
+
19
+ @dataclass
20
+ class DexFactory:
21
+ """DEX factory configuration."""
22
+ name: str
23
+ address: str
24
+ version: str # v2 or v3
25
+ pair_created_topic: str
26
+
27
+
28
+ @dataclass
29
+ class ChainConfig:
30
+ """Chain configuration."""
31
+ name: str
32
+ chain_id: int
33
+ rpc_url: str
34
+ native_symbol: str
35
+ wrapped_native: str
36
+ block_time: float
37
+ explorer_url: str
38
+
39
+
40
+ # Chain configurations
41
+ CHAINS: Dict[str, ChainConfig] = {
42
+ "ethereum": ChainConfig(
43
+ name="Ethereum",
44
+ chain_id=1,
45
+ rpc_url="https://eth.llamarpc.com",
46
+ native_symbol="ETH",
47
+ wrapped_native="0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
48
+ block_time=12.0,
49
+ explorer_url="https://etherscan.io",
50
+ ),
51
+ "bsc": ChainConfig(
52
+ name="BNB Chain",
53
+ chain_id=56,
54
+ rpc_url="https://bsc-dataseed1.binance.org",
55
+ native_symbol="BNB",
56
+ wrapped_native="0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
57
+ block_time=3.0,
58
+ explorer_url="https://bscscan.com",
59
+ ),
60
+ "arbitrum": ChainConfig(
61
+ name="Arbitrum One",
62
+ chain_id=42161,
63
+ rpc_url="https://arb1.arbitrum.io/rpc",
64
+ native_symbol="ETH",
65
+ wrapped_native="0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
66
+ block_time=0.25,
67
+ explorer_url="https://arbiscan.io",
68
+ ),
69
+ "base": ChainConfig(
70
+ name="Base",
71
+ chain_id=8453,
72
+ rpc_url="https://mainnet.base.org",
73
+ native_symbol="ETH",
74
+ wrapped_native="0x4200000000000000000000000000000000000006",
75
+ block_time=2.0,
76
+ explorer_url="https://basescan.org",
77
+ ),
78
+ "polygon": ChainConfig(
79
+ name="Polygon",
80
+ chain_id=137,
81
+ rpc_url="https://polygon-rpc.com",
82
+ native_symbol="MATIC",
83
+ wrapped_native="0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270",
84
+ block_time=2.0,
85
+ explorer_url="https://polygonscan.com",
86
+ ),
87
+ }
88
+
89
+
90
+ # DEX factory addresses per chain
91
+ DEX_FACTORIES: Dict[str, Dict[str, DexFactory]] = {
92
+ "ethereum": {
93
+ "uniswap_v2": DexFactory(
94
+ name="Uniswap V2",
95
+ address="0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f",
96
+ version="v2",
97
+ pair_created_topic=PAIR_CREATED_TOPIC,
98
+ ),
99
+ "uniswap_v3": DexFactory(
100
+ name="Uniswap V3",
101
+ address="0x1F98431c8aD98523631AE4a59f267346ea31F984",
102
+ version="v3",
103
+ pair_created_topic="0x783cca1c0412dd0d695e784568c96da2e9c22ff989357a2e8b1d9b2b4e6b7118",
104
+ ),
105
+ "sushiswap": DexFactory(
106
+ name="SushiSwap",
107
+ address="0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac",
108
+ version="v2",
109
+ pair_created_topic=PAIR_CREATED_TOPIC,
110
+ ),
111
+ },
112
+ "bsc": {
113
+ "pancakeswap_v2": DexFactory(
114
+ name="PancakeSwap V2",
115
+ address="0xcA143Ce32Fe78f1f7019d7d551a6402fC5350c73",
116
+ version="v2",
117
+ pair_created_topic=PAIR_CREATED_TOPIC,
118
+ ),
119
+ "pancakeswap_v3": DexFactory(
120
+ name="PancakeSwap V3",
121
+ address="0x0BFbCF9fa4f9C56B0F40a671Ad40E0805A091865",
122
+ version="v3",
123
+ pair_created_topic="0x783cca1c0412dd0d695e784568c96da2e9c22ff989357a2e8b1d9b2b4e6b7118",
124
+ ),
125
+ },
126
+ "arbitrum": {
127
+ "uniswap_v3": DexFactory(
128
+ name="Uniswap V3",
129
+ address="0x1F98431c8aD98523631AE4a59f267346ea31F984",
130
+ version="v3",
131
+ pair_created_topic="0x783cca1c0412dd0d695e784568c96da2e9c22ff989357a2e8b1d9b2b4e6b7118",
132
+ ),
133
+ "camelot": DexFactory(
134
+ name="Camelot",
135
+ address="0x6EcCab422D763aC031210895C81787E87B43A652",
136
+ version="v2",
137
+ pair_created_topic=PAIR_CREATED_TOPIC,
138
+ ),
139
+ "sushiswap": DexFactory(
140
+ name="SushiSwap",
141
+ address="0xc35DADB65012eC5796536bD9864eD8773aBc74C4",
142
+ version="v2",
143
+ pair_created_topic=PAIR_CREATED_TOPIC,
144
+ ),
145
+ },
146
+ "base": {
147
+ "uniswap_v3": DexFactory(
148
+ name="Uniswap V3",
149
+ address="0x33128a8fC17869897dcE68Ed026d694621f6FDfD",
150
+ version="v3",
151
+ pair_created_topic="0x783cca1c0412dd0d695e784568c96da2e9c22ff989357a2e8b1d9b2b4e6b7118",
152
+ ),
153
+ "aerodrome": DexFactory(
154
+ name="Aerodrome",
155
+ address="0x420DD381b31aEf6683db6B902084cB0FFECe40Da",
156
+ version="v2",
157
+ pair_created_topic=PAIR_CREATED_TOPIC,
158
+ ),
159
+ },
160
+ "polygon": {
161
+ "uniswap_v3": DexFactory(
162
+ name="Uniswap V3",
163
+ address="0x1F98431c8aD98523631AE4a59f267346ea31F984",
164
+ version="v3",
165
+ pair_created_topic="0x783cca1c0412dd0d695e784568c96da2e9c22ff989357a2e8b1d9b2b4e6b7118",
166
+ ),
167
+ "quickswap": DexFactory(
168
+ name="QuickSwap",
169
+ address="0x5757371414417b8C6CAad45bAeF941aBc7d3Ab32",
170
+ version="v2",
171
+ pair_created_topic=PAIR_CREATED_TOPIC,
172
+ ),
173
+ "sushiswap": DexFactory(
174
+ name="SushiSwap",
175
+ address="0xc35DADB65012eC5796536bD9864eD8773aBc74C4",
176
+ version="v2",
177
+ pair_created_topic=PAIR_CREATED_TOPIC,
178
+ ),
179
+ },
180
+ }
181
+
182
+
183
+ # Common stablecoins per chain (for base pair detection)
184
+ STABLECOINS: Dict[str, List[str]] = {
185
+ "ethereum": [
186
+ "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", # USDC
187
+ "0xdAC17F958D2ee523a2206206994597C13D831ec7", # USDT
188
+ "0x6B175474E89094C44Da98b954E2deC563975aA61", # DAI
189
+ ],
190
+ "bsc": [
191
+ "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d", # USDC
192
+ "0x55d398326f99059fF775485246999027B3197955", # USDT
193
+ "0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56", # BUSD
194
+ ],
195
+ "arbitrum": [
196
+ "0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8", # USDC.e
197
+ "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", # USDC
198
+ "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", # USDT
199
+ ],
200
+ "base": [
201
+ "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", # USDC
202
+ "0xd9aAEc86B65D86f6A7B5B1b0c42FFA531710b6CA", # USDbC
203
+ ],
204
+ "polygon": [
205
+ "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", # USDC
206
+ "0xc2132D05D31c914a87C6611C10748AEb04B58e8F", # USDT
207
+ "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063", # DAI
208
+ ],
209
+ }
210
+
211
+
212
+ def get_chain_config(chain: str) -> ChainConfig:
213
+ """Get chain configuration."""
214
+ chain = chain.lower()
215
+ if chain not in CHAINS:
216
+ raise ValueError(f"Unsupported chain: {chain}. Supported: {list(CHAINS.keys())}")
217
+ return CHAINS[chain]
218
+
219
+
220
+ def get_dex_factories(chain: str) -> Dict[str, DexFactory]:
221
+ """Get DEX factories for a chain."""
222
+ chain = chain.lower()
223
+ return DEX_FACTORIES.get(chain, {})
224
+
225
+
226
+ def get_all_factory_addresses(chain: str) -> List[str]:
227
+ """Get all factory addresses for a chain."""
228
+ factories = get_dex_factories(chain)
229
+ return [f.address.lower() for f in factories.values()]
230
+
231
+
232
+ def identify_dex(chain: str, factory_address: str) -> str:
233
+ """Identify DEX by factory address."""
234
+ factories = get_dex_factories(chain)
235
+ factory_address = factory_address.lower()
236
+
237
+ for name, factory in factories.items():
238
+ if factory.address.lower() == factory_address:
239
+ return factory.name
240
+
241
+ return "Unknown DEX"
242
+
243
+
244
+ def is_base_token(chain: str, address: str) -> bool:
245
+ """Check if address is a base token (stablecoin or wrapped native)."""
246
+ address = address.lower()
247
+ chain_config = get_chain_config(chain)
248
+
249
+ if address == chain_config.wrapped_native.lower():
250
+ return True
251
+
252
+ stables = STABLECOINS.get(chain, [])
253
+ return address in [s.lower() for s in stables]
254
+
255
+
256
+ def main():
257
+ """CLI entry point for testing."""
258
+ print("=== Supported Chains ===")
259
+ for chain_id, config in CHAINS.items():
260
+ print(f" {chain_id}: {config.name} (Chain ID: {config.chain_id})")
261
+
262
+ print("\n=== DEX Factories ===")
263
+ for chain_id, factories in DEX_FACTORIES.items():
264
+ print(f"\n{chain_id.upper()}:")
265
+ for name, factory in factories.items():
266
+ print(f" {factory.name} ({factory.version}): {factory.address[:20]}...")
267
+
268
+
269
+ if __name__ == "__main__":
270
+ main()
@@ -0,0 +1,345 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Blockchain Event Monitor
4
+
5
+ Monitor DEX factory events for new pair creations.
6
+
7
+ Author: Jeremy Longshore <jeremy@intentsolutions.io>
8
+ Version: 1.0.0
9
+ License: MIT
10
+ """
11
+
12
+ import os
13
+ import time
14
+ from dataclasses import dataclass
15
+ from typing import Dict, Any, List, Optional
16
+
17
+ try:
18
+ import requests
19
+ except ImportError:
20
+ requests = None
21
+
22
+ from dex_sources import (
23
+ get_chain_config,
24
+ get_dex_factories,
25
+ identify_dex,
26
+ is_base_token,
27
+ PAIR_CREATED_TOPIC,
28
+ )
29
+
30
+
31
+ @dataclass
32
+ class PairCreated:
33
+ """New trading pair event."""
34
+ block_number: int
35
+ tx_hash: str
36
+ timestamp: int
37
+ pair_address: str
38
+ token0: str
39
+ token1: str
40
+ dex: str
41
+ chain: str
42
+ factory_address: str
43
+
44
+
45
+ @dataclass
46
+ class TokenBasicInfo:
47
+ """Basic token info from RPC."""
48
+ address: str
49
+ name: str
50
+ symbol: str
51
+ decimals: int
52
+
53
+
54
+ class EventMonitor:
55
+ """Monitor blockchain events for new pairs."""
56
+
57
+ def __init__(
58
+ self,
59
+ chain: str = "ethereum",
60
+ rpc_url: str = None,
61
+ verbose: bool = False
62
+ ):
63
+ """Initialize event monitor.
64
+
65
+ Args:
66
+ chain: Chain to monitor
67
+ rpc_url: Custom RPC URL
68
+ verbose: Enable verbose output
69
+ """
70
+ self.chain = chain.lower()
71
+ self.config = get_chain_config(chain)
72
+ self.rpc_url = rpc_url or os.environ.get(
73
+ f"{chain.upper()}_RPC_URL",
74
+ self.config.rpc_url
75
+ )
76
+ self.verbose = verbose
77
+ # Use a size-limited cache to prevent memory leaks on long-running instances
78
+ # Keeps most recent 1000 block timestamps
79
+ self._block_cache = {}
80
+ self._block_cache_max_size = 1000
81
+
82
+ def _rpc_call(self, method: str, params: List = None) -> Any:
83
+ """Make JSON-RPC call."""
84
+ if not requests:
85
+ raise ImportError("requests library required")
86
+
87
+ if self.verbose:
88
+ print(f"RPC: {method}")
89
+
90
+ response = requests.post(
91
+ self.rpc_url,
92
+ json={
93
+ "jsonrpc": "2.0",
94
+ "method": method,
95
+ "params": params or [],
96
+ "id": 1,
97
+ },
98
+ headers={"Content-Type": "application/json"},
99
+ timeout=30,
100
+ )
101
+ response.raise_for_status()
102
+
103
+ result = response.json()
104
+ if "error" in result:
105
+ raise Exception(f"RPC error: {result['error']}")
106
+
107
+ return result.get("result")
108
+
109
+ def _get_block_timestamp(self, block_number: int) -> int:
110
+ """Get block timestamp with size-limited caching."""
111
+ if block_number in self._block_cache:
112
+ return self._block_cache[block_number]
113
+
114
+ block = self._rpc_call(
115
+ "eth_getBlockByNumber",
116
+ [hex(block_number), False]
117
+ )
118
+
119
+ if block:
120
+ timestamp = int(block.get("timestamp", "0x0"), 16)
121
+
122
+ # Evict old entries if cache is full
123
+ if len(self._block_cache) >= self._block_cache_max_size:
124
+ # Remove oldest entries (first 100)
125
+ oldest_keys = sorted(self._block_cache.keys())[:100]
126
+ for key in oldest_keys:
127
+ del self._block_cache[key]
128
+
129
+ self._block_cache[block_number] = timestamp
130
+ return timestamp
131
+
132
+ return int(time.time())
133
+
134
+ def get_current_block(self) -> int:
135
+ """Get current block number."""
136
+ result = self._rpc_call("eth_blockNumber")
137
+ return int(result, 16)
138
+
139
+ def get_recent_pairs(
140
+ self,
141
+ hours: int = 24,
142
+ dex: str = None
143
+ ) -> List[PairCreated]:
144
+ """Get recently created pairs.
145
+
146
+ Args:
147
+ hours: Hours to look back
148
+ dex: Filter by specific DEX
149
+
150
+ Returns:
151
+ List of PairCreated events
152
+ """
153
+ current_block = self.get_current_block()
154
+ blocks_per_hour = int(3600 / self.config.block_time)
155
+ from_block = current_block - (blocks_per_hour * hours)
156
+
157
+ factories = get_dex_factories(self.chain)
158
+ factory_addresses = []
159
+
160
+ for name, factory in factories.items():
161
+ if dex is None or dex.lower() in name.lower():
162
+ factory_addresses.append(factory.address)
163
+
164
+ if not factory_addresses:
165
+ return []
166
+
167
+ pairs = []
168
+
169
+ for factory_addr in factory_addresses:
170
+ try:
171
+ logs = self._rpc_call("eth_getLogs", [{
172
+ "fromBlock": hex(from_block),
173
+ "toBlock": "latest",
174
+ "address": factory_addr,
175
+ "topics": [PAIR_CREATED_TOPIC],
176
+ }])
177
+
178
+ for log in logs or []:
179
+ pair = self._parse_pair_created(log, factory_addr)
180
+ if pair:
181
+ pairs.append(pair)
182
+
183
+ except Exception as e:
184
+ if self.verbose:
185
+ print(f"Error fetching logs from {factory_addr}: {e}")
186
+
187
+ # Sort by block number descending (newest first)
188
+ pairs.sort(key=lambda x: x.block_number, reverse=True)
189
+
190
+ return pairs
191
+
192
+ def _parse_pair_created(
193
+ self,
194
+ log: Dict,
195
+ factory_address: str
196
+ ) -> Optional[PairCreated]:
197
+ """Parse PairCreated event log."""
198
+ try:
199
+ block_number = int(log["blockNumber"], 16)
200
+ tx_hash = log["transactionHash"]
201
+ topics = log.get("topics", [])
202
+ data = log.get("data", "0x")
203
+
204
+ # For Uniswap V2 style: topics[1] = token0, topics[2] = token1
205
+ # data contains pair address and pair index
206
+ if len(topics) >= 3:
207
+ token0 = "0x" + topics[1][-40:]
208
+ token1 = "0x" + topics[2][-40:]
209
+
210
+ # Pair address is first 32 bytes of data
211
+ pair_address = "0x" + data[26:66]
212
+
213
+ timestamp = self._get_block_timestamp(block_number)
214
+ dex = identify_dex(self.chain, factory_address)
215
+
216
+ return PairCreated(
217
+ block_number=block_number,
218
+ tx_hash=tx_hash,
219
+ timestamp=timestamp,
220
+ pair_address=pair_address,
221
+ token0=token0,
222
+ token1=token1,
223
+ dex=dex,
224
+ chain=self.chain,
225
+ factory_address=factory_address,
226
+ )
227
+
228
+ except Exception as e:
229
+ if self.verbose:
230
+ print(f"Error parsing log: {e}")
231
+
232
+ return None
233
+
234
+ def get_token_info(self, address: str) -> Optional[TokenBasicInfo]:
235
+ """Get basic token info via RPC calls.
236
+
237
+ Args:
238
+ address: Token contract address
239
+
240
+ Returns:
241
+ TokenBasicInfo or None
242
+ """
243
+ try:
244
+ # ERC20 function signatures
245
+ name_sig = "0x06fdde03" # name()
246
+ symbol_sig = "0x95d89b41" # symbol()
247
+ decimals_sig = "0x313ce567" # decimals()
248
+
249
+ name = self._call_contract(address, name_sig)
250
+ symbol = self._call_contract(address, symbol_sig)
251
+ decimals = self._call_contract(address, decimals_sig)
252
+
253
+ return TokenBasicInfo(
254
+ address=address,
255
+ name=self._decode_string(name) if name else "Unknown",
256
+ symbol=self._decode_string(symbol) if symbol else "???",
257
+ decimals=int(decimals, 16) if decimals else 18,
258
+ )
259
+
260
+ except Exception as e:
261
+ if self.verbose:
262
+ print(f"Error getting token info for {address}: {e}")
263
+ return None
264
+
265
+ def _call_contract(self, address: str, data: str) -> Optional[str]:
266
+ """Make eth_call to contract."""
267
+ try:
268
+ result = self._rpc_call("eth_call", [
269
+ {"to": address, "data": data},
270
+ "latest"
271
+ ])
272
+ return result if result and result != "0x" else None
273
+ except Exception:
274
+ return None
275
+
276
+ def _decode_string(self, data: str) -> str:
277
+ """Decode string from ABI-encoded data."""
278
+ if not data or data == "0x":
279
+ return ""
280
+
281
+ try:
282
+ # Remove 0x prefix
283
+ data = data[2:]
284
+
285
+ # If short string (< 32 bytes, not ABI encoded)
286
+ if len(data) <= 64:
287
+ return bytes.fromhex(data).decode("utf-8", errors="ignore").strip("\x00")
288
+
289
+ # ABI encoded string: offset (32 bytes) + length (32 bytes) + data
290
+ if len(data) >= 128:
291
+ length = int(data[64:128], 16)
292
+ string_data = data[128:128 + length * 2]
293
+ return bytes.fromhex(string_data).decode("utf-8", errors="ignore")
294
+
295
+ return bytes.fromhex(data).decode("utf-8", errors="ignore").strip("\x00")
296
+
297
+ except Exception:
298
+ return "Unknown"
299
+
300
+ def identify_new_token(self, pair: PairCreated) -> str:
301
+ """Identify which token in the pair is the new one.
302
+
303
+ Args:
304
+ pair: PairCreated event
305
+
306
+ Returns:
307
+ Address of the new token
308
+ """
309
+ # If one is a base token (WETH, stablecoin), the other is new
310
+ if is_base_token(self.chain, pair.token0):
311
+ return pair.token1
312
+ if is_base_token(self.chain, pair.token1):
313
+ return pair.token0
314
+
315
+ # Otherwise, consider both as potentially new
316
+ return pair.token0
317
+
318
+
319
+ def main():
320
+ """CLI entry point for testing."""
321
+ monitor = EventMonitor(chain="ethereum", verbose=True)
322
+
323
+ print("=== Recent Pairs (Last 1 Hour) ===")
324
+ pairs = monitor.get_recent_pairs(hours=1)
325
+
326
+ if not pairs:
327
+ print("No new pairs found")
328
+ return
329
+
330
+ for pair in pairs[:10]:
331
+ print(f"\n{pair.dex} on {pair.chain}")
332
+ print(f" Block: {pair.block_number}")
333
+ print(f" Pair: {pair.pair_address[:20]}...")
334
+ print(f" Token0: {pair.token0[:20]}...")
335
+ print(f" Token1: {pair.token1[:20]}...")
336
+
337
+ # Get token info
338
+ new_token = monitor.identify_new_token(pair)
339
+ token_info = monitor.get_token_info(new_token)
340
+ if token_info:
341
+ print(f" New Token: {token_info.symbol} ({token_info.name})")
342
+
343
+
344
+ if __name__ == "__main__":
345
+ main()