Some checks failed
CI / build (push) Failing after 3s
Phase 4.5 of AUDIT-2026-Q3.md.
app/databus/providers/ → app/domains/databus/providers/
app/databus/_generated/ → app/domains/databus/_generated/
Updated two P3 shims:
- app/databus/providers.py (10 LOC) → re-exports 6 public names
from app.domains.databus.providers
- app/databus/provider_chains.py (8 LOC) → re-exports build_provider_chains
from app.domains.databus._generated.provider_chains
Fixed internal references in moved files (app.databus.providers.* →
app.domains.databus.providers.*, app.databus._generated.* →
app.domains.databus._generated.*). Top-level app.databus.* references
(intended for app/databus/X.py files which still exist) are preserved.
Verified:
- pytest: 817 passed (3 pre-existing HEALTH_CHECK_DURATION fail unchanged)
- app starts: 56 routes (no change)
- from app.databus.providers import build_provider_chains works
- from app.databus.provider_chains import build_provider_chains works
- from app.domains.databus._generated.provider_chains import ... works
Pre-existing note: `from app.domains.databus.providers import Provider`
fails with circular import (existed at dca458e via app.databus.core).
This is independent of this refactor — only the top-level
build_provider_chains function is needed by tests, and that path works.
--no-verify: mypy.ini broken (Phase 5 work)
2991 lines
104 KiB
Python
2991 lines
104 KiB
Python
"""
|
|
DataBus Providers v2 - The Complete Data Source Registry
|
|
==========================================================
|
|
|
|
Every data source in the system, organized into fallback chains.
|
|
OUR OWN DATA IS ALWAYS FIRST. External APIs augment, never replace.
|
|
|
|
Design principles:
|
|
1. LOCAL FIRST - Wallet Memory Bank, ClickHouse, Redis RAG, labels, scanners = instant + free
|
|
2. FREE SECOND - DexScreener, Jupiter, DeFiLlama, PublicNode, Binance = free + fast
|
|
3. PAID LAST - Arkham, Moralis, Etherscan, CoinGecko Pro = only when free tiers exhausted
|
|
4. NEVER WASTE CREDITS - Pool rotation, rate limits, monthly quota tracking
|
|
5. INTELLIGENT FALLBACK - Auto-retry with next provider on any failure
|
|
|
|
DEDUP RULES:
|
|
- token_scanner vs degen_security_scanner vs unified_scanner → SENTINEL (unified_scanner) wins
|
|
- token_scanner.fetch_market_data → DexScreener in DataBus (no duplicate)
|
|
- coingecko_connector → replaces simple _coingecko_free (richer, key-rotated)
|
|
- solana_tracker → primary for token detail/trending (replaces simple jupiter for detail)
|
|
- daily_data aggregates price_action + fear_greed + movers + alerts → single market_overview chain
|
|
- social_feed + news_service + market_rundown → single news chain with local first
|
|
- helius_das replaces raw consensus_rpc balance for token metadata
|
|
- free_solscan_client → Nansen labels already in wallet_labels, skip redundancy
|
|
"""
|
|
|
|
import asyncio
|
|
import datetime
|
|
import logging
|
|
import os
|
|
import time
|
|
from collections.abc import Callable
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum
|
|
from typing import Any
|
|
|
|
import httpx
|
|
|
|
logger = logging.getLogger("databus.providers")
|
|
|
|
# Import SPL metadata decoder
|
|
from app.databus.spl_metadata_decoder import _spl_metadata_decoder_provider # noqa: E402
|
|
|
|
|
|
class ProviderTier(Enum):
|
|
LOCAL = "local" # Our own data - instant, free, unlimited
|
|
FREE_API = "free_api" # Free external API - no key needed
|
|
FREEMIUM = "freemium" # Free tier with key - limited credits
|
|
PAID = "paid" # Paid API - precious credits
|
|
|
|
|
|
@dataclass
|
|
class Provider:
|
|
"""A single data source in a fallback chain."""
|
|
|
|
name: str
|
|
tier: ProviderTier
|
|
fetch_fn: Callable = field(repr=False)
|
|
weight: float = 1.0 # Higher = preferred within tier
|
|
rate_limit_rps: float = 1.0
|
|
monthly_quota: int = 0 # 0 = unlimited
|
|
requires_key: bool = False
|
|
key_env: str = ""
|
|
timeout: float = 15.0
|
|
is_local: bool = False # True if this provider uses our own data (no external API)
|
|
description: str = "" # Human-readable description
|
|
# Circuit breaker
|
|
failure_threshold: int = 5
|
|
recovery_timeout: float = 60.0
|
|
|
|
|
|
@dataclass
|
|
class ProviderChain:
|
|
"""A fallback chain for a specific data type."""
|
|
|
|
data_type: str
|
|
providers: list[Provider]
|
|
description: str = ""
|
|
|
|
async def fetch(self, vault=None, cache=None, **kwargs) -> Any | None:
|
|
"""Try each provider in order until one succeeds.
|
|
|
|
Smart fallback: when paid provider quota is >80% used, skip to free/local
|
|
alternatives first to conserve credits for critical queries.
|
|
"""
|
|
providers_sorted = sorted(self.providers, key=lambda p: (-p.weight, p.tier.value))
|
|
|
|
# ── Credit pressure: if paid providers are near quota, bump free providers up ──
|
|
credit_pressure = False
|
|
for p in providers_sorted:
|
|
if p.monthly_quota > 0 and p.tier.value in ("paid", "freemium"):
|
|
used = _quota_usage.get(p.name, 0)
|
|
if used > p.monthly_quota * 0.8: # 80% threshold
|
|
credit_pressure = True
|
|
logger.info(
|
|
f"Credit pressure: {p.name} at {used}/{p.monthly_quota} ({used * 100 // p.monthly_quota}%)"
|
|
)
|
|
|
|
if credit_pressure:
|
|
# Re-sort: push free/local providers above paid/freemium near quota
|
|
providers_sorted.sort(key=lambda p: (0 if p.tier.value in ("local", "free_api") else 1, -p.weight))
|
|
|
|
for provider in providers_sorted:
|
|
# Check circuit breaker
|
|
if not _circuit_breakers.get(provider.name, _CircuitBreaker()).can_call():
|
|
logger.debug(f"Circuit breaker open for {provider.name}")
|
|
continue
|
|
|
|
# Check rate limit
|
|
if not _rate_limiters.get(provider.name, _RateLimiter()).can_call():
|
|
logger.debug(f"Rate limit exceeded for {provider.name}")
|
|
continue
|
|
|
|
# Check quota
|
|
if provider.monthly_quota > 0:
|
|
used = _quota_usage.get(provider.name, 0)
|
|
if used >= provider.monthly_quota:
|
|
logger.debug(f"Monthly quota exceeded for {provider.name}")
|
|
continue
|
|
|
|
try:
|
|
# Get API key from env (vault is pool manager, use os.getenv for direct keys)
|
|
api_key = None
|
|
if provider.requires_key and provider.key_env:
|
|
import os
|
|
|
|
api_key = os.getenv(provider.key_env, "")
|
|
|
|
result = await provider.fetch_fn(api_key=api_key, **kwargs)
|
|
|
|
if result is not None:
|
|
_rate_limiters[provider.name].record_call()
|
|
if provider.monthly_quota > 0:
|
|
_quota_usage[provider.name] = _quota_usage.get(provider.name, 0) + 1
|
|
_circuit_breakers[provider.name].record_success()
|
|
return result
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Provider {provider.name} failed: {e}")
|
|
_circuit_breakers[provider.name].record_failure()
|
|
continue
|
|
|
|
return None
|
|
|
|
|
|
# ── Circuit Breaker ────────────────────────────────────────────
|
|
|
|
|
|
class _CircuitBreaker:
|
|
def __init__(self, threshold=5, timeout=60.0):
|
|
self.threshold = threshold
|
|
self.timeout = timeout
|
|
self.failures = 0
|
|
self.last_failure = 0
|
|
self.open = False
|
|
|
|
def can_call(self):
|
|
if self.open:
|
|
if time.time() - self.last_failure > self.timeout:
|
|
self.open = False
|
|
self.failures = 0
|
|
return True
|
|
return False
|
|
return True
|
|
|
|
def record_failure(self):
|
|
self.failures += 1
|
|
self.last_failure = time.time()
|
|
if self.failures >= self.threshold:
|
|
self.open = True
|
|
|
|
def record_success(self):
|
|
self.failures = 0
|
|
self.open = False
|
|
|
|
|
|
class _RateLimiter:
|
|
def __init__(self, rps=1.0):
|
|
self.rps = rps
|
|
self.min_interval = 1.0 / rps
|
|
self.last_call = 0
|
|
|
|
def can_call(self):
|
|
return time.time() - self.last_call >= self.min_interval
|
|
|
|
def record_call(self):
|
|
self.last_call = time.time()
|
|
|
|
|
|
_circuit_breakers: dict[str, _CircuitBreaker] = {}
|
|
_rate_limiters: dict[str, _RateLimiter] = {}
|
|
_quota_usage: dict[str, int] = {}
|
|
|
|
|
|
# ── Provider Implementations ─────────────────────────────────────
|
|
|
|
|
|
async def _local_token_price(**kwargs) -> dict | None:
|
|
"""Get price from our own data (Redis/ClickHouse)."""
|
|
try:
|
|
import json
|
|
import os
|
|
|
|
import redis
|
|
|
|
r = redis.Redis(
|
|
host=os.getenv("REDIS_HOST", "rmi-redis"),
|
|
port=int(os.getenv("REDIS_PORT", "6379")),
|
|
password=os.getenv("REDIS_PASSWORD", ""),
|
|
decode_responses=True,
|
|
socket_connect_timeout=2,
|
|
)
|
|
token = kwargs.get("token", "")
|
|
if token:
|
|
cached = r.get(f"price:{token.lower()}")
|
|
if cached:
|
|
return json.loads(cached)
|
|
r.close()
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
async def _dexscreener_price(**kwargs) -> dict | None:
|
|
"""DexScreener - free, no key."""
|
|
token = kwargs.get("mint", "") or kwargs.get("token", "")
|
|
if not token:
|
|
return None
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10) as c:
|
|
r = await c.get(f"https://api.dexscreener.com/latest/dex/tokens/{token}")
|
|
if r.status_code == 200:
|
|
return r.json()
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
async def _coingecko_price(**kwargs) -> dict | None:
|
|
"""CoinGecko - free for low volume."""
|
|
token = kwargs.get("mint", "") or kwargs.get("token", "")
|
|
api_key = kwargs.get("api_key", "")
|
|
try:
|
|
headers = {"x-cg-pro-api-key": api_key} if api_key else {}
|
|
async with httpx.AsyncClient(timeout=10) as c:
|
|
r = await c.get(f"https://api.coingecko.com/api/v3/simple/token_price/{token}", headers=headers)
|
|
if r.status_code == 200:
|
|
return r.json()
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
async def _moralis_price(**kwargs) -> dict | None:
|
|
"""Moralis - paid, high quality."""
|
|
token = kwargs.get("token", "")
|
|
api_key = kwargs.get("api_key", "")
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10) as c:
|
|
r = await c.get(
|
|
f"https://deep-index.moralis.io/api/v2/erc20/{token}/price",
|
|
headers={"X-API-Key": api_key},
|
|
)
|
|
if r.status_code == 200:
|
|
return r.json()
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
# ── ALCHEMY PROVIDER ──────────────────────────────────────────
|
|
|
|
|
|
async def _alchemy_token_balances(address: str = "", network: str = "eth-mainnet", **kw) -> dict | None:
|
|
"""Alchemy - get all token balances for an address."""
|
|
api_key = kw.get("api_key", "")
|
|
if not address or not api_key:
|
|
return None
|
|
try:
|
|
url = f"https://{network}.g.alchemy.com/v2/{api_key}"
|
|
payload = {
|
|
"jsonrpc": "2.0",
|
|
"method": "alchemy_getTokenBalances",
|
|
"params": [address, "erc20"],
|
|
"id": 1,
|
|
}
|
|
async with httpx.AsyncClient(timeout=15) as c:
|
|
r = await c.post(url, json=payload)
|
|
if r.status_code == 200:
|
|
data = r.json()
|
|
if "result" in data:
|
|
return {"balances": data["result"], "address": address, "network": network}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
async def _alchemy_token_metadata(contract: str = "", network: str = "eth-mainnet", **kw) -> dict | None:
|
|
"""Alchemy - get token metadata."""
|
|
api_key = kw.get("api_key", "")
|
|
if not contract or not api_key:
|
|
return None
|
|
try:
|
|
url = f"https://{network}.g.alchemy.com/v2/{api_key}"
|
|
payload = {
|
|
"jsonrpc": "2.0",
|
|
"method": "alchemy_getTokenMetadata",
|
|
"params": [contract],
|
|
"id": 1,
|
|
}
|
|
async with httpx.AsyncClient(timeout=10) as c:
|
|
r = await c.post(url, json=payload)
|
|
if r.status_code == 200:
|
|
data = r.json()
|
|
if "result" in data:
|
|
return {"metadata": data["result"], "contract": contract}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
# ── PASSTHROUGH PROVIDERS - call our own backend endpoints ──────
|
|
|
|
|
|
async def _passthrough_market_overview(**kwargs) -> dict | None:
|
|
"""Market overview from our own /api/v1/content/market-overview."""
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10) as c:
|
|
r = await c.get("http://localhost:8000/api/v1/content/market-overview")
|
|
if r.status_code == 200:
|
|
return r.json()
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
async def _passthrough_trending(**kwargs) -> dict | None:
|
|
"""Trending tokens from our own /api/v1/tokens/trending."""
|
|
try:
|
|
limit = kwargs.get("limit", 20)
|
|
async with httpx.AsyncClient(timeout=10) as c:
|
|
r = await c.get(f"http://localhost:8000/api/v1/tokens/trending?limit={limit}")
|
|
if r.status_code == 200:
|
|
return r.json()
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
async def _passthrough_news(**kwargs) -> dict | None:
|
|
"""News from our own 30-source aggregator."""
|
|
try:
|
|
limit = kwargs.get("limit", 20)
|
|
async with httpx.AsyncClient(timeout=15) as c:
|
|
r = await c.get(f"http://localhost:8000/api/v1/news/combined?limit={limit}")
|
|
if r.status_code == 200:
|
|
return r.json()
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
async def _passthrough_alerts(**kwargs) -> dict | None:
|
|
"""Alerts from our real alert pipeline."""
|
|
try:
|
|
from app.alert_pipeline import get_active_alert_count, get_recent_alerts
|
|
|
|
count = await get_active_alert_count()
|
|
recent = await get_recent_alerts(limit=kwargs.get("limit", 20))
|
|
return {"count": count, "alerts": recent}
|
|
except Exception:
|
|
pass
|
|
return {"count": 0, "alerts": []}
|
|
|
|
|
|
# ── LOCAL DATA PROVIDERS - our own databases ───────────────────
|
|
|
|
|
|
async def _local_wallet_labels(address: str = "", **kw) -> dict | None:
|
|
"""Our 190K wallet labels from Redis - rmi:label:{chain}:{address} format."""
|
|
if not address:
|
|
return None
|
|
try:
|
|
import json
|
|
import os
|
|
|
|
import redis
|
|
from dotenv import load_dotenv
|
|
|
|
load_dotenv("/app/.env", override=True)
|
|
r = redis.Redis(
|
|
host=os.getenv("REDIS_HOST", "rmi-redis"),
|
|
port=int(os.getenv("REDIS_PORT", "6379")),
|
|
password=os.getenv("REDIS_PASSWORD", ""),
|
|
decode_responses=True,
|
|
socket_connect_timeout=2,
|
|
)
|
|
# Search across all chains - label key is rmi:label:{chain}:{address}
|
|
chains = [
|
|
"solana",
|
|
"ethereum",
|
|
"bsc",
|
|
"base",
|
|
"arbitrum",
|
|
"optimism",
|
|
"polygon",
|
|
"avalanche",
|
|
]
|
|
for chain in chains:
|
|
key = f"rmi:label:{chain}:{address}"
|
|
data = r.get(key)
|
|
if data:
|
|
label = json.loads(data)
|
|
r.close()
|
|
return {
|
|
"labels": [label],
|
|
"address": address,
|
|
"source": "wallet_memory_bank",
|
|
"total_labels": "190K+",
|
|
}
|
|
# Try prefix search as fallback
|
|
keys = r.keys(f"rmi:label:*:{address}")
|
|
if keys:
|
|
data = r.get(keys[0])
|
|
if data:
|
|
label = json.loads(data)
|
|
r.close()
|
|
return {
|
|
"labels": [label],
|
|
"address": address,
|
|
"source": "wallet_memory_bank",
|
|
"total_labels": "190K+",
|
|
}
|
|
r.close()
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
async def _passthrough_scanner(address: str = "", chain: str = "solana", **kw) -> dict | None:
|
|
"""SENTINEL scanner - our own token security analysis."""
|
|
try:
|
|
async with httpx.AsyncClient(timeout=30) as c:
|
|
r = await c.post("http://localhost:8000/api/v1/token/scan", json={"address": address, "chain": chain})
|
|
if r.status_code == 200:
|
|
return r.json()
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
async def _passthrough_rag(query: str = "", collection: str = "known_scams", **kw) -> dict | None:
|
|
"""RAG search - our 17K+ document knowledge base."""
|
|
try:
|
|
async with httpx.AsyncClient(timeout=15) as c:
|
|
r = await c.post(
|
|
"http://localhost:8000/api/v1/rag/search",
|
|
json={"query": query, "collection": collection, "limit": kw.get("limit", 10)},
|
|
)
|
|
if r.status_code == 200:
|
|
return r.json()
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
# ── MORALIS WEB3 AI AGENTS - Full API Suite ──────────────────────
|
|
|
|
_MORALIS_BASE = "https://deep-index.moralis.io/api/v2.2"
|
|
|
|
|
|
async def _moralis_wallet_tokens(address: str = "", chain: str = "eth", **kw) -> dict | None:
|
|
"""Moralis - get wallet token balances with metadata."""
|
|
api_key = kw.get("api_key", "")
|
|
if not address or not api_key:
|
|
return None
|
|
try:
|
|
async with httpx.AsyncClient(timeout=15) as c:
|
|
r = await c.get(
|
|
f"{_MORALIS_BASE}/{address}/erc20",
|
|
params={"chain": chain},
|
|
headers={"X-API-Key": api_key},
|
|
)
|
|
if r.status_code == 200:
|
|
return {"tokens": r.json(), "address": address, "chain": chain}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
async def _moralis_wallet_nfts(address: str = "", chain: str = "eth", **kw) -> dict | None:
|
|
"""Moralis - get wallet NFTs."""
|
|
api_key = kw.get("api_key", "")
|
|
if not address or not api_key:
|
|
return None
|
|
try:
|
|
async with httpx.AsyncClient(timeout=15) as c:
|
|
r = await c.get(
|
|
f"{_MORALIS_BASE}/{address}/nft",
|
|
params={"chain": chain},
|
|
headers={"X-API-Key": api_key},
|
|
)
|
|
if r.status_code == 200:
|
|
return {"nfts": r.json(), "address": address, "chain": chain}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
async def _moralis_wallet_transactions(address: str = "", chain: str = "eth", **kw) -> dict | None:
|
|
"""Moralis - get wallet transaction history."""
|
|
api_key = kw.get("api_key", "")
|
|
if not address or not api_key:
|
|
return None
|
|
try:
|
|
limit = kw.get("limit", 50)
|
|
async with httpx.AsyncClient(timeout=15) as c:
|
|
r = await c.get(
|
|
f"{_MORALIS_BASE}/{address}",
|
|
params={"chain": chain, "limit": limit},
|
|
headers={"X-API-Key": api_key},
|
|
)
|
|
if r.status_code == 200:
|
|
return {"transactions": r.json(), "address": address, "chain": chain}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
async def _moralis_token_price(address: str = "", chain: str = "eth", **kw) -> dict | None:
|
|
"""Moralis - get token price via Token API."""
|
|
api_key = kw.get("api_key", "")
|
|
if not address or not api_key:
|
|
return None
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10) as c:
|
|
r = await c.get(
|
|
f"{_MORALIS_BASE}/erc20/{address}/price",
|
|
params={"chain": chain},
|
|
headers={"X-API-Key": api_key},
|
|
)
|
|
if r.status_code == 200:
|
|
return {"price": r.json(), "address": address, "chain": chain}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
async def _moralis_token_metadata(address: str = "", chain: str = "eth", **kw) -> dict | None:
|
|
"""Moralis - get token metadata."""
|
|
api_key = kw.get("api_key", "")
|
|
if not address or not api_key:
|
|
return None
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10) as c:
|
|
r = await c.get(
|
|
f"{_MORALIS_BASE}/erc20/{address}",
|
|
params={"chain": chain},
|
|
headers={"X-API-Key": api_key},
|
|
)
|
|
if r.status_code == 200:
|
|
return {"metadata": r.json(), "address": address}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
async def _moralis_wallet_net_worth(address: str = "", **kw) -> dict | None:
|
|
"""Moralis - wallet net worth across chains."""
|
|
api_key = kw.get("api_key", "")
|
|
if not address or not api_key:
|
|
return None
|
|
try:
|
|
async with httpx.AsyncClient(timeout=20) as c:
|
|
r = await c.get(f"{_MORALIS_BASE}/wallets/{address}/net-worth", headers={"X-API-Key": api_key})
|
|
if r.status_code == 200:
|
|
return {"net_worth": r.json(), "address": address}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
async def _moralis_search_tokens(query: str = "", **kw) -> dict | None:
|
|
"""Moralis - search tokens by name/symbol/address."""
|
|
api_key = kw.get("api_key", "")
|
|
if not query or not api_key:
|
|
return None
|
|
try:
|
|
limit = kw.get("limit", 10)
|
|
async with httpx.AsyncClient(timeout=10) as c:
|
|
r = await c.get(
|
|
f"{_MORALIS_BASE}/search",
|
|
params={"q": query, "filter": "token", "limit": limit},
|
|
headers={"X-API-Key": api_key},
|
|
)
|
|
if r.status_code == 200:
|
|
return {"results": r.json(), "query": query}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
# ── MCP BRIDGE - Call local MCP servers from DataBus ──────────────
|
|
|
|
|
|
async def _mcp_bridge(mcp_server: str = "", mcp_tool: str = "", **kw) -> dict | None:
|
|
"""Universal MCP bridge - calls any local MCP server tool.
|
|
|
|
Uses subprocess to call MCP servers installed in /root/.hermes/mcp-servers/.
|
|
Falls back to HTTP for configured HTTP MCP servers.
|
|
"""
|
|
if not mcp_server or not mcp_tool:
|
|
return None
|
|
try:
|
|
import json as _json
|
|
import subprocess
|
|
|
|
# Build tool args dict from kwargs (strip internal params)
|
|
tool_args = {k: v for k, v in kw.items() if k not in ("api_key", "mcp_server", "mcp_tool")}
|
|
|
|
# Known MCP server → command mapping
|
|
MCP_COMMANDS = {
|
|
"evm-direct": ["node", "/root/.hermes/mcp-servers/evm-direct/bin/cli.js"],
|
|
"evmscope": ["node", "/root/.hermes/mcp-servers/evmscope/dist/cli.js"],
|
|
"jupiter-mcp": ["node", "/root/.hermes/mcp-servers/jupiter-mcp/index.js"],
|
|
"crypto-feargreed-mcp": [
|
|
"python3",
|
|
"/root/.hermes/mcp-servers/crypto-feargreed-mcp/main.py",
|
|
],
|
|
"crypto-indicators-mcp": [
|
|
"node",
|
|
"/root/.hermes/mcp-servers/crypto-indicators-mcp/index.js",
|
|
],
|
|
"moralis-mcp": ["node", "/root/.hermes/mcp-servers/moralis-mcp/src/index.mjs"],
|
|
"web3-research-mcp": ["node", "/root/.hermes/mcp-servers/web3-research-mcp/bin/cli.js"],
|
|
"solana-mcp": ["node", "/root/.hermes/mcp-servers/solana-mcp-official/index.js"],
|
|
}
|
|
|
|
if mcp_server in MCP_COMMANDS:
|
|
# Build MCP JSON-RPC call
|
|
mcp_request = _json.dumps(
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"method": "tools/call",
|
|
"params": {"name": mcp_tool, "arguments": tool_args},
|
|
"id": 1,
|
|
}
|
|
)
|
|
cmd = MCP_COMMANDS[mcp_server]
|
|
result = subprocess.run(cmd, input=mcp_request, capture_output=True, text=True, timeout=30)
|
|
if result.returncode == 0 and result.stdout:
|
|
data = _json.loads(result.stdout)
|
|
if "result" in data:
|
|
return {"result": data["result"], "server": mcp_server, "tool": mcp_tool}
|
|
else:
|
|
# Try HTTP MCP server
|
|
mcp_configs = {
|
|
"coingecko": "https://mcp.api.coingecko.com/mcp",
|
|
}
|
|
if mcp_server in mcp_configs:
|
|
async with httpx.AsyncClient(timeout=30) as c:
|
|
r = await c.post(
|
|
mcp_configs[mcp_server],
|
|
json={
|
|
"jsonrpc": "2.0",
|
|
"method": "tools/call",
|
|
"params": {"name": mcp_tool, "arguments": tool_args},
|
|
"id": 1,
|
|
},
|
|
)
|
|
if r.status_code == 200:
|
|
data = r.json()
|
|
if "result" in data:
|
|
return {
|
|
"result": data["result"],
|
|
"server": mcp_server,
|
|
"tool": mcp_tool,
|
|
}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
# ── ARKHAM INTELLIGENCE - Free trial month, premium entity data ──
|
|
|
|
_ARKHAM_BASE = "https://api.arkhamintelligence.com"
|
|
|
|
|
|
async def _arkham_entity(address: str = "", **kw) -> dict | None:
|
|
"""Arkham - entity intelligence (tx counts, top counterparties, tags)."""
|
|
api_key = kw.get("api_key", "")
|
|
if not address or not api_key:
|
|
return None
|
|
try:
|
|
headers = {"API-Key": api_key}
|
|
async with httpx.AsyncClient(timeout=15) as c:
|
|
r = await c.get(f"{_ARKHAM_BASE}/intelligence/address/{address}", headers=headers)
|
|
if r.status_code == 200:
|
|
return {"entity": r.json(), "address": address, "source": "arkham"}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
async def _arkham_counterparties(address: str = "", **kw) -> dict | None:
|
|
"""Arkham - top counterparties ranked by transaction volume."""
|
|
api_key = kw.get("api_key", "")
|
|
if not address or not api_key:
|
|
return None
|
|
try:
|
|
limit = kw.get("limit", 25)
|
|
headers = {"API-Key": api_key}
|
|
async with httpx.AsyncClient(timeout=15) as c:
|
|
r = await c.get(
|
|
f"{_ARKHAM_BASE}/counterparties/address/{address}",
|
|
params={"limit": limit},
|
|
headers=headers,
|
|
)
|
|
if r.status_code == 200:
|
|
return {"counterparties": r.json(), "address": address, "source": "arkham"}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
async def _arkham_intel_search(query: str = "", **kw) -> dict | None:
|
|
"""Arkham - search entities by address prefix (up to 20 matches)."""
|
|
api_key = kw.get("api_key", "")
|
|
if not query or not api_key:
|
|
return None
|
|
try:
|
|
headers = {"API-Key": api_key}
|
|
async with httpx.AsyncClient(timeout=15) as c:
|
|
r = await c.get(f"{_ARKHAM_BASE}/intelligence/search", params={"query": query}, headers=headers)
|
|
if r.status_code == 200:
|
|
return {"results": r.json(), "query": query, "source": "arkham"}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
async def _arkham_portfolio(address: str = "", **kw) -> dict | None:
|
|
"""Arkham - portfolio via entity intelligence (includes balance info)."""
|
|
api_key = kw.get("api_key", "")
|
|
if not address or not api_key:
|
|
return None
|
|
try:
|
|
headers = {"API-Key": api_key}
|
|
async with httpx.AsyncClient(timeout=20) as c:
|
|
r = await c.get(f"{_ARKHAM_BASE}/intelligence/address/{address}", headers=headers)
|
|
if r.status_code == 200:
|
|
return {"portfolio": r.json(), "address": address, "source": "arkham"}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
async def _arkham_transfers(address: str = "", **kw) -> dict | None:
|
|
"""Arkham - transfer history via counterparties endpoint."""
|
|
api_key = kw.get("api_key", "")
|
|
if not address or not api_key:
|
|
return None
|
|
try:
|
|
limit = kw.get("limit", 100)
|
|
headers = {"API-Key": api_key}
|
|
async with httpx.AsyncClient(timeout=20) as c:
|
|
r = await c.get(
|
|
f"{_ARKHAM_BASE}/counterparties/address/{address}",
|
|
params={"limit": limit},
|
|
headers=headers,
|
|
)
|
|
if r.status_code == 200:
|
|
return {"transfers": r.json(), "address": address, "source": "arkham"}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
async def _arkham_labels(address: str = "", **kw) -> dict | None:
|
|
"""Arkham - labels extracted from entity intelligence (arkhamLabel field)."""
|
|
api_key = kw.get("api_key", "")
|
|
if not address or not api_key:
|
|
return None
|
|
try:
|
|
headers = {"API-Key": api_key}
|
|
async with httpx.AsyncClient(timeout=15) as c:
|
|
r = await c.get(f"{_ARKHAM_BASE}/intelligence/address/{address}", headers=headers)
|
|
if r.status_code == 200:
|
|
data = r.json()
|
|
label = data.get("arkhamLabel", {})
|
|
entity = data.get("arkhamEntity", {})
|
|
labels = []
|
|
if label and label.get("name"):
|
|
labels.append({"name": label["name"], "id": label.get("id"), "type": "arkham"})
|
|
if entity and entity.get("name"):
|
|
labels.append({"name": entity["name"], "id": entity.get("id"), "type": "entity"})
|
|
return {
|
|
"labels": labels,
|
|
"address": address,
|
|
"chain": data.get("chain"),
|
|
"source": "arkham",
|
|
}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
# ── FREE FALLBACK PROVIDERS - never pay for what's free ──────────
|
|
|
|
|
|
async def _dexscreener_token_metadata(mint: str = "", **kw) -> dict | None:
|
|
"""DexScreener - free token metadata from pairs endpoint."""
|
|
token = mint or kw.get("token", "") or kw.get("contract", "")
|
|
if not token:
|
|
return None
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10) as c:
|
|
r = await c.get(f"https://api.dexscreener.com/latest/dex/tokens/{token}")
|
|
if r.status_code == 200:
|
|
data = r.json()
|
|
pairs = data.get("pairs", [])
|
|
if pairs:
|
|
p = pairs[0]
|
|
return {
|
|
"metadata": {
|
|
"name": p.get("baseToken", {}).get("name", ""),
|
|
"symbol": p.get("baseToken", {}).get("symbol", ""),
|
|
"decimals": None,
|
|
"price_usd": p.get("priceUsd"),
|
|
"liquidity_usd": p.get("liquidity", {}).get("usd"),
|
|
"fdv": p.get("fdv"),
|
|
"chain": p.get("chainId", ""),
|
|
},
|
|
"source": "dexscreener",
|
|
}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
async def _dexscreener_holders(mint: str = "", **kw) -> dict | None:
|
|
"""DexScreener - free holder/liquidity data from pairs."""
|
|
token = mint or kw.get("token", "")
|
|
if not token:
|
|
return None
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10) as c:
|
|
r = await c.get(f"https://api.dexscreener.com/latest/dex/tokens/{token}")
|
|
if r.status_code == 200:
|
|
data = r.json()
|
|
pairs = data.get("pairs", [])
|
|
if pairs:
|
|
return {"pairs": pairs, "total_pairs": len(pairs), "source": "dexscreener"}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
async def _dexscreener_trades(token: str = "", chain: str = "solana", **kw) -> dict | None:
|
|
"""DexScreener - recent trades for a token (free, no API key required)."""
|
|
if not token:
|
|
return None
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10) as c:
|
|
r = await c.get(f"https://api.dexscreener.com/latest/dex/tokens/{token}")
|
|
if r.status_code == 200:
|
|
data = r.json()
|
|
pairs = data.get("pairs", [])
|
|
if pairs:
|
|
# DexScreener doesn't have a direct trades endpoint, but we can simulate
|
|
# recent activity from pair data or use a mock structure for the frontend
|
|
# to render until a dedicated trades API is wired.
|
|
return {
|
|
"trades": [],
|
|
"message": "Live trades feed requires premium DEX API. Showing pair summary.",
|
|
"pairs": pairs[:5],
|
|
"source": "dexscreener",
|
|
}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
async def _dexscreener_top_traders(token: str = "", chain: str = "solana", **kw) -> dict | None:
|
|
"""DexScreener - top profitable traders for a token."""
|
|
if not token:
|
|
return None
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10) as c:
|
|
r = await c.get(f"https://api.dexscreener.com/latest/dex/tokens/{token}")
|
|
if r.status_code == 200:
|
|
data = r.json()
|
|
pairs = data.get("pairs", [])
|
|
if pairs:
|
|
return {
|
|
"top_traders": [],
|
|
"message": "Top trader analytics require premium on-chain indexer. Showing pair summary.",
|
|
"pairs": pairs[:3],
|
|
"source": "dexscreener",
|
|
}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
async def _etherscan_tx_trace(tx_hash: str = "", network: str = "ethereum", **kw) -> dict | None:
|
|
"""Etherscan - free transaction data (5 req/sec, no key needed for basic)."""
|
|
if not tx_hash:
|
|
return None
|
|
try:
|
|
# Map network to Etherscan domain
|
|
domains = {
|
|
"ethereum": "api.etherscan.io",
|
|
"eth-mainnet": "api.etherscan.io",
|
|
"bsc": "api.bscscan.com",
|
|
"polygon": "api.polygonscan.com",
|
|
"arbitrum": "api.arbiscan.io",
|
|
"optimism": "api-optimistic.etherscan.io",
|
|
"base": "api.basescan.org",
|
|
}
|
|
domain = domains.get(network, "api.etherscan.io")
|
|
async with httpx.AsyncClient(timeout=10) as c:
|
|
r = await c.get(
|
|
f"https://{domain}/api",
|
|
params={
|
|
"module": "proxy",
|
|
"action": "eth_getTransactionByHash",
|
|
"txhash": tx_hash,
|
|
"apikey": "YourApiKeyToken",
|
|
},
|
|
)
|
|
if r.status_code == 200:
|
|
data = r.json()
|
|
if data.get("result"):
|
|
return {
|
|
"transaction": data["result"],
|
|
"tx_hash": tx_hash,
|
|
"source": "etherscan",
|
|
}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
# ── DEFILLAMA PROVIDER (100% FREE, NO API KEY) ────────────────────
|
|
|
|
|
|
async def _defillama_tvl(**kw) -> dict | None:
|
|
"""DeFiLlama - global TVL and protocol data (completely free)."""
|
|
try:
|
|
async with httpx.AsyncClient(timeout=15) as c:
|
|
r = await c.get("https://api.llama.fi/protocols")
|
|
if r.status_code == 200:
|
|
data = r.json()
|
|
total_tvl = sum(p.get("tvl", 0) for p in data if isinstance(p, dict))
|
|
return {
|
|
"total_tvl": total_tvl,
|
|
"protocols_count": len(data),
|
|
"top_protocols": data[:10],
|
|
"source": "defillama",
|
|
}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
async def _defillama_chains(**kw) -> dict | None:
|
|
"""DeFiLlama - TVL by chain (completely free)."""
|
|
try:
|
|
async with httpx.AsyncClient(timeout=15) as c:
|
|
r = await c.get("https://api.llama.fi/chains")
|
|
if r.status_code == 200:
|
|
data = r.json()
|
|
return {
|
|
"chains": [{"name": c.get("name"), "tvl": c.get("tvl")} for c in data if isinstance(c, dict)],
|
|
"source": "defillama",
|
|
}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
# ── BLOCKCHAIR PROVIDER (FREE TIER, NO API KEY FOR BASIC) ────────
|
|
|
|
|
|
async def _blockchair_address(address: str = "", chain: str = "bitcoin", **kw) -> dict | None:
|
|
"""Blockchair - address balance and tx count (free tier)."""
|
|
if not address:
|
|
return None
|
|
try:
|
|
async with httpx.AsyncClient(timeout=15) as c:
|
|
r = await c.get(f"https://api.blockchair.com/{chain}/dashboards/address/{address}")
|
|
if r.status_code == 200:
|
|
data = r.json()
|
|
if data.get("data") and address in data["data"]:
|
|
return {
|
|
"address": address,
|
|
"chain": chain,
|
|
"data": data["data"][address],
|
|
"source": "blockchair",
|
|
}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
async def _blockchair_stats(chain: str = "bitcoin", **kw) -> dict | None:
|
|
"""Blockchair - chain statistics (free tier)."""
|
|
try:
|
|
async with httpx.AsyncClient(timeout=15) as c:
|
|
r = await c.get(f"https://api.blockchair.com/{chain}/stats")
|
|
if r.status_code == 200:
|
|
data = r.json()
|
|
return {
|
|
"chain": chain,
|
|
"stats": data.get("data", {}).get("stats", {}),
|
|
"source": "blockchair",
|
|
}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
# ── BIRDEYE PROVIDER (FREEMIUM, FREE TIER AVAILABLE) ─────────────
|
|
|
|
|
|
async def _birdeye_overview(address: str = "", **kw) -> dict | None:
|
|
"""Birdeye - token overview, liquidity, and holder stats (free tier)."""
|
|
api_key = kw.get("api_key", "") or os.getenv("BIRDEYE_API_KEY", "")
|
|
if not address:
|
|
return None
|
|
try:
|
|
headers = {"X-API-KEY": api_key, "accept": "application/json"} if api_key else {"accept": "application/json"}
|
|
async with httpx.AsyncClient(timeout=15) as c:
|
|
r = await c.get(
|
|
f"https://public-api.birdeye.so/defi/token_overview?address={address}",
|
|
headers=headers,
|
|
)
|
|
if r.status_code == 200:
|
|
data = r.json()
|
|
return {"address": address, "data": data.get("data", {}), "source": "birdeye"}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
async def _birdeye_price(address: str = "", **kw) -> dict | None:
|
|
"""Birdeye - real-time token price (free tier)."""
|
|
api_key = kw.get("api_key", "") or os.getenv("BIRDEYE_API_KEY", "")
|
|
if not address:
|
|
return None
|
|
try:
|
|
headers = {"X-API-KEY": api_key, "accept": "application/json"} if api_key else {"accept": "application/json"}
|
|
async with httpx.AsyncClient(timeout=10) as c:
|
|
r = await c.get(f"https://public-api.birdeye.so/defi/price?address={address}", headers=headers)
|
|
if r.status_code == 200:
|
|
data = r.json()
|
|
return {
|
|
"address": address,
|
|
"price": data.get("data", {}).get("value"),
|
|
"source": "birdeye",
|
|
}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
# ── SOLANA TRACKER PROVIDER (FREEMIUM, 5K/MO FREE QUOTA) ─────────
|
|
|
|
|
|
async def _solana_tracker_price(mint: str = "", **kw) -> dict | None:
|
|
"""Solana Tracker - real-time token price (2 keys, 5000 req/mo total)."""
|
|
if not mint:
|
|
return None
|
|
try:
|
|
from app.caching_shield.solana_tracker import get_solana_tracker
|
|
|
|
st = get_solana_tracker()
|
|
data = await st.get_price(mint)
|
|
if data:
|
|
return {"mint": mint, "price": data, "source": "solana_tracker"}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
async def _solana_tracker_token(mint: str = "", **kw) -> dict | None:
|
|
"""Solana Tracker - detailed token metadata and stats."""
|
|
if not mint:
|
|
return None
|
|
try:
|
|
from app.caching_shield.solana_tracker import get_solana_tracker
|
|
|
|
st = get_solana_tracker()
|
|
data = await st.get_token(mint)
|
|
if data:
|
|
return {"mint": mint, "data": data, "source": "solana_tracker"}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
async def _solana_tracker_trending(**kw) -> dict | None:
|
|
"""Solana Tracker - trending tokens on Solana."""
|
|
try:
|
|
from app.caching_shield.solana_tracker import get_solana_tracker
|
|
|
|
st = get_solana_tracker()
|
|
data = await st.get_tokens_trending(limit=20)
|
|
if data:
|
|
return {"trending": data, "source": "solana_tracker"}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
# ── MESSARI PROVIDER (FREEMIUM, 200 REQ/MIN) ─────────────
|
|
|
|
|
|
async def _messari_news(limit: int = 20, **kw) -> dict | None:
|
|
"""Messari News API - curated crypto news with per-asset sentiment scores."""
|
|
api_key = kw.get("api_key", "") or os.getenv("MESSARI_API_KEY", "")
|
|
if not api_key:
|
|
return None
|
|
try:
|
|
headers = {"X-Messari-API-Key": api_key, "Accept": "application/json"}
|
|
async with httpx.AsyncClient(timeout=15) as c:
|
|
r = await c.get("https://api.messari.io/news/v1/news/feed", params={"limit": limit}, headers=headers)
|
|
if r.status_code == 200:
|
|
data = r.json()
|
|
if data.get("data"):
|
|
# Transform Messari format to our standard format
|
|
articles = []
|
|
for item in data["data"]:
|
|
assets = [a.get("symbol", "Unknown") for a in item.get("assets", [])]
|
|
sentiment = item.get("sentiment", [])
|
|
avg_sentiment = sum(s.get("sentiment", 0) for s in sentiment) / max(len(sentiment), 1)
|
|
|
|
articles.append(
|
|
{
|
|
"title": item.get("title", ""),
|
|
"url": item.get("url", ""),
|
|
"source": item.get("source", {}).get("sourceName", "Messari"),
|
|
"published_at": item.get("publishTime", ""),
|
|
"description": item.get("description", ""),
|
|
"assets": assets,
|
|
"sentiment_score": avg_sentiment,
|
|
"category": item.get("category", "news"),
|
|
}
|
|
)
|
|
return {"articles": articles, "source": "messari", "count": len(articles)}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
# ── COINDESK PROVIDER (FREEMIUM, HIGH-QUALITY NEWS) ─────────────
|
|
|
|
|
|
async def _coindesk_news(limit: int = 20, **kw) -> dict | None:
|
|
"""CoinDesk News API - institutional-grade crypto news with categorization."""
|
|
api_key = kw.get("api_key", "") or os.getenv("COINDESK_API_KEY", "")
|
|
if not api_key:
|
|
return None
|
|
try:
|
|
async with httpx.AsyncClient(timeout=15) as c:
|
|
r = await c.get(
|
|
"https://min-api.cryptocompare.com/data/v2/news/",
|
|
params={"lang": "EN", "limit": limit, "api_key": api_key},
|
|
)
|
|
if r.status_code == 200:
|
|
data = r.json()
|
|
if data.get("Response") == "Success" and data.get("Data"):
|
|
articles = []
|
|
for item in data["Data"]:
|
|
articles.append(
|
|
{
|
|
"title": item.get("title", ""),
|
|
"url": item.get("url", ""),
|
|
"source": item.get("source", "CoinDesk"),
|
|
"published_at": datetime.fromtimestamp(
|
|
item.get("published_on", 0), tz=timezone.utc # noqa: F821 -- pre-existing bug, see fix(f821) tracking issue
|
|
).isoformat(),
|
|
"description": item.get("body", ""),
|
|
"category": item.get("categories", "news").lower(),
|
|
"upvotes": item.get("upvotes", 0),
|
|
"downvotes": item.get("downvotes", 0),
|
|
}
|
|
)
|
|
return {"articles": articles, "source": "coindesk", "count": len(articles)}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
# ── SANTIMENT PROVIDER (FREEMIUM, DEV ACTIVITY & SOCIAL) ───────
|
|
|
|
|
|
async def _santiment_dev_activity(project_slug: str = "bitcoin", **kw) -> dict | None:
|
|
"""Santiment - GitHub dev activity and social volume (free tier: 100 calls/day)."""
|
|
api_key = kw.get("api_key", "") or os.getenv("SANTIMENT_API_KEY", "")
|
|
if not api_key or not project_slug:
|
|
return None
|
|
try:
|
|
query = f"""
|
|
{{
|
|
getMetric(metric: "dev_activity")
|
|
{{
|
|
timeseriesData(
|
|
slug: "{project_slug}"
|
|
from: "30d_ago"
|
|
to: "now"
|
|
interval: "1d"
|
|
)
|
|
}}
|
|
}}
|
|
"""
|
|
async with httpx.AsyncClient(timeout=15) as c:
|
|
r = await c.post(
|
|
"https://api.santiment.net/graphql",
|
|
json={"query": query},
|
|
headers={"Authorization": f"Apikey {api_key}"},
|
|
)
|
|
if r.status_code == 200:
|
|
data = r.json()
|
|
return {
|
|
"project": project_slug,
|
|
"dev_activity": data.get("data", {}).get("getMetric", {}).get("timeseriesData", []),
|
|
"source": "santiment",
|
|
}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
# ── VIRUSTOTAL PROVIDER (FREE, 500 REQ/DAY) ────────────────────
|
|
|
|
|
|
async def _virustotal_url_scan(url: str, **kw) -> dict | None:
|
|
"""VirusTotal - scan token website URLs for phishing/malware (free: 500 req/day)."""
|
|
api_key = kw.get("api_key", "") or os.getenv("VIRUSTOTAL_API_KEY", "")
|
|
if not api_key or not url:
|
|
return None
|
|
try:
|
|
import base64
|
|
|
|
url_id = base64.urlsafe_b64encode(url.encode()).decode().strip("=")
|
|
async with httpx.AsyncClient(timeout=15) as c:
|
|
r = await c.get(f"https://www.virustotal.com/api/v3/urls/{url_id}", headers={"x-apikey": api_key})
|
|
if r.status_code == 200:
|
|
data = r.json()
|
|
stats = data.get("data", {}).get("attributes", {}).get("last_analysis_stats", {})
|
|
return {
|
|
"url": url,
|
|
"malicious": stats.get("malicious", 0),
|
|
"suspicious": stats.get("suspicious", 0),
|
|
"harmless": stats.get("harmless", 0),
|
|
"source": "virustotal",
|
|
}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
# ── DUNE ANALYTICS PROVIDER (FREE TIER: 10K CU/MO + FALLBACK) ──
|
|
|
|
|
|
async def _dune_early_buyers(token_address: str = "", chain: str = "ethereum", **kw) -> dict | None:
|
|
"""Dune Analytics - finds the first 5-minute buyers of a token.
|
|
Uses dual-key fallback to double free tier capacity (10k CU/mo per key).
|
|
Cached aggressively (4 hours) to preserve free tier.
|
|
"""
|
|
primary_key = kw.get("api_key", "") or os.getenv("DUNE_API_KEY", "")
|
|
secondary_key = os.getenv("DUNE_API_KEY_2", "")
|
|
|
|
if not token_address:
|
|
return None
|
|
|
|
query_id = 3946245 # Replace with actual saved Dune query ID for early buyers
|
|
|
|
async def _try_dune_key(api_key: str) -> dict | None:
|
|
if not api_key:
|
|
return None
|
|
try:
|
|
headers = {"X-Dune-API-Key": api_key}
|
|
params = {"token_address": token_address.lower(), "chain": chain.lower()}
|
|
|
|
async with httpx.AsyncClient(timeout=30) as c:
|
|
exec_url = f"https://api.dune.com/api/v1/query/{query_id}/execute"
|
|
r_exec = await c.post(exec_url, json={"query_parameters": params}, headers=headers)
|
|
|
|
# Check for quota/rate limit errors
|
|
if r_exec.status_code in (429, 402, 403):
|
|
return {"error": "quota_exceeded", "status": r_exec.status_code}
|
|
|
|
if r_exec.status_code == 200:
|
|
exec_id = r_exec.json().get("execution_id")
|
|
if exec_id:
|
|
for _ in range(3):
|
|
await asyncio.sleep(2)
|
|
r_result = await c.get(
|
|
f"https://api.dune.com/api/v1/execution/{exec_id}/results",
|
|
headers=headers,
|
|
)
|
|
if r_result.status_code == 200:
|
|
data = r_result.json()
|
|
if data.get("state") == "QUERY_STATE_COMPLETED":
|
|
return {
|
|
"token": token_address,
|
|
"chain": chain,
|
|
"early_buyers": data.get("result", {}).get("rows", [])[:20],
|
|
"source": "dune",
|
|
}
|
|
elif r_result.status_code != 202:
|
|
break
|
|
except Exception as e:
|
|
logger.warning(f"Dune query failed with key: {e}")
|
|
return None
|
|
|
|
# Try primary key first
|
|
result = await _try_dune_key(primary_key)
|
|
|
|
# If primary key failed due to quota/rate limit, try secondary key
|
|
if isinstance(result, dict) and result.get("error") == "quota_exceeded":
|
|
logger.info("Dune primary key quota exceeded, failing over to secondary key")
|
|
result = await _try_dune_key(secondary_key)
|
|
|
|
# If result is the error dict, return None to let DataBus handle fallback
|
|
if isinstance(result, dict) and result.get("error") == "quota_exceeded":
|
|
return None
|
|
|
|
return result
|
|
|
|
|
|
# ── Build All Chains ───────────────────────────────────────────
|
|
|
|
|
|
def build_provider_chains() -> dict[str, ProviderChain]:
|
|
"""Build all fallback chains for every data type."""
|
|
chains = {}
|
|
|
|
# Token Price
|
|
chains["token_price"] = ProviderChain(
|
|
data_type="token_price",
|
|
description="Token price across DEXs and CEXs",
|
|
providers=[
|
|
Provider("local_price", ProviderTier.LOCAL, _local_token_price, weight=10.0),
|
|
Provider(
|
|
"solana_tracker",
|
|
ProviderTier.FREEMIUM,
|
|
_solana_tracker_price,
|
|
weight=8.0,
|
|
rate_limit_rps=3.0,
|
|
requires_key=True,
|
|
key_env="SOLANATRACKER_API_KEY",
|
|
monthly_quota=5000,
|
|
),
|
|
Provider(
|
|
"dexscreener",
|
|
ProviderTier.FREE_API,
|
|
_dexscreener_price,
|
|
weight=5.0,
|
|
rate_limit_rps=2.0,
|
|
),
|
|
Provider("coingecko", ProviderTier.FREE_API, _coingecko_price, weight=5.0),
|
|
Provider(
|
|
"moralis",
|
|
ProviderTier.PAID,
|
|
_moralis_price,
|
|
weight=1.0,
|
|
requires_key=True,
|
|
key_env="MORALIS_API_KEY",
|
|
monthly_quota=5000,
|
|
),
|
|
],
|
|
)
|
|
|
|
# Market Overview
|
|
chains["market_overview"] = ProviderChain(
|
|
data_type="market_overview",
|
|
description="Global market cap, BTC/ETH/SOL prices, fear & greed",
|
|
providers=[
|
|
Provider("rmi_market_overview", ProviderTier.LOCAL, _passthrough_market_overview, weight=10.0),
|
|
Provider(
|
|
"coingecko_global",
|
|
ProviderTier.FREEMIUM,
|
|
_coingecko_price,
|
|
weight=5.0,
|
|
requires_key=True,
|
|
key_env="COINGECKO_API_KEY",
|
|
),
|
|
],
|
|
)
|
|
|
|
# Trending
|
|
chains["trending"] = ProviderChain(
|
|
data_type="trending",
|
|
description="Trending tokens across chains",
|
|
providers=[
|
|
Provider("rmi_trending", ProviderTier.LOCAL, _passthrough_trending, weight=10.0),
|
|
Provider(
|
|
"solana_tracker_trending",
|
|
ProviderTier.FREEMIUM,
|
|
_solana_tracker_trending,
|
|
weight=8.0,
|
|
rate_limit_rps=3.0,
|
|
requires_key=True,
|
|
key_env="SOLANATRACKER_API_KEY",
|
|
monthly_quota=5000,
|
|
),
|
|
Provider(
|
|
"dexscreener_trending",
|
|
ProviderTier.FREE_API,
|
|
_dexscreener_price,
|
|
weight=5.0,
|
|
rate_limit_rps=1.0,
|
|
),
|
|
],
|
|
)
|
|
|
|
# Token Trades (Live buys/sells)
|
|
chains["token_trades"] = ProviderChain(
|
|
data_type="token_trades",
|
|
description="Live token trades (buys/sells) from DexScreener",
|
|
providers=[
|
|
Provider(
|
|
"dexscreener_trades",
|
|
ProviderTier.FREE_API,
|
|
_dexscreener_trades,
|
|
weight=10.0,
|
|
rate_limit_rps=2.0,
|
|
),
|
|
],
|
|
)
|
|
|
|
# Top Traders (Profitable wallets for a token)
|
|
chains["top_traders"] = ProviderChain(
|
|
data_type="top_traders",
|
|
description="Top profitable traders and win rates for a token",
|
|
providers=[
|
|
Provider(
|
|
"dexscreener_top_traders",
|
|
ProviderTier.FREE_API,
|
|
_dexscreener_top_traders,
|
|
weight=10.0,
|
|
rate_limit_rps=2.0,
|
|
),
|
|
],
|
|
)
|
|
|
|
# ── Dune Analytics (Free Tier: 10k CU/mo) ──
|
|
chains["dune_early_buyers"] = ProviderChain(
|
|
data_type="dune_early_buyers",
|
|
description="First 5-minute buyers of a token (Dune Analytics, low-CU cached query)",
|
|
providers=[
|
|
Provider(
|
|
"dune_early_buyers_api",
|
|
ProviderTier.FREEMIUM,
|
|
_dune_early_buyers,
|
|
weight=10.0,
|
|
rate_limit_rps=0.5,
|
|
requires_key=True,
|
|
key_env="DUNE_API_KEY",
|
|
monthly_quota=10000,
|
|
),
|
|
],
|
|
)
|
|
|
|
# News
|
|
chains["news"] = ProviderChain(
|
|
data_type="news",
|
|
description="Real-time crypto news from 30+ sources + Messari + CoinDesk institutional feeds",
|
|
providers=[
|
|
Provider("rmi_news", ProviderTier.LOCAL, _passthrough_news, weight=10.0),
|
|
Provider(
|
|
"messari_news",
|
|
ProviderTier.FREEMIUM,
|
|
_messari_news,
|
|
weight=8.0,
|
|
rate_limit_rps=2.0,
|
|
requires_key=True,
|
|
key_env="MESSARI_API_KEY",
|
|
monthly_quota=10000,
|
|
),
|
|
Provider(
|
|
"coindesk_news",
|
|
ProviderTier.FREEMIUM,
|
|
_coindesk_news,
|
|
weight=8.0,
|
|
rate_limit_rps=2.0,
|
|
requires_key=True,
|
|
key_env="COINDESK_API_KEY",
|
|
monthly_quota=10000,
|
|
),
|
|
],
|
|
)
|
|
|
|
# Alerts / Live Intel
|
|
chains["alerts"] = ProviderChain(
|
|
data_type="alerts",
|
|
description="Active threat alerts from scanner pipeline",
|
|
providers=[
|
|
Provider("rmi_alerts", ProviderTier.LOCAL, _passthrough_alerts, weight=10.0),
|
|
],
|
|
)
|
|
|
|
# ── Advanced Security & Dev Activity ──
|
|
chains["dev_activity"] = ProviderChain(
|
|
data_type="dev_activity",
|
|
description="GitHub developer activity and social volume (Santiment free tier)",
|
|
providers=[
|
|
Provider(
|
|
"santiment_dev",
|
|
ProviderTier.FREEMIUM,
|
|
_santiment_dev_activity,
|
|
weight=10.0,
|
|
rate_limit_rps=1.0,
|
|
requires_key=True,
|
|
key_env="SANTIMENT_API_KEY",
|
|
monthly_quota=3000,
|
|
),
|
|
],
|
|
)
|
|
chains["url_security_scan"] = ProviderChain(
|
|
data_type="url_security_scan",
|
|
description="Phishing and malware scan for token websites (VirusTotal free tier)",
|
|
providers=[
|
|
Provider(
|
|
"virustotal_scan",
|
|
ProviderTier.FREEMIUM,
|
|
_virustotal_url_scan,
|
|
weight=10.0,
|
|
rate_limit_rps=0.5,
|
|
requires_key=True,
|
|
key_env="VIRUSTOTAL_API_KEY",
|
|
monthly_quota=15000,
|
|
),
|
|
],
|
|
)
|
|
|
|
# ── Bitquery chains (activate free plan at bitquery.io/pricing) ──
|
|
try:
|
|
from app.databus.bitquery_provider import BitqueryProvider
|
|
|
|
_bq = BitqueryProvider()
|
|
|
|
async def _bq_token_price(**kw):
|
|
return await _bq.get_token_price(kw.get("network", "ethereum"), kw.get("token", ""))
|
|
|
|
async def _bq_holder_data(**kw):
|
|
return await _bq.get_holder_distribution(kw.get("network", "ethereum"), kw.get("token", ""))
|
|
|
|
async def _bq_tx_trace(**kw):
|
|
return await _bq.get_transaction_trace(kw.get("network", "ethereum"), kw.get("tx_hash", ""))
|
|
|
|
async def _bq_dex_volume(**kw):
|
|
return await _bq.get_dex_volume(kw.get("network", "ethereum"))
|
|
|
|
async def _bq_address_balance(**kw):
|
|
return await _bq.get_address_balance(kw.get("network", "ethereum"), kw.get("address", ""))
|
|
|
|
async def _bq_cross_chain(**kw):
|
|
return await _bq.get_cross_chain_transfers(kw.get("address", ""))
|
|
|
|
chains["holder_data"] = ProviderChain(
|
|
"holder_data",
|
|
description="Token holder distribution (DexScreener free → Bitquery freemium)",
|
|
providers=[
|
|
Provider(
|
|
"dexscreener_holders",
|
|
ProviderTier.FREE_API,
|
|
_dexscreener_holders,
|
|
weight=10.0,
|
|
rate_limit_rps=2.0,
|
|
),
|
|
Provider(
|
|
"bitquery_holders",
|
|
ProviderTier.FREEMIUM,
|
|
_bq_holder_data,
|
|
weight=5.0,
|
|
rate_limit_rps=1.0,
|
|
requires_key=True,
|
|
key_env="BITQUERY_API_KEY",
|
|
monthly_quota=10000,
|
|
),
|
|
],
|
|
)
|
|
chains["tx_trace"] = ProviderChain(
|
|
"tx_trace",
|
|
description="Transaction traces (Etherscan free → Bitquery freemium)",
|
|
providers=[
|
|
Provider(
|
|
"etherscan_trace",
|
|
ProviderTier.FREE_API,
|
|
_etherscan_tx_trace,
|
|
weight=10.0,
|
|
rate_limit_rps=3.0,
|
|
),
|
|
Provider(
|
|
"bitquery_trace",
|
|
ProviderTier.FREEMIUM,
|
|
_bq_tx_trace,
|
|
weight=5.0,
|
|
rate_limit_rps=1.0,
|
|
requires_key=True,
|
|
key_env="BITQUERY_API_KEY",
|
|
monthly_quota=10000,
|
|
),
|
|
],
|
|
)
|
|
chains["cross_chain"] = ProviderChain(
|
|
"cross_chain",
|
|
description="Cross-chain transfer tracking and bridge monitoring",
|
|
providers=[
|
|
Provider(
|
|
"bitquery_cross_chain",
|
|
ProviderTier.FREEMIUM,
|
|
_bq_cross_chain,
|
|
weight=5.0,
|
|
rate_limit_rps=1.0,
|
|
requires_key=True,
|
|
key_env="BITQUERY_API_KEY",
|
|
monthly_quota=10000,
|
|
)
|
|
],
|
|
)
|
|
logger.info("Bitquery chains registered: holder_data, tx_trace, cross_chain")
|
|
except Exception as e:
|
|
logger.warning(f"Bitquery not available: {e}")
|
|
|
|
# Wallet Labels - OUR data first
|
|
chains["wallet_labels"] = ProviderChain(
|
|
data_type="wallet_labels",
|
|
description="Address labels and entity identification (190K local + external)",
|
|
providers=[
|
|
Provider("wallet_memory_bank", ProviderTier.LOCAL, _local_wallet_labels, weight=10.0),
|
|
Provider(
|
|
"nansen_labels",
|
|
ProviderTier.PAID,
|
|
_moralis_price,
|
|
weight=5.0,
|
|
requires_key=True,
|
|
key_env="NANSEN_API_KEY",
|
|
),
|
|
],
|
|
)
|
|
|
|
# Scanner / Security
|
|
chains["scanner"] = ProviderChain(
|
|
data_type="scanner",
|
|
description="Token security scan via SENTINEL (rug pull, honeypot, risk score)",
|
|
providers=[
|
|
Provider(
|
|
"sentinel_scanner",
|
|
ProviderTier.LOCAL,
|
|
_passthrough_scanner,
|
|
weight=10.0,
|
|
rate_limit_rps=2.0,
|
|
),
|
|
],
|
|
)
|
|
|
|
# ── SPL Token Metadata Decoder (FREE, bypasses unreliable 3rd-party APIs) ──
|
|
chains["spl_token_metadata"] = ProviderChain(
|
|
data_type="spl_token_metadata",
|
|
description="Raw SPL token metadata decoder: mint authority, freeze authority, decimals, supply, and Token-2022 extensions",
|
|
providers=[
|
|
Provider(
|
|
"spl_metadata_decoder",
|
|
ProviderTier.FREE_API,
|
|
_spl_metadata_decoder_provider,
|
|
weight=10.0,
|
|
rate_limit_rps=3.0,
|
|
),
|
|
],
|
|
)
|
|
|
|
# RAG Search
|
|
chains["rag_search"] = ProviderChain(
|
|
data_type="rag_search",
|
|
description="Semantic search across 17K+ scam documents and patterns",
|
|
providers=[
|
|
Provider("local_rag", ProviderTier.LOCAL, _passthrough_rag, weight=10.0, rate_limit_rps=5.0),
|
|
],
|
|
)
|
|
|
|
# ── DeFiLlama (100% FREE, NO API KEY) ──
|
|
chains["defillama_tvl"] = ProviderChain(
|
|
data_type="defillama_tvl",
|
|
description="Global DeFi TVL and top protocols (completely free)",
|
|
providers=[
|
|
Provider(
|
|
"defillama_tvl_api",
|
|
ProviderTier.FREE_API,
|
|
_defillama_tvl,
|
|
weight=10.0,
|
|
rate_limit_rps=2.0,
|
|
),
|
|
],
|
|
)
|
|
chains["defillama_chains"] = ProviderChain(
|
|
data_type="defillama_chains",
|
|
description="TVL breakdown by blockchain (completely free)",
|
|
providers=[
|
|
Provider(
|
|
"defillama_chains_api",
|
|
ProviderTier.FREE_API,
|
|
_defillama_chains,
|
|
weight=10.0,
|
|
rate_limit_rps=2.0,
|
|
),
|
|
],
|
|
)
|
|
|
|
# ── Blockchair (FREE TIER, NO API KEY FOR BASIC) ──
|
|
chains["blockchair_address"] = ProviderChain(
|
|
data_type="blockchair_address",
|
|
description="Multi-chain address balance and tx count (BTC, ETH, SOL, etc.)",
|
|
providers=[
|
|
Provider(
|
|
"blockchair_addr_api",
|
|
ProviderTier.FREE_API,
|
|
_blockchair_address,
|
|
weight=10.0,
|
|
rate_limit_rps=3.0,
|
|
),
|
|
],
|
|
)
|
|
chains["blockchair_stats"] = ProviderChain(
|
|
data_type="blockchair_stats",
|
|
description="Multi-chain network statistics and mempool data",
|
|
providers=[
|
|
Provider(
|
|
"blockchair_stats_api",
|
|
ProviderTier.FREE_API,
|
|
_blockchair_stats,
|
|
weight=10.0,
|
|
rate_limit_rps=3.0,
|
|
),
|
|
],
|
|
)
|
|
|
|
# ── Birdeye (FREEMIUM, FREE TIER AVAILABLE) ──
|
|
chains["birdeye_overview"] = ProviderChain(
|
|
data_type="birdeye_overview",
|
|
description="Token overview, liquidity, and holder stats (Solana/EVM)",
|
|
providers=[
|
|
Provider(
|
|
"birdeye_overview_api",
|
|
ProviderTier.FREEMIUM,
|
|
_birdeye_overview,
|
|
weight=10.0,
|
|
rate_limit_rps=2.0,
|
|
requires_key=True,
|
|
key_env="BIRDEYE_API_KEY",
|
|
monthly_quota=50000,
|
|
),
|
|
],
|
|
)
|
|
chains["birdeye_price"] = ProviderChain(
|
|
data_type="birdeye_price",
|
|
description="Real-time token price from Birdeye",
|
|
providers=[
|
|
Provider(
|
|
"birdeye_price_api",
|
|
ProviderTier.FREEMIUM,
|
|
_birdeye_price,
|
|
weight=5.0,
|
|
rate_limit_rps=5.0,
|
|
requires_key=True,
|
|
key_env="BIRDEYE_API_KEY",
|
|
monthly_quota=50000,
|
|
),
|
|
Provider(
|
|
"dexscreener_price",
|
|
ProviderTier.FREE_API,
|
|
_dexscreener_price,
|
|
weight=10.0,
|
|
rate_limit_rps=2.0,
|
|
), # Fallback to free
|
|
],
|
|
)
|
|
|
|
# ── Alchemy chains ──
|
|
chains["wallet_tokens"] = ProviderChain(
|
|
data_type="wallet_tokens",
|
|
description="Token balances for any address (Alchemy + Moralis)",
|
|
providers=[
|
|
Provider(
|
|
"alchemy_balances",
|
|
ProviderTier.FREEMIUM,
|
|
_alchemy_token_balances,
|
|
weight=10.0,
|
|
rate_limit_rps=5.0,
|
|
requires_key=True,
|
|
key_env="ALCHEMY_API_KEY",
|
|
monthly_quota=300000,
|
|
),
|
|
Provider(
|
|
"moralis_tokens",
|
|
ProviderTier.FREEMIUM,
|
|
_moralis_wallet_tokens,
|
|
weight=5.0,
|
|
rate_limit_rps=3.0,
|
|
requires_key=True,
|
|
key_env="MORALIS_API_KEY",
|
|
),
|
|
],
|
|
)
|
|
chains["token_metadata"] = ProviderChain(
|
|
data_type="token_metadata",
|
|
description="Token metadata lookup (DexScreener free → Alchemy freemium)",
|
|
providers=[
|
|
Provider(
|
|
"dexscreener_meta",
|
|
ProviderTier.FREE_API,
|
|
_dexscreener_token_metadata,
|
|
weight=10.0,
|
|
rate_limit_rps=3.0,
|
|
),
|
|
Provider(
|
|
"alchemy_metadata",
|
|
ProviderTier.FREEMIUM,
|
|
_alchemy_token_metadata,
|
|
weight=5.0,
|
|
rate_limit_rps=10.0,
|
|
requires_key=True,
|
|
key_env="ALCHEMY_API_KEY",
|
|
monthly_quota=300000,
|
|
),
|
|
],
|
|
)
|
|
chains["wallet_nfts"] = ProviderChain(
|
|
data_type="wallet_nfts",
|
|
description="NFT holdings for any address (Moralis - free tier available)",
|
|
providers=[
|
|
Provider(
|
|
"moralis_nfts",
|
|
ProviderTier.FREEMIUM,
|
|
_moralis_wallet_nfts,
|
|
weight=5.0,
|
|
rate_limit_rps=1.0,
|
|
requires_key=True,
|
|
key_env="MORALIS_API_KEY",
|
|
),
|
|
],
|
|
)
|
|
|
|
# ── Moralis expanded chains ──
|
|
chains["wallet_transactions"] = ProviderChain(
|
|
data_type="wallet_transactions",
|
|
description="Wallet transaction history (Etherscan free → Moralis freemium)",
|
|
providers=[
|
|
Provider(
|
|
"etherscan_wallet",
|
|
ProviderTier.FREE_API,
|
|
_etherscan_tx_trace,
|
|
weight=10.0,
|
|
rate_limit_rps=3.0,
|
|
),
|
|
Provider(
|
|
"moralis_txns",
|
|
ProviderTier.FREEMIUM,
|
|
_moralis_wallet_transactions,
|
|
weight=5.0,
|
|
rate_limit_rps=3.0,
|
|
requires_key=True,
|
|
key_env="MORALIS_API_KEY",
|
|
),
|
|
],
|
|
)
|
|
|
|
chains["wallet_net_worth"] = ProviderChain(
|
|
data_type="wallet_net_worth",
|
|
description="Wallet net worth across chains (Moralis)",
|
|
providers=[
|
|
Provider(
|
|
"moralis_net_worth",
|
|
ProviderTier.FREEMIUM,
|
|
_moralis_wallet_net_worth,
|
|
weight=10.0,
|
|
rate_limit_rps=2.0,
|
|
requires_key=True,
|
|
key_env="MORALIS_API_KEY",
|
|
),
|
|
],
|
|
)
|
|
|
|
chains["token_search"] = ProviderChain(
|
|
data_type="token_search",
|
|
description="Search tokens by name/symbol/address (Moralis + MCP)",
|
|
providers=[
|
|
Provider(
|
|
"moralis_search",
|
|
ProviderTier.FREEMIUM,
|
|
_moralis_search_tokens,
|
|
weight=10.0,
|
|
rate_limit_rps=5.0,
|
|
requires_key=True,
|
|
key_env="MORALIS_API_KEY",
|
|
),
|
|
],
|
|
)
|
|
|
|
# ── Arkham Intelligence - Free trial month (Nov 2026) ──
|
|
# Priority: LOCAL labels → Arkham (free trial) → paid alternatives
|
|
chains["entity_intel"] = ProviderChain(
|
|
data_type="entity_intel",
|
|
description="Entity resolution and risk scoring (local labels → Arkham free → Moralis)",
|
|
providers=[
|
|
Provider("wallet_labels_local", ProviderTier.LOCAL, _local_wallet_labels, weight=10.0),
|
|
Provider(
|
|
"arkham_entity",
|
|
ProviderTier.FREEMIUM,
|
|
_arkham_entity,
|
|
weight=8.0,
|
|
rate_limit_rps=5.0,
|
|
requires_key=True,
|
|
key_env="ARKHAM_API_KEY",
|
|
monthly_quota=100000,
|
|
),
|
|
Provider(
|
|
"moralis_labels",
|
|
ProviderTier.FREEMIUM,
|
|
_moralis_wallet_tokens,
|
|
weight=3.0,
|
|
requires_key=True,
|
|
key_env="MORALIS_API_KEY",
|
|
),
|
|
],
|
|
)
|
|
|
|
chains["arkham_labels"] = ProviderChain(
|
|
data_type="arkham_labels",
|
|
description="Entity labels with confidence scores (local → Arkham free trial)",
|
|
providers=[
|
|
Provider("wallet_labels_local", ProviderTier.LOCAL, _local_wallet_labels, weight=10.0),
|
|
Provider(
|
|
"arkham_labels_api",
|
|
ProviderTier.FREEMIUM,
|
|
_arkham_labels,
|
|
weight=8.0,
|
|
rate_limit_rps=5.0,
|
|
requires_key=True,
|
|
key_env="ARKHAM_API_KEY",
|
|
monthly_quota=100000,
|
|
),
|
|
],
|
|
)
|
|
|
|
chains["arkham_portfolio"] = ProviderChain(
|
|
data_type="arkham_portfolio",
|
|
description="Portfolio holdings with USD values (Arkham free trial)",
|
|
providers=[
|
|
Provider(
|
|
"arkham_portfolio_api",
|
|
ProviderTier.FREEMIUM,
|
|
_arkham_portfolio,
|
|
weight=10.0,
|
|
rate_limit_rps=3.0,
|
|
requires_key=True,
|
|
key_env="ARKHAM_API_KEY",
|
|
monthly_quota=100000,
|
|
),
|
|
],
|
|
)
|
|
|
|
chains["arkham_transfers"] = ProviderChain(
|
|
data_type="arkham_transfers",
|
|
description="Transfer history with counterparty mapping (Arkham free trial)",
|
|
providers=[
|
|
Provider(
|
|
"arkham_transfers_api",
|
|
ProviderTier.FREEMIUM,
|
|
_arkham_transfers,
|
|
weight=10.0,
|
|
rate_limit_rps=3.0,
|
|
requires_key=True,
|
|
key_env="ARKHAM_API_KEY",
|
|
monthly_quota=100000,
|
|
),
|
|
],
|
|
)
|
|
|
|
chains["arkham_counterparties"] = ProviderChain(
|
|
data_type="arkham_counterparties",
|
|
description="Counterparty network and relationship mapping (Arkham free trial)",
|
|
providers=[
|
|
Provider(
|
|
"arkham_counterparties_api",
|
|
ProviderTier.FREEMIUM,
|
|
_arkham_counterparties,
|
|
weight=10.0,
|
|
rate_limit_rps=3.0,
|
|
requires_key=True,
|
|
key_env="ARKHAM_API_KEY",
|
|
monthly_quota=100000,
|
|
),
|
|
],
|
|
)
|
|
|
|
chains["arkham_intel"] = ProviderChain(
|
|
data_type="arkham_intel",
|
|
description="Entity intelligence search (Arkham free trial)",
|
|
providers=[
|
|
Provider(
|
|
"arkham_intel_search",
|
|
ProviderTier.FREEMIUM,
|
|
_arkham_intel_search,
|
|
weight=10.0,
|
|
rate_limit_rps=5.0,
|
|
requires_key=True,
|
|
key_env="ARKHAM_API_KEY",
|
|
monthly_quota=100000,
|
|
),
|
|
],
|
|
)
|
|
|
|
# ── MCP Bridge - all installed MCP servers ──
|
|
chains["mcp_bridge"] = ProviderChain(
|
|
data_type="mcp_bridge",
|
|
description="Universal MCP bridge - calls any local/remote MCP server tool",
|
|
providers=[
|
|
Provider("mcp_bridge", ProviderTier.LOCAL, _mcp_bridge, weight=10.0),
|
|
],
|
|
)
|
|
|
|
# ── PREMIUM SCANNER - 10 high-value detection chains ──
|
|
try:
|
|
from app.databus.premium_scanner import (
|
|
detect_bot_farms,
|
|
detect_bundles,
|
|
detect_copy_trading,
|
|
detect_fresh_wallets,
|
|
detect_insider_signals,
|
|
detect_mev_sandwich,
|
|
detect_snipers,
|
|
detect_wash_trading,
|
|
find_dev_wallets,
|
|
map_clusters,
|
|
)
|
|
|
|
def _premium_provider(name, fn, rps=3.0):
|
|
return Provider(name, ProviderTier.LOCAL, fn, weight=10.0, rate_limit_rps=rps)
|
|
|
|
chains["bundle_detect"] = ProviderChain(
|
|
"bundle_detect",
|
|
description="Coordinated wallet bundle detection (Bubblemaps-style cluster analysis)",
|
|
providers=[_premium_provider("bundle_scanner", detect_bundles)],
|
|
)
|
|
|
|
chains["cluster_map"] = ProviderChain(
|
|
"cluster_map",
|
|
description="Full wallet cluster mapping - funders, recipients, counterparties (graph-ready)",
|
|
providers=[_premium_provider("cluster_mapper", map_clusters, rps=1.0)],
|
|
)
|
|
|
|
chains["dev_finder"] = ProviderChain(
|
|
"dev_finder",
|
|
description="Find developer/creator wallets behind tokens (deployer→funder→team)",
|
|
providers=[_premium_provider("dev_finder_scanner", find_dev_wallets)],
|
|
)
|
|
|
|
chains["sniper_detect"] = ProviderChain(
|
|
"sniper_detect",
|
|
description="Sniper detection - first-block buyers with fast dump patterns",
|
|
providers=[_premium_provider("sniper_scanner", detect_snipers)],
|
|
)
|
|
|
|
chains["bot_farm_detect"] = ProviderChain(
|
|
"bot_farm_detect",
|
|
description="Bot farm detection - identical behavior patterns across wallets",
|
|
providers=[_premium_provider("bot_farm_scanner", detect_bot_farms)],
|
|
)
|
|
|
|
chains["copy_trade_detect"] = ProviderChain(
|
|
"copy_trade_detect",
|
|
description="Copy trading pattern detection - wallets mirroring trades with delay",
|
|
providers=[_premium_provider("copy_trade_scanner", detect_copy_trading)],
|
|
)
|
|
|
|
chains["insider_detect"] = ProviderChain(
|
|
"insider_detect",
|
|
description="Insider trading signals - large buys before major announcements",
|
|
providers=[_premium_provider("insider_scanner", detect_insider_signals)],
|
|
)
|
|
|
|
chains["wash_trade_detect"] = ProviderChain(
|
|
"wash_trade_detect",
|
|
description="Wash trading detection - circular transactions, self-trading",
|
|
providers=[_premium_provider("wash_trade_scanner", detect_wash_trading)],
|
|
)
|
|
|
|
chains["mev_detect"] = ProviderChain(
|
|
"mev_detect",
|
|
description="MEV sandwich attack detection - frontrun/backrun patterns",
|
|
providers=[_premium_provider("mev_scanner", detect_mev_sandwich)],
|
|
)
|
|
|
|
chains["fresh_wallet_analysis"] = ProviderChain(
|
|
"fresh_wallet_analysis",
|
|
description="Fresh wallet concentration analysis - high new-wallet % = rug risk",
|
|
providers=[_premium_provider("fresh_wallet_scanner", detect_fresh_wallets, rps=2.0)],
|
|
)
|
|
|
|
logger.info(
|
|
"Premium scanner chains registered: bundle_detect, cluster_map, dev_finder, sniper_detect, bot_farm, copy_trade, insider, wash_trade, mev, fresh_wallets"
|
|
)
|
|
except ImportError as e:
|
|
logger.warning(f"Premium scanner not available: {e}")
|
|
|
|
# ── WEBHOOK SYSTEM ──
|
|
try:
|
|
from app.databus.webhooks import handle_webhook, list_webhooks, setup_webhook # noqa: F401
|
|
|
|
chains["webhook_handler"] = ProviderChain(
|
|
"webhook_handler",
|
|
description="Intelligent webhook receiver - Arkham, Helius, Moralis, Alchemy + custom",
|
|
providers=[
|
|
Provider(
|
|
"webhook_processor",
|
|
ProviderTier.LOCAL,
|
|
lambda **kw: handle_webhook(
|
|
kw.get("service", ""),
|
|
kw.get("payload", {}),
|
|
kw.get("headers", {}),
|
|
kw.get("raw_body", b""),
|
|
),
|
|
weight=10.0,
|
|
)
|
|
],
|
|
)
|
|
except ImportError:
|
|
pass
|
|
|
|
# ── ARKHAM WEBSOCKET - real-time intelligence streaming ──
|
|
try:
|
|
from app.databus.arkham_ws import arkham_ws_subscribe
|
|
|
|
chains["arkham_ws"] = ProviderChain(
|
|
"arkham_ws",
|
|
description="Arkham Intelligence WebSocket - real-time entity updates, transfers, labels",
|
|
providers=[
|
|
Provider(
|
|
"arkham_ws_stream",
|
|
ProviderTier.FREEMIUM,
|
|
arkham_ws_subscribe,
|
|
weight=10.0,
|
|
rate_limit_rps=10.0,
|
|
requires_key=True,
|
|
key_env="ARKHAM_WS_KEY",
|
|
monthly_quota=500000,
|
|
)
|
|
],
|
|
)
|
|
logger.info("Arkham WebSocket chain registered")
|
|
except ImportError:
|
|
logger.info("Arkham WS module not found - skipping WS chain")
|
|
|
|
# ── RUGCHARTS: Volume Authenticity (Fake Volume %) ──
|
|
try:
|
|
from app.databus.volume_authenticity import analyze_volume_authenticity
|
|
|
|
chains["volume_authenticity"] = ProviderChain(
|
|
"volume_authenticity",
|
|
description="Fake volume detection - 4-layer analysis (statistical, graph, heuristic, ML) with bootstrap CI",
|
|
providers=[
|
|
Provider(
|
|
"volume_auth_scorer",
|
|
ProviderTier.LOCAL,
|
|
analyze_volume_authenticity,
|
|
weight=10.0,
|
|
rate_limit_rps=5.0,
|
|
)
|
|
],
|
|
)
|
|
logger.info("Volume Authenticity chain registered")
|
|
except ImportError:
|
|
logger.info("Volume Authenticity module not found")
|
|
|
|
# ── RUGCHARTS: OHLCV Engine ──
|
|
try:
|
|
from app.databus.ohlcv_engine import fetch_ohlcv, ingest_trade_data
|
|
|
|
chains["ohlcv"] = ProviderChain(
|
|
"ohlcv",
|
|
description="Real-time OHLCV candle aggregation (1m/5m/15m/1h/4h/1d) with authenticity scoring",
|
|
providers=[
|
|
Provider(
|
|
"ohlcv_fetcher",
|
|
ProviderTier.LOCAL,
|
|
fetch_ohlcv,
|
|
weight=10.0,
|
|
rate_limit_rps=20.0,
|
|
),
|
|
Provider(
|
|
"ohlcv_ingest",
|
|
ProviderTier.LOCAL,
|
|
ingest_trade_data,
|
|
weight=5.0,
|
|
rate_limit_rps=50.0,
|
|
),
|
|
],
|
|
)
|
|
logger.info("OHLCV Engine chain registered")
|
|
except ImportError:
|
|
logger.info("OHLCV Engine module not found")
|
|
|
|
# ── RUGCHARTS: Token Security Matrix (37+ checks) ──
|
|
try:
|
|
from app.databus.token_security import get_check_matrix_endpoint, run_full_scan
|
|
|
|
chains["token_security"] = ProviderChain(
|
|
"token_security",
|
|
description="37+ security checks - GoPlus, honeypot, contract, liquidity, holders, deployer, tokenomics, rug pull",
|
|
providers=[
|
|
Provider(
|
|
"security_scanner",
|
|
ProviderTier.LOCAL,
|
|
run_full_scan,
|
|
weight=10.0,
|
|
rate_limit_rps=10.0,
|
|
),
|
|
Provider(
|
|
"check_matrix",
|
|
ProviderTier.LOCAL,
|
|
get_check_matrix_endpoint,
|
|
weight=1.0,
|
|
rate_limit_rps=60.0,
|
|
),
|
|
],
|
|
)
|
|
logger.info("Token Security Matrix chain registered (37+ checks)")
|
|
except ImportError:
|
|
logger.info("Token Security module not found")
|
|
|
|
# ── RUGCHARTS INTELLIGENCE - 10 Premium Features ──
|
|
try:
|
|
from app.databus.data_quality import enhanced_token_report, get_tier_comparison
|
|
from app.databus.rugcharts_intel import (
|
|
cross_chain_entity,
|
|
developer_reputation,
|
|
holder_health_score,
|
|
insider_pattern_detector,
|
|
liquidity_risk_monitor,
|
|
rug_pattern_matcher,
|
|
smart_money_feed,
|
|
token_launch_scanner,
|
|
whale_alert_stream,
|
|
)
|
|
|
|
chains["smart_money"] = ProviderChain(
|
|
"smart_money",
|
|
description="Smart money feed - what profitable wallets are buying right now",
|
|
providers=[
|
|
Provider(
|
|
"smart_money_tracker",
|
|
ProviderTier.LOCAL,
|
|
smart_money_feed,
|
|
weight=10.0,
|
|
rate_limit_rps=5.0,
|
|
)
|
|
],
|
|
)
|
|
|
|
chains["whale_alerts"] = ProviderChain(
|
|
"whale_alerts",
|
|
description="Real-time whale transaction detection - large transfers across chains",
|
|
providers=[
|
|
Provider(
|
|
"whale_detector",
|
|
ProviderTier.LOCAL,
|
|
whale_alert_stream,
|
|
weight=10.0,
|
|
rate_limit_rps=10.0,
|
|
)
|
|
],
|
|
)
|
|
|
|
chains["token_launches"] = ProviderChain(
|
|
"token_launches",
|
|
description="New token launch scanner with instant risk scoring (by age)",
|
|
providers=[
|
|
Provider(
|
|
"launch_scanner",
|
|
ProviderTier.LOCAL,
|
|
token_launch_scanner,
|
|
weight=10.0,
|
|
rate_limit_rps=5.0,
|
|
)
|
|
],
|
|
)
|
|
|
|
chains["insider_detection"] = ProviderChain(
|
|
"insider_detection",
|
|
description="Pre-pump accumulation pattern detection - volume spikes before price moves",
|
|
providers=[
|
|
Provider(
|
|
"insider_detector",
|
|
ProviderTier.LOCAL,
|
|
insider_pattern_detector,
|
|
weight=10.0,
|
|
rate_limit_rps=5.0,
|
|
)
|
|
],
|
|
)
|
|
|
|
chains["liquidity_risk"] = ProviderChain(
|
|
"liquidity_risk",
|
|
description="LP health monitor - concentration, lock status, removal risk",
|
|
providers=[
|
|
Provider(
|
|
"liquidity_monitor",
|
|
ProviderTier.LOCAL,
|
|
liquidity_risk_monitor,
|
|
weight=10.0,
|
|
rate_limit_rps=10.0,
|
|
)
|
|
],
|
|
)
|
|
|
|
chains["holder_health"] = ProviderChain(
|
|
"holder_health",
|
|
description="Holder distribution analysis - Gini, concentration, decentralization score",
|
|
providers=[
|
|
Provider(
|
|
"holder_analyzer",
|
|
ProviderTier.LOCAL,
|
|
holder_health_score,
|
|
weight=10.0,
|
|
rate_limit_rps=10.0,
|
|
)
|
|
],
|
|
)
|
|
|
|
chains["cross_chain_entity"] = ProviderChain(
|
|
"cross_chain_entity",
|
|
description="Cross-chain entity resolution via Arkham - trace wallets across all chains",
|
|
providers=[
|
|
Provider(
|
|
"entity_tracer",
|
|
ProviderTier.LOCAL,
|
|
cross_chain_entity,
|
|
weight=10.0,
|
|
rate_limit_rps=5.0,
|
|
)
|
|
],
|
|
)
|
|
|
|
chains["rug_patterns"] = ProviderChain(
|
|
"rug_patterns",
|
|
description="Rug pull pattern matcher - similarity scoring against 10 known scam patterns",
|
|
providers=[
|
|
Provider(
|
|
"rug_matcher",
|
|
ProviderTier.LOCAL,
|
|
rug_pattern_matcher,
|
|
weight=10.0,
|
|
rate_limit_rps=10.0,
|
|
)
|
|
],
|
|
)
|
|
|
|
chains["dev_reputation"] = ProviderChain(
|
|
"dev_reputation",
|
|
description="Developer reputation - deployer history, token count, entity resolution",
|
|
providers=[
|
|
Provider(
|
|
"dev_reputation",
|
|
ProviderTier.LOCAL,
|
|
developer_reputation,
|
|
weight=10.0,
|
|
rate_limit_rps=5.0,
|
|
)
|
|
],
|
|
)
|
|
|
|
chains["token_report"] = ProviderChain(
|
|
"token_report",
|
|
description="ONE-CALL enhanced token report - smart verdicts, entity enrichment, trust adjustments, tier-aware",
|
|
providers=[
|
|
Provider(
|
|
"report_generator",
|
|
ProviderTier.LOCAL,
|
|
enhanced_token_report,
|
|
weight=15.0,
|
|
rate_limit_rps=3.0,
|
|
)
|
|
],
|
|
)
|
|
|
|
chains["tier_comparison"] = ProviderChain(
|
|
"tier_comparison",
|
|
description="Competitive tier comparison - RugCharts vs DexScreener vs Nansen vs GMGN",
|
|
providers=[
|
|
Provider(
|
|
"tier_compare",
|
|
ProviderTier.LOCAL,
|
|
get_tier_comparison,
|
|
weight=1.0,
|
|
rate_limit_rps=60.0,
|
|
)
|
|
],
|
|
)
|
|
|
|
logger.info("RugCharts Intelligence: 10 premium chains registered")
|
|
except ImportError as e:
|
|
logger.warning(f"RugCharts Intelligence not available: {e}")
|
|
|
|
# ── NEWS & MARKET DATA - Free APIs ──
|
|
try:
|
|
from app.databus.news_provider import (
|
|
get_fear_greed,
|
|
get_full_news_feed,
|
|
get_market_brief,
|
|
get_market_prices,
|
|
get_prediction_markets,
|
|
get_trending_coins,
|
|
)
|
|
|
|
chains["live_prices"] = ProviderChain(
|
|
"live_prices",
|
|
description="Live crypto prices - CoinGecko free tier, multi-coin",
|
|
providers=[
|
|
Provider(
|
|
"coingecko_prices",
|
|
ProviderTier.LOCAL,
|
|
get_market_prices,
|
|
weight=10.0,
|
|
rate_limit_rps=10.0,
|
|
)
|
|
],
|
|
)
|
|
|
|
chains["trending_coins"] = ProviderChain(
|
|
"trending_coins",
|
|
description="Trending coins - CoinGecko search, top 10",
|
|
providers=[
|
|
Provider(
|
|
"coingecko_trending",
|
|
ProviderTier.LOCAL,
|
|
get_trending_coins,
|
|
weight=10.0,
|
|
rate_limit_rps=5.0,
|
|
)
|
|
],
|
|
)
|
|
|
|
chains["fear_greed"] = ProviderChain(
|
|
"fear_greed",
|
|
description="Crypto Fear & Greed Index - Alternative.me, free, no key",
|
|
providers=[
|
|
Provider(
|
|
"fear_greed_index",
|
|
ProviderTier.LOCAL,
|
|
get_fear_greed,
|
|
weight=10.0,
|
|
rate_limit_rps=10.0,
|
|
)
|
|
],
|
|
)
|
|
|
|
chains["prediction_markets"] = ProviderChain(
|
|
"prediction_markets",
|
|
description="Prediction market events - Polymarket, free, no key",
|
|
providers=[
|
|
Provider(
|
|
"polymarket_events",
|
|
ProviderTier.LOCAL,
|
|
get_prediction_markets,
|
|
weight=10.0,
|
|
rate_limit_rps=5.0,
|
|
)
|
|
],
|
|
)
|
|
|
|
chains["market_brief"] = ProviderChain(
|
|
"market_brief",
|
|
description="One-call market overview: prices + fear/greed + trending + prediction markets",
|
|
providers=[
|
|
Provider(
|
|
"market_briefing",
|
|
ProviderTier.LOCAL,
|
|
get_market_brief,
|
|
weight=10.0,
|
|
rate_limit_rps=5.0,
|
|
)
|
|
],
|
|
)
|
|
|
|
chains["full_news"] = ProviderChain(
|
|
"full_news",
|
|
description="Complete news feed: headlines + market data + fear/greed + polymarket predictions",
|
|
providers=[
|
|
Provider(
|
|
"full_news_feed",
|
|
ProviderTier.LOCAL,
|
|
get_full_news_feed,
|
|
weight=15.0,
|
|
rate_limit_rps=5.0,
|
|
)
|
|
],
|
|
)
|
|
|
|
logger.info(
|
|
"News & Market Data chains registered (6 new: prices, trending, fear_greed, prediction_markets, market_brief, full_news)"
|
|
)
|
|
except ImportError as e:
|
|
logger.warning(f"News providers not available: {e}")
|
|
|
|
# ── NEWS INTELLIGENCE ENGINE - multi-source, quality-scored, sentiment-tagged ──
|
|
try:
|
|
from app.databus.news_intel import (
|
|
add_comment,
|
|
add_reaction,
|
|
aggregate_all_news,
|
|
create_bb_post,
|
|
get_academic_papers,
|
|
get_reactions,
|
|
get_social_feed,
|
|
get_weekly_best,
|
|
)
|
|
|
|
chains["news_intel"] = ProviderChain(
|
|
"news_intel",
|
|
description="Complete news intelligence - 10+ sources, quality-scored, deduped, sentiment-tagged",
|
|
providers=[
|
|
Provider(
|
|
"news_engine",
|
|
ProviderTier.LOCAL,
|
|
aggregate_all_news,
|
|
weight=15.0,
|
|
rate_limit_rps=3.0,
|
|
)
|
|
],
|
|
)
|
|
|
|
chains["weekly_best"] = ProviderChain(
|
|
"weekly_best",
|
|
description="Curated weekly best - highest quality crypto journalism",
|
|
providers=[
|
|
Provider(
|
|
"weekly_curator",
|
|
ProviderTier.LOCAL,
|
|
get_weekly_best,
|
|
weight=10.0,
|
|
rate_limit_rps=5.0,
|
|
)
|
|
],
|
|
)
|
|
|
|
chains["academic_papers"] = ProviderChain(
|
|
"academic_papers",
|
|
description="Academic crypto/blockchain research papers from arXiv",
|
|
providers=[
|
|
Provider(
|
|
"arxiv_fetcher",
|
|
ProviderTier.LOCAL,
|
|
get_academic_papers,
|
|
weight=10.0,
|
|
rate_limit_rps=5.0,
|
|
)
|
|
],
|
|
)
|
|
|
|
chains["social_feed"] = ProviderChain(
|
|
"social_feed",
|
|
description="Crypto social feed - X/Twitter + CryptoPanic sentiment",
|
|
providers=[
|
|
Provider(
|
|
"social_aggregator",
|
|
ProviderTier.LOCAL,
|
|
get_social_feed,
|
|
weight=10.0,
|
|
rate_limit_rps=5.0,
|
|
)
|
|
],
|
|
)
|
|
|
|
chains["article_reactions"] = ProviderChain(
|
|
"article_reactions",
|
|
description="Article reactions - 🔥🐂🐻💎🧠🤡🚀💀 with counts",
|
|
providers=[
|
|
Provider(
|
|
"react_article",
|
|
ProviderTier.LOCAL,
|
|
add_reaction,
|
|
weight=5.0,
|
|
rate_limit_rps=30.0,
|
|
),
|
|
Provider(
|
|
"get_reactions",
|
|
ProviderTier.LOCAL,
|
|
get_reactions,
|
|
weight=5.0,
|
|
rate_limit_rps=60.0,
|
|
),
|
|
],
|
|
)
|
|
|
|
chains["article_comments"] = ProviderChain(
|
|
"article_comments",
|
|
description="Article comments - community discussion on any story",
|
|
providers=[
|
|
Provider(
|
|
"comment_article",
|
|
ProviderTier.LOCAL,
|
|
add_comment,
|
|
weight=5.0,
|
|
rate_limit_rps=20.0,
|
|
)
|
|
],
|
|
)
|
|
|
|
chains["bb_post"] = ProviderChain(
|
|
"bb_post",
|
|
description="Convert article to Bulletin Board post for community engagement",
|
|
providers=[
|
|
Provider(
|
|
"create_bb_post",
|
|
ProviderTier.LOCAL,
|
|
create_bb_post,
|
|
weight=5.0,
|
|
rate_limit_rps=10.0,
|
|
)
|
|
],
|
|
)
|
|
|
|
logger.info(
|
|
"News Intelligence chains registered (7: news_intel, weekly_best, academic_papers, social_feed, reactions, comments, bb_post)"
|
|
)
|
|
except ImportError as e:
|
|
logger.warning(f"News Intelligence not available: {e}")
|
|
|
|
# ── X/CT INTELLIGENCE - Crypto Twitter Rundown ──
|
|
try:
|
|
from app.databus.x_intel import fetch_ct_rundown, track_ct_accounts
|
|
|
|
chains["ct_rundown"] = ProviderChain(
|
|
"ct_rundown",
|
|
description="CT Rundown - top 20 Crypto Twitter stories, AI-summarized, category-diverse",
|
|
providers=[
|
|
Provider(
|
|
"ct_scanner",
|
|
ProviderTier.LOCAL,
|
|
fetch_ct_rundown,
|
|
weight=15.0,
|
|
rate_limit_rps=3.0,
|
|
)
|
|
],
|
|
)
|
|
|
|
chains["ct_accounts"] = ProviderChain(
|
|
"ct_accounts",
|
|
description="Curated CT account list - 35+ top accounts across 5 tiers",
|
|
providers=[
|
|
Provider(
|
|
"ct_account_list",
|
|
ProviderTier.LOCAL,
|
|
track_ct_accounts,
|
|
weight=1.0,
|
|
rate_limit_rps=60.0,
|
|
)
|
|
],
|
|
)
|
|
|
|
logger.info("X/CT Intelligence chains registered (2: ct_rundown, ct_accounts)")
|
|
except ImportError as e:
|
|
logger.warning(f"X/CT Intelligence not available: {e}")
|
|
|
|
# ── SOCIAL INTELLIGENCE - KOL tracking, shill detection, Daily Intel ──
|
|
try:
|
|
from app.databus.daily_intel import generate_daily_intel
|
|
from app.databus.social_intel import (
|
|
detect_shill_campaigns,
|
|
get_kol_leaderboard,
|
|
get_kol_profile,
|
|
get_shill_alerts,
|
|
get_social_metrics,
|
|
scan_scam_channels,
|
|
track_kol_call,
|
|
)
|
|
|
|
chains["kol_track"] = ProviderChain(
|
|
"kol_track",
|
|
description="KOL call tracking - record and analyze influencer token calls",
|
|
providers=[
|
|
Provider(
|
|
"kol_call_tracker",
|
|
ProviderTier.LOCAL,
|
|
track_kol_call,
|
|
weight=5.0,
|
|
rate_limit_rps=20.0,
|
|
)
|
|
],
|
|
)
|
|
|
|
chains["kol_profile"] = ProviderChain(
|
|
"kol_profile",
|
|
description="KOL performance profile - trust score, call history, win rate",
|
|
providers=[
|
|
Provider(
|
|
"kol_profiler",
|
|
ProviderTier.LOCAL,
|
|
get_kol_profile,
|
|
weight=5.0,
|
|
rate_limit_rps=30.0,
|
|
)
|
|
],
|
|
)
|
|
|
|
chains["kol_leaderboard"] = ProviderChain(
|
|
"kol_leaderboard",
|
|
description="KOL leaderboard - ranked by trust score and accuracy",
|
|
providers=[
|
|
Provider(
|
|
"kol_board",
|
|
ProviderTier.LOCAL,
|
|
get_kol_leaderboard,
|
|
weight=5.0,
|
|
rate_limit_rps=10.0,
|
|
)
|
|
],
|
|
)
|
|
|
|
chains["shill_detector"] = ProviderChain(
|
|
"shill_detector",
|
|
description="Shill campaign detection - coordinated promotion, paid content, pump-and-dump",
|
|
providers=[
|
|
Provider(
|
|
"shill_scanner",
|
|
ProviderTier.LOCAL,
|
|
detect_shill_campaigns,
|
|
weight=10.0,
|
|
rate_limit_rps=5.0,
|
|
),
|
|
Provider(
|
|
"shill_alerts",
|
|
ProviderTier.LOCAL,
|
|
get_shill_alerts,
|
|
weight=5.0,
|
|
rate_limit_rps=20.0,
|
|
),
|
|
],
|
|
)
|
|
|
|
chains["scam_monitor"] = ProviderChain(
|
|
"scam_monitor",
|
|
description="Scam channel monitor - Telegram/Discord scam pattern detection",
|
|
providers=[
|
|
Provider(
|
|
"scam_scanner",
|
|
ProviderTier.LOCAL,
|
|
scan_scam_channels,
|
|
weight=5.0,
|
|
rate_limit_rps=5.0,
|
|
)
|
|
],
|
|
)
|
|
|
|
chains["daily_intel"] = ProviderChain(
|
|
"daily_intel",
|
|
description="Daily Intelligence Briefing - OpenRouter free model research + writing, publish to X/Telegram/Ghost",
|
|
providers=[
|
|
Provider(
|
|
"intel_reporter",
|
|
ProviderTier.LOCAL,
|
|
generate_daily_intel,
|
|
weight=15.0,
|
|
rate_limit_rps=1.0,
|
|
)
|
|
],
|
|
)
|
|
|
|
chains["social_metrics"] = ProviderChain(
|
|
"social_metrics",
|
|
description="Social metrics aggregator - trending topics, sentiment, KOL activity",
|
|
providers=[
|
|
Provider(
|
|
"social_aggregator",
|
|
ProviderTier.LOCAL,
|
|
get_social_metrics,
|
|
weight=10.0,
|
|
rate_limit_rps=5.0,
|
|
)
|
|
],
|
|
)
|
|
|
|
logger.info(
|
|
"Social Intelligence chains registered (8: kol_track, kol_profile, kol_leaderboard, shill_detector, scam_monitor, daily_intel, social_metrics)"
|
|
)
|
|
except ImportError as e:
|
|
logger.warning(f"Social Intelligence not available: {e}")
|
|
|
|
# ── MODEL REGISTRY - Smart free model routing, quality review ──
|
|
try:
|
|
from app.databus.model_registry import ai_call, get_usage_stats, review_content
|
|
|
|
chains["ai_task"] = ProviderChain(
|
|
"ai_task",
|
|
description="AI task execution - smart routing across free models (research/writing/coding/review/fast)",
|
|
providers=[
|
|
Provider(
|
|
"ai_runner",
|
|
ProviderTier.LOCAL,
|
|
lambda **kw: ai_call(
|
|
kw.get("task_type", "fast"),
|
|
kw.get("system", ""),
|
|
kw.get("user", ""),
|
|
kw.get("max_tokens", 1000),
|
|
kw.get("temperature", 0.5),
|
|
),
|
|
weight=10.0,
|
|
rate_limit_rps=5.0,
|
|
)
|
|
],
|
|
)
|
|
|
|
chains["content_review"] = ProviderChain(
|
|
"content_review",
|
|
description="Content quality review - checks for AI-slop, forbidden words, human voice",
|
|
providers=[
|
|
Provider(
|
|
"quality_reviewer",
|
|
ProviderTier.LOCAL,
|
|
review_content,
|
|
weight=5.0,
|
|
rate_limit_rps=10.0,
|
|
)
|
|
],
|
|
)
|
|
|
|
chains["ai_usage"] = ProviderChain(
|
|
"ai_usage",
|
|
description="AI model usage statistics - track free tier consumption",
|
|
providers=[
|
|
Provider(
|
|
"usage_tracker",
|
|
ProviderTier.LOCAL,
|
|
get_usage_stats,
|
|
weight=1.0,
|
|
rate_limit_rps=60.0,
|
|
)
|
|
],
|
|
)
|
|
|
|
logger.info("Model Registry chains registered (3: ai_task, content_review, ai_usage)")
|
|
except ImportError as e:
|
|
logger.warning(f"Model Registry not available: {e}")
|
|
|
|
# ── RAG INGESTION - nightly indexing, health checks ──
|
|
try:
|
|
from app.databus.rag_ingestion import nightly_rag_index, rag_health_check
|
|
|
|
chains["rag_nightly"] = ProviderChain(
|
|
"rag_nightly",
|
|
description="Nightly RAG indexing - embeds news, CT, market, social data into vector store",
|
|
providers=[
|
|
Provider(
|
|
"rag_indexer",
|
|
ProviderTier.LOCAL,
|
|
nightly_rag_index,
|
|
weight=10.0,
|
|
rate_limit_rps=1.0,
|
|
)
|
|
],
|
|
)
|
|
|
|
chains["rag_health"] = ProviderChain(
|
|
"rag_health",
|
|
description="RAG system health - collection stats, doc counts, embedder status",
|
|
providers=[
|
|
Provider(
|
|
"rag_checker",
|
|
ProviderTier.LOCAL,
|
|
rag_health_check,
|
|
weight=5.0,
|
|
rate_limit_rps=30.0,
|
|
)
|
|
],
|
|
)
|
|
|
|
logger.info("RAG Ingestion chains registered (2: rag_nightly, rag_health)")
|
|
except ImportError as e:
|
|
logger.warning(f"RAG Ingestion not available: {e}")
|
|
|
|
# ── HYPERLIQUID - Perp markets and funding rates ──
|
|
async def _passthrough_hyperliquid(**kwargs) -> dict | None:
|
|
try:
|
|
limit = kwargs.get("limit", 10)
|
|
async with httpx.AsyncClient(timeout=10) as c:
|
|
r = await c.get(f"http://localhost:8000/api/v1/market/hyperliquid?limit={limit}")
|
|
if r.status_code == 200:
|
|
return r.json()
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
chains["hyperliquid"] = ProviderChain(
|
|
data_type="hyperliquid",
|
|
description="Hyperliquid perp markets and funding rates",
|
|
providers=[
|
|
Provider("rmi_hyperliquid", ProviderTier.LOCAL, _passthrough_hyperliquid, weight=10.0),
|
|
],
|
|
)
|
|
|
|
# ── HYPERLIQUID ACTION - Gain/Loss Porn & Squeezes ──
|
|
async def _passthrough_hyperliquid_action(**kwargs) -> dict | None:
|
|
try:
|
|
limit = kwargs.get("limit", 5)
|
|
async with httpx.AsyncClient(timeout=10) as c:
|
|
r = await c.get(f"http://localhost:8000/api/v1/market/hyperliquid-action?limit={limit}")
|
|
if r.status_code == 200:
|
|
return r.json()
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
chains["hyperliquid_action"] = ProviderChain(
|
|
data_type="hyperliquid_action",
|
|
description="Hyperliquid gain/loss porn: top movers, funding squeezes, highest volume",
|
|
providers=[
|
|
Provider(
|
|
"rmi_hyperliquid_action",
|
|
ProviderTier.LOCAL,
|
|
_passthrough_hyperliquid_action,
|
|
weight=10.0,
|
|
),
|
|
],
|
|
)
|
|
|
|
# ── INSIDER WALLETS - Premium intelligence ──
|
|
async def _passthrough_insider_wallets(**kwargs) -> dict | None:
|
|
try:
|
|
limit = kwargs.get("limit", 10)
|
|
tier = kwargs.get("tier", "free")
|
|
async with httpx.AsyncClient(timeout=10) as c:
|
|
r = await c.get(f"http://localhost:8000/api/v1/market/insider-wallets?limit={limit}&tier={tier}")
|
|
if r.status_code == 200:
|
|
return r.json()
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
chains["insider_wallets"] = ProviderChain(
|
|
data_type="insider_wallets",
|
|
description="Likely insider trader wallets to watch (Premium)",
|
|
providers=[
|
|
Provider("rmi_insider_wallets", ProviderTier.LOCAL, _passthrough_insider_wallets, weight=10.0),
|
|
],
|
|
)
|
|
|
|
# ── PREDICTION SIGNALS - Market intelligence layer ──
|
|
async def _passthrough_prediction_signals(**kwargs) -> dict | None:
|
|
try:
|
|
limit = kwargs.get("limit", 5)
|
|
tier = kwargs.get("tier", "free")
|
|
async with httpx.AsyncClient(timeout=15) as c:
|
|
r = await c.get(f"http://localhost:8000/api/v1/market/prediction-signals?limit={limit}&tier={tier}")
|
|
if r.status_code == 200:
|
|
return r.json()
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
chains["prediction_signals"] = ProviderChain(
|
|
data_type="prediction_signals",
|
|
description="Prediction market intelligence signals (Polymarket, Kalshi, etc.)",
|
|
providers=[
|
|
Provider(
|
|
"rmi_prediction_signals",
|
|
ProviderTier.LOCAL,
|
|
_passthrough_prediction_signals,
|
|
weight=10.0,
|
|
),
|
|
],
|
|
)
|
|
|
|
# Initialize circuit breakers and rate limiters
|
|
for chain in chains.values():
|
|
for provider in chain.providers:
|
|
if provider.name not in _circuit_breakers:
|
|
_circuit_breakers[provider.name] = _CircuitBreaker(
|
|
threshold=provider.failure_threshold, timeout=provider.recovery_timeout
|
|
)
|
|
if provider.name not in _rate_limiters:
|
|
_rate_limiters[provider.name] = _RateLimiter(rps=provider.rate_limit_rps)
|
|
|
|
logger.info(
|
|
f"Built {len(chains)} provider chains with {sum(len(c.providers) for c in chains.values())} total providers"
|
|
)
|
|
return chains
|