dexscreener-python 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,11 @@
1
+ """dexscreener-python — async DexScreener API client."""
2
+
3
+ from .client import DexScreenerClient
4
+ from .models import DexPairData
5
+
6
+ __all__ = [
7
+ "DexPairData",
8
+ "DexScreenerClient",
9
+ ]
10
+
11
+ __version__ = "0.1.0"
dexscreener/client.py ADDED
@@ -0,0 +1,458 @@
1
+ """
2
+ Async DexScreener API client.
3
+
4
+ Free public API — no authentication required.
5
+ Rate limit: ~300 req/min (~5/s).
6
+ API docs: https://docs.dexscreener.com/api/reference
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import logging
13
+ import time
14
+ from decimal import Decimal
15
+ from typing import Any
16
+
17
+ import httpx
18
+
19
+ from .models import DexPairData
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ _BASE_URL = "https://api.dexscreener.com"
24
+
25
+ # 429 retry settings
26
+ _MAX_429_RETRIES = 3
27
+ _429_BASE_BACKOFF = 2.0
28
+
29
+
30
+ def _retry_after_seconds(resp: httpx.Response) -> float | None:
31
+ header = resp.headers.get("Retry-After")
32
+ if not header:
33
+ return None
34
+ try:
35
+ return max(0.0, float(header))
36
+ except (TypeError, ValueError):
37
+ return None
38
+
39
+
40
+ def _safe_decimal(val: Any) -> Decimal:
41
+ if val is None:
42
+ return Decimal("0")
43
+ try:
44
+ return Decimal(str(val))
45
+ except Exception:
46
+ return Decimal("0")
47
+
48
+
49
+ def _safe_int(val: Any) -> int:
50
+ try:
51
+ return int(val or 0)
52
+ except (ValueError, TypeError):
53
+ return 0
54
+
55
+
56
+ class DexScreenerClient:
57
+ """Async DexScreener API client with rate limiting and 429 handling.
58
+
59
+ Works on any chain DexScreener supports: solana, ethereum, base, bsc,
60
+ arbitrum, polygon, avalanche, etc.
61
+
62
+ Example::
63
+
64
+ async with DexScreenerClient() as client:
65
+ pairs = await client.get_token_pairs("solana", token_address)
66
+ print(pairs[0].price_usd)
67
+ """
68
+
69
+ __slots__ = (
70
+ "_http",
71
+ "_cache",
72
+ "_cache_ttl",
73
+ "_429_cooldown_until",
74
+ "_429_consecutive",
75
+ "_rate_limit",
76
+ "_rate_tokens",
77
+ "_rate_last_refill",
78
+ )
79
+
80
+ def __init__(
81
+ self,
82
+ *,
83
+ rate_limit: float = 5.0,
84
+ cache_ttl: float = 8.0,
85
+ ) -> None:
86
+ """
87
+ Args:
88
+ rate_limit: Max requests per second (default 5.0).
89
+ cache_ttl: Default cache TTL in seconds.
90
+ """
91
+ self._http: httpx.AsyncClient | None = None
92
+ self._cache: dict[str, tuple[Any, float]] = {} # key → (value, expires_at)
93
+ self._cache_ttl = cache_ttl
94
+ self._429_cooldown_until: float = 0.0
95
+ self._429_consecutive: int = 0
96
+ self._rate_limit = max(rate_limit, 0.1)
97
+ self._rate_tokens = rate_limit
98
+ self._rate_last_refill = time.monotonic()
99
+
100
+ async def __aenter__(self) -> DexScreenerClient:
101
+ await self.startup()
102
+ return self
103
+
104
+ async def __aexit__(self, *exc: Any) -> None:
105
+ await self.shutdown()
106
+
107
+ async def startup(self) -> None:
108
+ self._http = httpx.AsyncClient(
109
+ timeout=httpx.Timeout(10.0),
110
+ headers={"Accept": "application/json"},
111
+ )
112
+
113
+ async def shutdown(self) -> None:
114
+ if self._http:
115
+ await self._http.aclose()
116
+ self._http = None
117
+
118
+ # -- Public API ----------------------------------------------------------
119
+
120
+ async def get_token_pairs(
121
+ self,
122
+ chain: str,
123
+ token_address: str,
124
+ ) -> list[DexPairData]:
125
+ """Get all trading pairs for a token, sorted by liquidity (highest first).
126
+
127
+ Args:
128
+ chain: Chain identifier (e.g., "solana", "ethereum", "base").
129
+ token_address: Token contract/mint address.
130
+ """
131
+ cache_key = f"pairs:{chain}:{token_address}"
132
+ cached = self._cache_get(cache_key)
133
+ if cached is not None:
134
+ return cached
135
+
136
+ data = await self._get_with_retry(
137
+ f"{_BASE_URL}/tokens/v1/{chain}/{token_address}",
138
+ "token_pairs",
139
+ )
140
+ raw_pairs = data if isinstance(data, list) else data.get("pairs", []) if data else []
141
+ pairs = self._parse_pairs(raw_pairs)
142
+ pairs.sort(key=lambda p: p.liquidity_usd, reverse=True)
143
+ self._cache_set(cache_key, pairs)
144
+ return pairs
145
+
146
+ async def get_pair_by_address(
147
+ self,
148
+ chain: str,
149
+ pair_address: str,
150
+ ) -> DexPairData | None:
151
+ """Get data for a specific pair address.
152
+
153
+ Returns None if the pair is not found.
154
+ """
155
+ cache_key = f"pair:{chain}:{pair_address}"
156
+ cached = self._cache_get(cache_key)
157
+ if cached is not None:
158
+ return cached
159
+
160
+ data = await self._get_with_retry(
161
+ f"{_BASE_URL}/pairs/v1/{chain}/{pair_address}",
162
+ "pair",
163
+ )
164
+ raw_pair = (
165
+ data[0]
166
+ if isinstance(data, list) and data
167
+ else data.get("pair", data) if data else None
168
+ )
169
+ if not raw_pair:
170
+ return None
171
+
172
+ pairs = self._parse_pairs([raw_pair])
173
+ result = pairs[0] if pairs else None
174
+ if result:
175
+ self._cache_set(cache_key, result)
176
+ return result
177
+
178
+ async def get_token_price(
179
+ self,
180
+ chain: str,
181
+ token_address: str,
182
+ ) -> Decimal | None:
183
+ """Get the best available USD price for a token.
184
+
185
+ Uses the highest-liquidity pair. Returns None if no price available.
186
+ """
187
+ pairs = await self.get_token_pairs(chain, token_address)
188
+ if not pairs:
189
+ return None
190
+ best = pairs[0]
191
+ return best.price_usd if best.price_usd > 0 else None
192
+
193
+ async def get_token_liquidity(
194
+ self,
195
+ chain: str,
196
+ token_address: str,
197
+ ) -> Decimal:
198
+ """Get total USD liquidity across all pairs for a token."""
199
+ pairs = await self.get_token_pairs(chain, token_address)
200
+ return sum((p.liquidity_usd for p in pairs), Decimal("0"))
201
+
202
+ async def get_tokens_batch(
203
+ self,
204
+ chain: str,
205
+ token_addresses: list[str],
206
+ ) -> dict[str, DexPairData]:
207
+ """Fetch market data for up to 30 tokens in a single request.
208
+
209
+ Returns a dict keyed by token address → best DexPairData (highest liquidity).
210
+ Tokens with no data are absent from the result.
211
+ """
212
+ if not token_addresses:
213
+ return {}
214
+
215
+ BATCH_SIZE = 30
216
+ result: dict[str, DexPairData] = {}
217
+
218
+ for i in range(0, len(token_addresses), BATCH_SIZE):
219
+ batch = token_addresses[i : i + BATCH_SIZE]
220
+ comma_addrs = ",".join(batch)
221
+
222
+ data = await self._get_with_retry(
223
+ f"{_BASE_URL}/tokens/v1/{chain}/{comma_addrs}",
224
+ "batch_tokens",
225
+ )
226
+ raw_pairs = data if isinstance(data, list) else data.get("pairs", []) if data else []
227
+ pairs = self._parse_pairs(raw_pairs)
228
+
229
+ for pair in pairs:
230
+ addr = pair.base_token_address
231
+ if addr and (
232
+ addr not in result
233
+ or pair.liquidity_usd > result[addr].liquidity_usd
234
+ ):
235
+ result[addr] = pair
236
+
237
+ return result
238
+
239
+ async def search_pairs(self, query: str) -> list[DexPairData]:
240
+ """Search for pairs by token name, symbol, or address."""
241
+ data = await self._get_with_retry(
242
+ f"{_BASE_URL}/latest/dex/search",
243
+ "search",
244
+ params={"q": query},
245
+ )
246
+ raw_pairs = data.get("pairs", []) if data else []
247
+ return self._parse_pairs(raw_pairs)
248
+
249
+ async def get_boosted_tokens(
250
+ self,
251
+ chain: str | None = None,
252
+ ) -> list[dict]:
253
+ """Get currently boosted (promoted) tokens.
254
+
255
+ Boosted tokens are paid promotions — treat with caution.
256
+
257
+ Args:
258
+ chain: Optional chain filter (e.g., "solana"). If None, returns all chains.
259
+ """
260
+ cache_key = f"boosted:{chain or 'all'}"
261
+ cached = self._cache_get(cache_key)
262
+ if cached is not None:
263
+ return cached
264
+
265
+ data = await self._get_with_retry(
266
+ f"{_BASE_URL}/token-boosts/latest/v1",
267
+ "boosted",
268
+ )
269
+ items = data if isinstance(data, list) else []
270
+ if chain:
271
+ items = [item for item in items if item.get("chainId") == chain]
272
+
273
+ self._cache_set(cache_key, items, ttl=30.0)
274
+ return items
275
+
276
+ async def is_token_boosted(
277
+ self,
278
+ chain: str,
279
+ token_address: str,
280
+ ) -> bool:
281
+ """Check if a token is currently boosted (paid promotion)."""
282
+ boosted = await self.get_boosted_tokens(chain)
283
+ return any(
284
+ b.get("tokenAddress", "").lower() == token_address.lower()
285
+ for b in boosted
286
+ )
287
+
288
+ # -- Internal helpers ----------------------------------------------------
289
+
290
+ async def _wait_429_cooldown(self) -> None:
291
+ now = time.time()
292
+ if now < self._429_cooldown_until:
293
+ wait = self._429_cooldown_until - now
294
+ logger.info("dexscreener 429 cooldown: waiting %.1fs", wait)
295
+ await asyncio.sleep(wait)
296
+
297
+ def _trigger_429_cooldown(self, endpoint: str) -> None:
298
+ self._429_consecutive += 1
299
+ backoff = 3.0 * (2 ** min(self._429_consecutive - 1, 3))
300
+ self._429_cooldown_until = time.time() + backoff
301
+ logger.warning(
302
+ "dexscreener 429 cooldown set: endpoint=%s consecutive=%d backoff=%.1fs",
303
+ endpoint,
304
+ self._429_consecutive,
305
+ backoff,
306
+ )
307
+
308
+ def _clear_429_cooldown(self) -> None:
309
+ if self._429_consecutive > 0:
310
+ self._429_consecutive = 0
311
+
312
+ async def _rate_limit_acquire(self) -> None:
313
+ """Simple token bucket rate limiter."""
314
+ now = time.monotonic()
315
+ elapsed = now - self._rate_last_refill
316
+ self._rate_tokens = min(
317
+ self._rate_limit,
318
+ self._rate_tokens + elapsed * self._rate_limit,
319
+ )
320
+ self._rate_last_refill = now
321
+
322
+ if self._rate_tokens < 1.0:
323
+ wait = (1.0 - self._rate_tokens) / self._rate_limit
324
+ await asyncio.sleep(wait)
325
+ self._rate_tokens = 0.0
326
+ else:
327
+ self._rate_tokens -= 1.0
328
+
329
+ async def _get_with_retry(
330
+ self,
331
+ url: str,
332
+ endpoint: str,
333
+ params: dict[str, Any] | None = None,
334
+ ) -> Any:
335
+ """GET with 429 retry + global cooldown."""
336
+ if self._http is None:
337
+ raise RuntimeError("DexScreenerClient.startup() not called")
338
+
339
+ await self._wait_429_cooldown()
340
+
341
+ for attempt in range(_MAX_429_RETRIES + 1):
342
+ await self._rate_limit_acquire()
343
+
344
+ try:
345
+ resp = await self._http.get(url, params=params)
346
+
347
+ if resp.status_code == 429:
348
+ self._trigger_429_cooldown(endpoint)
349
+ backoff = _429_BASE_BACKOFF * (2 ** attempt)
350
+ logger.warning(
351
+ "dexscreener 429: endpoint=%s attempt=%d backoff=%.1fs",
352
+ endpoint,
353
+ attempt + 1,
354
+ backoff,
355
+ )
356
+ await asyncio.sleep(backoff)
357
+ continue
358
+
359
+ self._clear_429_cooldown()
360
+
361
+ if resp.status_code != 200:
362
+ logger.warning(
363
+ "dexscreener HTTP %d: endpoint=%s",
364
+ resp.status_code,
365
+ endpoint,
366
+ )
367
+ return None
368
+
369
+ return resp.json()
370
+
371
+ except Exception as exc:
372
+ logger.warning(
373
+ "dexscreener request failed: endpoint=%s error=%s",
374
+ endpoint,
375
+ exc,
376
+ )
377
+ return None
378
+
379
+ logger.warning(
380
+ "dexscreener 429 exhausted retries: endpoint=%s",
381
+ endpoint,
382
+ )
383
+ return None
384
+
385
+ # -- Cache ---------------------------------------------------------------
386
+
387
+ def _cache_get(self, key: str) -> Any:
388
+ entry = self._cache.get(key)
389
+ if entry is None:
390
+ return None
391
+ value, expires_at = entry
392
+ if time.monotonic() > expires_at:
393
+ del self._cache[key]
394
+ return None
395
+ return value
396
+
397
+ def _cache_set(self, key: str, value: Any, ttl: float | None = None) -> None:
398
+ self._cache[key] = (value, time.monotonic() + (ttl or self._cache_ttl))
399
+
400
+ # -- Parsing -------------------------------------------------------------
401
+
402
+ def _parse_pairs(self, raw_pairs: list[dict]) -> list[DexPairData]:
403
+ """Parse raw pair data into structured DexPairData objects."""
404
+ pairs: list[DexPairData] = []
405
+
406
+ for raw in raw_pairs:
407
+ if not isinstance(raw, dict):
408
+ continue
409
+
410
+ base = raw.get("baseToken", {})
411
+ quote = raw.get("quoteToken", {})
412
+ volume = raw.get("volume", {})
413
+ price_change = raw.get("priceChange", {})
414
+ txns = raw.get("txns", {})
415
+ liquidity = raw.get("liquidity", {})
416
+
417
+ txns_5m = txns.get("m5", {})
418
+ txns_1h = txns.get("h1", {})
419
+ txns_24h = txns.get("h24", {})
420
+
421
+ pair = DexPairData(
422
+ chain_id=raw.get("chainId", ""),
423
+ dex_id=raw.get("dexId", ""),
424
+ pair_address=raw.get("pairAddress", ""),
425
+ base_token_address=base.get("address", ""),
426
+ base_token_symbol=base.get("symbol", ""),
427
+ base_token_name=base.get("name", ""),
428
+ quote_token_address=quote.get("address", ""),
429
+ quote_token_symbol=quote.get("symbol", ""),
430
+ price_usd=_safe_decimal(raw.get("priceUsd")),
431
+ price_native=_safe_decimal(raw.get("priceNative")),
432
+ market_cap=_safe_decimal(raw.get("marketCap")),
433
+ fdv=_safe_decimal(raw.get("fdv")),
434
+ liquidity_usd=_safe_decimal(liquidity.get("usd")),
435
+ liquidity_base=_safe_decimal(liquidity.get("base")),
436
+ liquidity_quote=_safe_decimal(liquidity.get("quote")),
437
+ volume_5m=_safe_decimal(volume.get("m5")),
438
+ volume_1h=_safe_decimal(volume.get("h1")),
439
+ volume_6h=_safe_decimal(volume.get("h6")),
440
+ volume_24h=_safe_decimal(volume.get("h24")),
441
+ price_change_5m=_safe_decimal(price_change.get("m5")),
442
+ price_change_1h=_safe_decimal(price_change.get("h1")),
443
+ price_change_6h=_safe_decimal(price_change.get("h6")),
444
+ price_change_24h=_safe_decimal(price_change.get("h24")),
445
+ buys_5m=_safe_int(txns_5m.get("buys")),
446
+ sells_5m=_safe_int(txns_5m.get("sells")),
447
+ buys_1h=_safe_int(txns_1h.get("buys")),
448
+ sells_1h=_safe_int(txns_1h.get("sells")),
449
+ buys_24h=_safe_int(txns_24h.get("buys")),
450
+ sells_24h=_safe_int(txns_24h.get("sells")),
451
+ pair_created_at=_safe_int(raw.get("pairCreatedAt")),
452
+ is_boosted=bool(raw.get("boosts")),
453
+ url=raw.get("url", ""),
454
+ raw=raw,
455
+ )
456
+ pairs.append(pair)
457
+
458
+ return pairs
dexscreener/models.py ADDED
@@ -0,0 +1,92 @@
1
+ """DexPairData model — typed representation of a DexScreener trading pair."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from dataclasses import dataclass, field
7
+ from decimal import Decimal
8
+ from typing import Any
9
+
10
+
11
+ @dataclass(slots=True)
12
+ class DexPairData:
13
+ """Parsed pair data from DexScreener."""
14
+
15
+ # Identity
16
+ chain_id: str = ""
17
+ dex_id: str = ""
18
+ pair_address: str = ""
19
+ base_token_address: str = ""
20
+ base_token_symbol: str = ""
21
+ base_token_name: str = ""
22
+ quote_token_address: str = ""
23
+ quote_token_symbol: str = ""
24
+
25
+ # Price
26
+ price_usd: Decimal = Decimal("0")
27
+ price_native: Decimal = Decimal("0")
28
+
29
+ # Market data
30
+ market_cap: Decimal = Decimal("0")
31
+ fdv: Decimal = Decimal("0")
32
+ liquidity_usd: Decimal = Decimal("0")
33
+ liquidity_base: Decimal = Decimal("0")
34
+ liquidity_quote: Decimal = Decimal("0")
35
+
36
+ # Volume (USD)
37
+ volume_5m: Decimal = Decimal("0")
38
+ volume_1h: Decimal = Decimal("0")
39
+ volume_6h: Decimal = Decimal("0")
40
+ volume_24h: Decimal = Decimal("0")
41
+
42
+ # Price changes (percentage)
43
+ price_change_5m: Decimal = Decimal("0")
44
+ price_change_1h: Decimal = Decimal("0")
45
+ price_change_6h: Decimal = Decimal("0")
46
+ price_change_24h: Decimal = Decimal("0")
47
+
48
+ # Transactions
49
+ buys_5m: int = 0
50
+ sells_5m: int = 0
51
+ buys_1h: int = 0
52
+ sells_1h: int = 0
53
+ buys_24h: int = 0
54
+ sells_24h: int = 0
55
+
56
+ # Age
57
+ pair_created_at: int = 0 # Unix timestamp ms
58
+
59
+ # Metadata
60
+ is_boosted: bool = False
61
+ url: str = ""
62
+
63
+ # Raw response
64
+ raw: dict = field(default_factory=dict)
65
+
66
+ @property
67
+ def buy_sell_ratio_5m(self) -> float:
68
+ """Buy/sell ratio over 5 minutes. >1 = more buying."""
69
+ total = self.buys_5m + self.sells_5m
70
+ if total == 0:
71
+ return 1.0
72
+ return self.buys_5m / max(self.sells_5m, 1)
73
+
74
+ @property
75
+ def buy_sell_ratio_1h(self) -> float:
76
+ """Buy/sell ratio over 1 hour."""
77
+ total = self.buys_1h + self.sells_1h
78
+ if total == 0:
79
+ return 1.0
80
+ return self.buys_1h / max(self.sells_1h, 1)
81
+
82
+ @property
83
+ def has_liquidity(self) -> bool:
84
+ """Token has meaningful liquidity (>$1000)."""
85
+ return self.liquidity_usd > Decimal("1000")
86
+
87
+ @property
88
+ def pair_age_seconds(self) -> int:
89
+ """How old the pair is in seconds."""
90
+ if self.pair_created_at <= 0:
91
+ return 0
92
+ return max(0, int(time.time()) - (self.pair_created_at // 1000))
dexscreener/py.typed ADDED
File without changes
@@ -0,0 +1,208 @@
1
+ Metadata-Version: 2.4
2
+ Name: dexscreener-python
3
+ Version: 0.1.0
4
+ Summary: Async DexScreener API client with rate limiting, 429 retry, and response caching.
5
+ Project-URL: Homepage, https://github.com/JinUltimate1995/dexscreener-python
6
+ Project-URL: Repository, https://github.com/JinUltimate1995/dexscreener-python
7
+ Project-URL: Issues, https://github.com/JinUltimate1995/dexscreener-python/issues
8
+ Author: JinUltimate
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: api,crypto,defi,dex,dexscreener,ethereum,solana,trading
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Framework :: AsyncIO
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Libraries
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: httpx>=0.24.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: mypy>=1.0; extra == 'dev'
25
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
26
+ Requires-Dist: pytest>=7.0; extra == 'dev'
27
+ Requires-Dist: respx>=0.21; extra == 'dev'
28
+ Requires-Dist: ruff>=0.4; extra == 'dev'
29
+ Description-Content-Type: text/markdown
30
+
31
+ <div align="center">
32
+ <h1>dexscreener-python</h1>
33
+ <p><strong>async dexscreener api client for python.</strong></p>
34
+ <p>rate limiting. 429 retry. response caching. request dedup. works on any chain.</p>
35
+
36
+ <br/>
37
+
38
+ <a href="https://github.com/JinUltimate1995/dexscreener-python/actions"><img src="https://img.shields.io/github/actions/workflow/status/JinUltimate1995/dexscreener-python/ci.yml?branch=main&style=flat-square&label=tests" /></a>
39
+ <a href="https://pypi.org/project/dexscreener-python/"><img src="https://img.shields.io/pypi/v/dexscreener-python?style=flat-square" /></a>
40
+ <img src="https://img.shields.io/pypi/pyversions/dexscreener-python?style=flat-square" />
41
+ <img src="https://img.shields.io/badge/typed-py.typed-blue?style=flat-square" />
42
+ <img src="https://img.shields.io/badge/async-first-blue?style=flat-square" />
43
+ <img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" />
44
+ </div>
45
+
46
+ ---
47
+
48
+ > extracted from a production trading system. handles rate limits gracefully.
49
+
50
+ ## 🛡️ Why this library?
51
+
52
+ dexscreener's api is free and powerful, but:
53
+
54
+ - **429s kill your app** if you don't handle them
55
+ - **no python client exists** that handles rate limits properly
56
+ - **concurrent requests** for the same token waste your quota
57
+
58
+ this library solves all three:
59
+
60
+ ```python
61
+ from dexscreener import DexScreenerClient
62
+
63
+ async with DexScreenerClient() as client:
64
+ # get all trading pairs for a token
65
+ pairs = await client.get_token_pairs("solana", "So11111111111111111111111111111111111111112")
66
+
67
+ for pair in pairs:
68
+ print(f"{pair.base_token_symbol}: ${pair.price_usd} | liq: ${pair.liquidity_usd}")
69
+ ```
70
+
71
+ 429s → automatic exponential backoff (3s → 6s → 12s).
72
+ duplicate requests → deduplicated into one HTTP call.
73
+ results → cached with configurable TTL.
74
+
75
+ ## 📦 Install
76
+
77
+ ```bash
78
+ pip install dexscreener-python
79
+ ```
80
+
81
+ requires python 3.11+
82
+
83
+ ## ⚡ Features
84
+
85
+ | feature | description |
86
+ |---|---|
87
+ | **any chain** | solana, ethereum, base, bsc, arbitrum — anything dexscreener supports |
88
+ | **adaptive rate limiting** | token bucket that backs off on 429 and recovers after success |
89
+ | **global 429 cooldown** | one 429 pauses ALL requests briefly (prevents request storms) |
90
+ | **response cache** | configurable TTL per endpoint. concurrent calls share one request. |
91
+ | **batch support** | fetch up to 30 tokens in a single request |
92
+ | **typed data** | `DexPairData` dataclass with computed properties (buy/sell ratio, age, etc.) |
93
+
94
+ ## 🔧 Usage
95
+
96
+ ### 💰 Token Pairs
97
+
98
+ ```python
99
+ pairs = await client.get_token_pairs("solana", token_address)
100
+
101
+ best_pair = pairs[0] # sorted by liquidity (highest first)
102
+ print(f"Price: ${best_pair.price_usd}")
103
+ print(f"Liquidity: ${best_pair.liquidity_usd}")
104
+ print(f"24h Volume: ${best_pair.volume_24h}")
105
+ print(f"Buy/Sell 5m: {best_pair.buy_sell_ratio_5m:.2f}")
106
+ print(f"Age: {best_pair.pair_age_seconds // 3600}h")
107
+ ```
108
+
109
+ ### 💵 Token Price
110
+
111
+ ```python
112
+ price = await client.get_token_price("solana", token_address)
113
+ print(f"${price}")
114
+ ```
115
+
116
+ ### 📦 Batch Fetch (up to 30 tokens)
117
+
118
+ ```python
119
+ data = await client.get_tokens_batch("solana", [addr1, addr2, addr3])
120
+ for addr, pair in data.items():
121
+ print(f"{pair.base_token_symbol}: ${pair.price_usd}")
122
+ ```
123
+
124
+ ### 🔍 Search
125
+
126
+ ```python
127
+ results = await client.search_pairs("BONK")
128
+ for pair in results:
129
+ print(f"{pair.base_token_symbol} on {pair.dex_id}: ${pair.price_usd}")
130
+ ```
131
+
132
+ ### 🚀 Boosted Tokens
133
+
134
+ ```python
135
+ boosted = await client.get_boosted_tokens("solana")
136
+ # boosted tokens are paid promotions — treat with caution
137
+ ```
138
+
139
+ ### 🎯 Specific Pair
140
+
141
+ ```python
142
+ pair = await client.get_pair_by_address("solana", pair_address)
143
+ print(f"{pair.dex_id}: ${pair.price_usd}")
144
+ ```
145
+
146
+ ## 📋 DexPairData
147
+
148
+ all methods return `DexPairData` objects with these fields:
149
+
150
+ | field | type | description |
151
+ |---|---|---|
152
+ | `chain_id` | str | chain identifier |
153
+ | `dex_id` | str | dex identifier (raydium, uniswap_v3, etc.) |
154
+ | `pair_address` | str | pair contract address |
155
+ | `base_token_address` | str | base token mint/address |
156
+ | `base_token_symbol` | str | token symbol |
157
+ | `price_usd` | Decimal | current price in USD |
158
+ | `price_native` | Decimal | price in native token (SOL, ETH, etc.) |
159
+ | `liquidity_usd` | Decimal | total liquidity in USD |
160
+ | `volume_5m / 1h / 6h / 24h` | Decimal | volume by timeframe |
161
+ | `price_change_5m / 1h / 6h / 24h` | Decimal | price change % |
162
+ | `buys_5m / 1h / 24h` | int | buy transactions |
163
+ | `sells_5m / 1h / 24h` | int | sell transactions |
164
+ | `pair_created_at` | int | unix timestamp (ms) |
165
+
166
+ computed properties:
167
+
168
+ | property | description |
169
+ |---|---|
170
+ | `buy_sell_ratio_5m` | buy/sell ratio over 5 minutes (>1 = more buying) |
171
+ | `buy_sell_ratio_1h` | buy/sell ratio over 1 hour |
172
+ | `has_liquidity` | True if liquidity > $1000 |
173
+ | `pair_age_seconds` | pair age in seconds |
174
+
175
+ ## ⚙️ Configuration
176
+
177
+ ```python
178
+ client = DexScreenerClient(
179
+ rate_limit=5.0, # requests/second (default: 5.0, dexscreener limit is ~300/min)
180
+ cache_ttl=8.0, # default cache TTL in seconds
181
+ )
182
+ ```
183
+
184
+ ---
185
+
186
+ ## 🆚 Comparison
187
+
188
+ | | dexscreener-python | raw `httpx` / `requests` |
189
+ |---|---|---|
190
+ | Rate limiting | ✅ adaptive token bucket | ❌ manual |
191
+ | 429 retry | ✅ exponential backoff | ❌ crash |
192
+ | Global cooldown | ✅ one 429 pauses all | ❌ per-request only |
193
+ | Response cache | ✅ TTL + dedup | ❌ manual |
194
+ | Typed data | ✅ `DexPairData` dataclass | ❌ raw dicts |
195
+ | Batch support | ✅ up to 30 tokens | ❌ manual loop |
196
+ | Async | ✅ native | depends |
197
+
198
+ ## License
199
+
200
+ MIT
201
+
202
+ ---
203
+
204
+ ## 📦 Also by JinUltimate1995
205
+
206
+ - **[jupiter-swap-python](https://github.com/JinUltimate1995/jupiter-swap-python)** — Jupiter swap client for Python. Async. Typed.
207
+ - **[pumpfun-python](https://github.com/JinUltimate1995/pumpfun-python)** — PumpFun bonding curve + PumpSwap AMM. Direct swaps from Python.
208
+ - **[solana-rpc-resilient](https://github.com/JinUltimate1995/solana-rpc-resilient)** — Fault-tolerant Solana RPC with automatic failover.
@@ -0,0 +1,8 @@
1
+ dexscreener/__init__.py,sha256=b9cOxsJdhv41bgyCC5nUDGJss5JbZLlhKjC7ZrvUBVY,212
2
+ dexscreener/client.py,sha256=P24VzaLKR9U1lN51ffqfqNYWoOum0Axia2PST_z54T4,14937
3
+ dexscreener/models.py,sha256=bo-NOqb7PCxfXgdxFbbPdSKR8oOgKg7N4X1iCwyDZiQ,2489
4
+ dexscreener/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ dexscreener_python-0.1.0.dist-info/METADATA,sha256=xX1apb1FTeWrwJjjzkLD5WeLghdADgOcaNUkVxDrG3I,7271
6
+ dexscreener_python-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
7
+ dexscreener_python-0.1.0.dist-info/licenses/LICENSE,sha256=QMTxmT1VJd2-bhYmxC2QwPx9CoAXNPflIa8uhLdG2nE,1068
8
+ dexscreener_python-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 JinUltimate
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.