| """ |
| CoinStats, Mobula, and CryptoNews DataBus Providers |
| =================================================== |
| Three free/freemium API providers for enhanced crypto intelligence. |
| |
| 1. CoinStats β Per-wallet DeFi resolution across 10K+ protocols |
| 2. Mobula β Long-tail DEX token pricing (10K free credits/month) |
| 3. CryptoNews β Free unlimited news API (no key needed) |
| """ |
|
|
| import logging |
|
|
| logger = logging.getLogger("databus.api_providers") |
|
|
| |
| |
| |
| |
|
|
| COINSTATS_BASE = "https://openapiv1.coinstats.app" |
|
|
|
|
| async def fetch_coinstats_wallet(address: str, chain: str = "ethereum") -> dict: |
| """Resolve a wallet's complete DeFi position β collateral, borrows, LPs, rewards.""" |
| import aiohttp |
|
|
| try: |
| api_key = __import__("os").environ.get("COINSTATS_API_KEY", "") |
| headers = {"X-API-KEY": api_key} if api_key else {} |
|
|
| async with aiohttp.ClientSession() as session: |
| |
| url = f"{COINSTATS_BASE}/wallet/v1/balance/history/{address}" |
| params = {"chain": chain, "limit": 1} |
|
|
| async with session.get( |
| url, params=params, headers=headers, timeout=aiohttp.ClientTimeout(total=15) |
| ) as resp: |
| if resp.status == 200: |
| data = await resp.json() |
| return { |
| "address": address, |
| "chain": chain, |
| "balance": data, |
| "source": "CoinStats (free tier)", |
| "url": "https://coinstats.app/api-docs", |
| } |
|
|
| |
| url2 = f"{COINSTATS_BASE}/defi/v1/positions/{address}" |
| async with session.get( |
| url2, headers=headers, timeout=aiohttp.ClientTimeout(total=15) |
| ) as resp: |
| if resp.status == 200: |
| return { |
| "address": address, |
| "defi_positions": await resp.json(), |
| "source": "CoinStats DeFi (free tier)", |
| } |
|
|
| return { |
| "address": address, |
| "error": f"CoinStats returned {resp.status}", |
| "source": "CoinStats", |
| } |
|
|
| except Exception as e: |
| logger.warning(f"CoinStats fetch failed: {e}") |
| return {"error": str(e), "source": "CoinStats"} |
|
|
|
|
| |
| |
| |
| |
|
|
| MOBULA_BASE = "https://api.mobula.io/api/1" |
|
|
|
|
| async def fetch_mobula_market( |
| asset: str | None = None, blockchain: str | None = None, limit: int = 20 |
| ) -> dict: |
| """Fetch market data from Mobula β covers long-tail tokens missed by CMC/CG.""" |
| import aiohttp |
|
|
| mobula_key = __import__("os").environ.get("MOBULA_API_KEY", "") |
|
|
| try: |
| async with aiohttp.ClientSession() as session: |
| headers = {"Authorization": mobula_key} if mobula_key else {} |
|
|
| |
| url = f"{MOBULA_BASE}/market/multi-data" |
| params = {"limit": limit} |
|
|
| if asset: |
| params["assets"] = asset |
| if blockchain: |
| params["blockchain"] = blockchain |
|
|
| async with session.get( |
| url, params=params, headers=headers, timeout=aiohttp.ClientTimeout(total=15) |
| ) as resp: |
| if resp.status == 200: |
| data = await resp.json() |
| tokens = data.get("data", []) |
| return { |
| "tokens": tokens[:limit], |
| "count": len(tokens), |
| "query": {"asset": asset, "blockchain": blockchain}, |
| "source": "Mobula (free tier β 10K credits/month)", |
| "url": "https://docs.mobula.io", |
| "credits_remaining": resp.headers.get("x-credits-remaining", "unknown"), |
| } |
|
|
| return {"error": f"Mobula returned {resp.status}", "source": "Mobula"} |
|
|
| except Exception as e: |
| logger.warning(f"Mobula fetch failed: {e}") |
| return {"error": str(e), "source": "Mobula"} |
|
|
|
|
| async def fetch_mobula_wallet(address: str, blockchain: str = "ethereum") -> dict: |
| """Fetch wallet portfolio via Mobula β balances, tokens, transaction history.""" |
| import aiohttp |
|
|
| mobula_key = __import__("os").environ.get("MOBULA_API_KEY", "") |
|
|
| try: |
| async with aiohttp.ClientSession() as session: |
| headers = {"Authorization": mobula_key} if mobula_key else {} |
|
|
| url = f"{MOBULA_BASE}/wallet/portfolio" |
| params = {"wallet": address, "blockchain": blockchain} |
|
|
| async with session.get( |
| url, params=params, headers=headers, timeout=aiohttp.ClientTimeout(total=15) |
| ) as resp: |
| if resp.status == 200: |
| data = await resp.json() |
| return { |
| "address": address, |
| "blockchain": blockchain, |
| "portfolio": data.get("data", data), |
| "source": "Mobula Wallet (free tier)", |
| } |
|
|
| return { |
| "address": address, |
| "error": f"Mobula wallet returned {resp.status}", |
| "source": "Mobula", |
| } |
|
|
| except Exception as e: |
| logger.warning(f"Mobula wallet fetch failed: {e}") |
| return {"error": str(e), "source": "Mobula"} |
|
|
|
|
| |
| |
| |
| |
|
|
| CRYPTONEWS_BASE = "https://cryptocurrency.cv/api" |
|
|
|
|
| async def fetch_crypto_news( |
| category: str | None = None, source: str | None = None, limit: int = 20 |
| ) -> dict: |
| """Fetch crypto news from cryptocurrency.cv β free, no key, unlimited.""" |
| import aiohttp |
|
|
| try: |
| async with aiohttp.ClientSession() as session: |
| |
| url = f"{CRYPTONEWS_BASE}/v1/news" |
| params = {"limit": limit} |
| if category: |
| params["category"] = category |
| if source: |
| params["source"] = source |
|
|
| async with session.get( |
| url, params=params, timeout=aiohttp.ClientTimeout(total=15) |
| ) as resp: |
| if resp.status == 200: |
| data = await resp.json() |
| articles = data.get("articles", data.get("news", data.get("data", []))) |
| return { |
| "articles": articles[:limit] if isinstance(articles, list) else [], |
| "count": len(articles) if isinstance(articles, list) else 0, |
| "category": category, |
| "source": "cryptocurrency.cv (free, no API key)", |
| "url": "https://github.com/nirholas/cryptocurrency.cv", |
| "features": "RSS/Atom feeds, JSON REST, MCP server, Python SDK", |
| } |
|
|
| |
| url2 = f"{CRYPTONEWS_BASE}/v1/news/rss" |
| async with session.get(url2, timeout=aiohttp.ClientTimeout(total=10)) as resp: |
| if resp.status == 200: |
| return { |
| "rss": (await resp.text())[:5000], |
| "source": "cryptocurrency.cv RSS (free)", |
| } |
|
|
| return {"error": f"CryptoNews returned {resp.status}", "source": "cryptocurrency.cv"} |
|
|
| except Exception as e: |
| logger.warning(f"CryptoNews fetch failed: {e}") |
| return {"error": str(e), "source": "cryptocurrency.cv"} |
|
|