211 lines
8.8 KiB
Python
211 lines
8.8 KiB
Python
"""
|
|
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")
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# 1. COINSTATS - Per-Wallet DeFi Resolution
|
|
# Free tier: public API, no key needed for basic endpoints
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
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:
|
|
# Wallet balance + DeFi positions
|
|
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",
|
|
}
|
|
|
|
# Fallback: try DeFi-specific endpoint
|
|
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"}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# 2. MOBULA - Long-tail DEX Token Pricing
|
|
# Free tier: 10,000 credits/month, no rate limit
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
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 {}
|
|
|
|
# Multi-data market endpoint
|
|
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"}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# 3. CRYPTONEWS (cryptocurrency.cv) - Free unlimited news API
|
|
# No API key, no rate limits, REST + RSS
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
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:
|
|
# Try REST API
|
|
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",
|
|
}
|
|
|
|
# Fallback: try RSS feed
|
|
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"}
|