| """ |
| DataBus Router β Unified API Endpoints |
| ======================================== |
| |
| Replaces the fractured caching_shield/router, cache_manager stats, |
| and all connector-specific endpoints with a single clean interface. |
| |
| POST /api/v1/databus/fetch β Fetch data (any type) |
| GET /api/v1/databus/health β System health + cache stats |
| GET /api/v1/databus/capacity β Credit report + recommendations |
| GET /api/v1/databus/chains β List all provider chains |
| POST /api/v1/databus/invalidate β Clear cache (admin) |
| GET /api/v1/databus/vault/status β Key pool status (admin, no key values) |
| GET /api/v1/databus/vault/reload β Reload keys from vault (admin) |
| GET /api/v1/databus/fetch/{type} β GET convenience for simple queries |
| """ |
|
|
| import logging |
| import os |
|
|
| from fastapi import APIRouter, HTTPException, Query, Request |
|
|
| from app.databus.core import databus |
|
|
| logger = logging.getLogger("databus.router") |
|
|
| router = APIRouter(prefix="/api/v1/databus", tags=["databus"]) |
|
|
|
|
| def _verify_admin(request: Request) -> bool: |
| admin_key = os.getenv("ADMIN_API_KEY", "") |
| if not admin_key: |
| return True |
| provided = request.headers.get("X-Admin-Key", "") |
| return provided == admin_key |
|
|
|
|
| @router.post("/fetch") |
| async def databus_fetch(request: Request): |
| """ |
| Universal data fetch endpoint. |
| Every response is packaged based on who's asking. |
| No raw data leaks β consumers only get what they're authorized for. |
| |
| Body: { |
| "data_type": "token_price", # required |
| "admin_key": "", # for admin data |
| "force_fresh": false, # skip cache |
| "rag_index": false, # index in RAG |
| "consumer_type": "", # public_web/authenticated/premium/admin/mcp_tool/x402_paid |
| "tool_id": "", # MCP tool ID for scoping |
| "x402_tier": "", # x402 pricing tier: free/basic/premium/enterprise |
| "mint": "So111...", # type-specific params |
| "address": "0x...", # type-specific params |
| ... |
| } |
| """ |
| body = await request.json() |
| data_type = body.pop("data_type", None) |
| if not data_type: |
| raise HTTPException(400, "data_type is required") |
|
|
| admin_key = body.pop("admin_key", "") |
| force_fresh = body.pop("force_fresh", False) |
| rag_index = body.pop("rag_index", False) |
| consumer_type = body.pop("consumer_type", "") |
| tool_id = body.pop("tool_id", "") |
| x402_tier = body.pop("x402_tier", "") |
|
|
| result = await databus.fetch( |
| data_type=data_type, |
| admin_key=admin_key, |
| force_fresh=force_fresh, |
| rag_index=rag_index, |
| consumer_type=consumer_type, |
| tool_id=tool_id, |
| x402_tier=x402_tier, |
| request=request, |
| **body, |
| ) |
| if result is None: |
| raise HTTPException(502, f"No data available for {data_type}") |
| return result |
|
|
|
|
| @router.get("/fetch/{data_type}") |
| async def databus_fetch_get( |
| data_type: str, |
| request: Request, |
| admin_key: str = Query(default=""), |
| force_fresh: bool = Query(default=False), |
| ): |
| """GET convenience for simple queries. Params passed as query args.""" |
| |
| kwargs = {} |
| for key, value in request.query_params.items(): |
| if key not in ("admin_key", "force_fresh"): |
| kwargs[key] = value |
|
|
| result = await databus.fetch(data_type=data_type, admin_key=admin_key, force_fresh=force_fresh, **kwargs) |
| if result is None: |
| raise HTTPException(502, f"No data available for {data_type}") |
| return result |
|
|
|
|
| @router.get("/health") |
| async def databus_health(): |
| """Full system health: cache, vault, chains, stats.""" |
| return await databus.health() |
|
|
|
|
| @router.get("/capacity") |
| async def databus_capacity(): |
| """Credit report and recommendations for free tier expansion.""" |
| if not databus._initialized: |
| await databus.initialize() |
| return databus.capacity_report() |
|
|
|
|
| @router.get("/chains") |
| async def databus_chains(): |
| """List all available data types and their provider fallback chains.""" |
| if not databus._initialized: |
| await databus.initialize() |
| return databus.list_chains() |
|
|
|
|
| @router.get("/access-matrix") |
| async def databus_access_matrix(request: Request): |
| """View the full access control matrix. Admin key required.""" |
| if not _verify_admin(request): |
| from app.databus.access_control import access_controller |
|
|
| |
| return {"note": "Full matrix requires admin key. Your access depends on your consumer type."} |
| from app.databus.access_control import access_controller |
|
|
| return access_controller.list_access_matrix() |
|
|
|
|
| @router.get("/mcp-scope/{tool_id}") |
| async def databus_mcp_scope(tool_id: str): |
| """Get the data types an MCP tool is authorized to access.""" |
| from app.databus.access_control import access_controller |
|
|
| allowed = access_controller.get_mcp_allowed_types(tool_id) |
| if not allowed: |
| return {"tool_id": tool_id, "allowed_types": [], "note": "Unknown tool or no scope defined"} |
| return {"tool_id": tool_id, "allowed_types": allowed} |
|
|
|
|
| @router.get("/x402-scope/{tier}") |
| async def databus_x402_scope(tier: str): |
| """Get the data types an x402 pricing tier can access.""" |
| from app.databus.access_control import access_controller |
|
|
| allowed = access_controller.get_x402_allowed_types(tier) |
| return {"tier": tier, "allowed_types": allowed} |
|
|
|
|
| @router.post("/invalidate") |
| async def databus_invalidate(request: Request): |
| """Clear cache. Admin key required.""" |
| if not _verify_admin(request): |
| raise HTTPException(401, "Admin key required") |
|
|
| body = await request.json() if request.headers.get("content-type") == "application/json" else {} |
| data_type = body.get("data_type") |
|
|
| if data_type: |
| await databus.invalidate(data_type, **{k: v for k, v in body.items() if k != "data_type"}) |
| return {"status": "invalidated", "data_type": data_type} |
| else: |
| await databus.invalidate_all() |
| return {"status": "all_cache_cleared"} |
|
|
|
|
| @router.get("/vault/status") |
| async def databus_vault_status(request: Request): |
| """Key pool status. NEVER exposes key values. Admin key required.""" |
| if not _verify_admin(request): |
| raise HTTPException(401, "Admin key required") |
|
|
| if not databus.vault: |
| await databus.initialize() |
| return databus.vault.status() |
|
|
|
|
| @router.get("/vault/capacity") |
| async def databus_vault_capacity(request: Request): |
| """Detailed capacity report with free tier recommendations. Admin key required.""" |
| if not _verify_admin(request): |
| raise HTTPException(401, "Admin key required") |
|
|
| if not databus.vault: |
| await databus.initialize() |
| return databus.vault.capacity_report() |
|
|
|
|
| @router.post("/vault/reload") |
| async def databus_vault_reload(request: Request): |
| """Reload keys from vault without restart. Admin key required.""" |
| if not _verify_admin(request): |
| raise HTTPException(401, "Admin key required") |
|
|
| await databus.vault.reload() |
| return {"status": "reloaded", "providers": len(databus.vault.pools)} |
|
|
|
|
| @router.post("/vault/add-key") |
| async def databus_vault_add_key(request: Request): |
| """Hot-add an API key to a provider pool. Admin key required.""" |
| if not _verify_admin(request): |
| raise HTTPException(401, "Admin key required") |
|
|
| body = await request.json() |
| provider = body.get("provider") |
| key_name = body.get("key_name") |
| key_value = body.get("key_value") |
|
|
| if not all([provider, key_name, key_value]): |
| raise HTTPException(400, "provider, key_name, key_value required") |
|
|
| await databus.vault.add_key(provider, key_name, key_value) |
| return {"status": "added", "provider": provider, "key_name": key_name} |
|
|
|
|
| @router.post("/vault/reset-monthly") |
| async def databus_vault_reset_monthly(request: Request): |
| """Reset monthly call counters. Admin key required.""" |
| if not _verify_admin(request): |
| raise HTTPException(401, "Admin key required") |
|
|
| databus.vault.reset_monthly_counters() |
| return {"status": "monthly_counters_reset"} |
|
|
|
|
| |
|
|
|
|
| @router.get("/arkham/entity/{address}") |
| async def arkham_entity(address: str, request: Request): |
| """Resolve a wallet address to an Arkham entity. Admin key required.""" |
| admin_key = request.headers.get("X-Admin-Key", "") |
| result = await databus.fetch("entity_intel", address=address, admin_key=admin_key) |
| if result is None: |
| raise HTTPException(502, f"Could not resolve entity for {address}") |
| return result |
|
|
|
|
| @router.get("/arkham/labels") |
| async def arkham_labels(request: Request): |
| """Look up labels for addresses. Admin key required for Arkham, public for local.""" |
| admin_key = request.headers.get("X-Admin-Key", "") |
| address = request.query_params.get("address", "") |
| result = await databus.fetch("wallet_labels", address=address, admin_key=admin_key) |
| if result is None: |
| raise HTTPException(502, "No labels found") |
| return result |
|
|
|
|
| @router.get("/arkham/portfolio/{address}") |
| async def arkham_portfolio(address: str, request: Request): |
| """Get portfolio for an entity. Admin key required.""" |
| admin_key = request.headers.get("X-Admin-Key", "") |
| result = await databus.fetch("arkham_portfolio", address=address, admin_key=admin_key) |
| if result is None: |
| raise HTTPException(502, f"Could not get portfolio for {address}") |
| return result |
|
|
|
|
| |
|
|
|
|
| @router.get("/token/{mint}") |
| async def token_detail(mint: str, request: Request): |
| """Rich token detail (metadata + price + security).""" |
| result = await databus.fetch("token_detail", mint=mint) |
| if result is None: |
| raise HTTPException(502, f"No data for token {mint}") |
| return result |
|
|
|
|
| @router.get("/wallet/{address}/tokens") |
| async def wallet_tokens(address: str, request: Request): |
| """Wallet token holdings.""" |
| result = await databus.fetch("wallet_tokens", address=address) |
| if result is None: |
| raise HTTPException(502, f"No token data for wallet {address}") |
| return result |
|
|
|
|
| @router.get("/wallet/{address}/pnl") |
| async def wallet_pnl(address: str, request: Request): |
| """Wallet profit/loss.""" |
| result = await databus.fetch("wallet_pnl", address=address) |
| if result is None: |
| raise HTTPException(502, f"No PnL data for wallet {address}") |
| return result |
|
|
|
|
| @router.get("/bubble-map/{address}") |
| async def bubble_map(address: str, request: Request, chain: str = "solana", depth: int = 2): |
| """Interactive holder bubble map.""" |
| result = await databus.fetch("bubble_map", address=address, chain=chain, depth=depth) |
| if result is None: |
| raise HTTPException(502, f"No bubble map data for {address}") |
| return result |
|
|
|
|
| @router.get("/rugmaps/{address}") |
| async def rugmaps_analysis(address: str, request: Request, chain: str = "solana"): |
| """RugMaps AI analysis + similar scam search.""" |
| result = await databus.fetch("rugmaps_analysis", address=address, chain=chain) |
| if result is None: |
| raise HTTPException(502, f"No RugMaps analysis for {address}") |
| return result |
|
|
|
|
| @router.get("/threat/{address}") |
| async def threat_check(address: str, request: Request, chain_id: str = "1"): |
| """Threat intelligence check (CryptoScamDB + GoPlus + Januus).""" |
| result = await databus.fetch("threat_check", address=address, chain_id=chain_id) |
| if result is None: |
| raise HTTPException(502, f"No threat data for {address}") |
| return result |
|
|
|
|
| @router.get("/contract-scan/{address}") |
| async def contract_scan(address: str, request: Request, chain: str = "base"): |
| """Smart contract deep scan (Slither + Mythril).""" |
| result = await databus.fetch("contract_scan", address=address, chain=chain) |
| if result is None: |
| raise HTTPException(502, f"No contract scan data for {address}") |
| return result |
|
|
|
|
| @router.get("/sentinel-deep/{address}") |
| async def sentinel_deep(address: str, request: Request, chain: str = "solana"): |
| """Deep SENTINEL scan + threat feeds + contract analysis.""" |
| admin_key = request.headers.get("X-Admin-Key", "") |
| result = await databus.fetch("sentinel_deep", address=address, chain=chain, admin_key=admin_key) |
| if result is None: |
| raise HTTPException(502, f"No deep scan data for {address}") |
| return result |
|
|
|
|
| @router.get("/prediction-markets") |
| async def prediction_markets(request: Request, query: str = "", category: str = ""): |
| """Prediction market intel (Polymarket + Kalshi).""" |
| result = await databus.fetch("prediction_markets", query=query, category=category) |
| if result is None: |
| raise HTTPException(502, "No prediction market data available") |
| return result |
|
|
|
|
| @router.get("/prediction-signals") |
| async def prediction_signals(request: Request): |
| """Auto-detected trading signals from prediction markets.""" |
| result = await databus.fetch("prediction_signals") |
| if result is None: |
| raise HTTPException(502, "No prediction signals available") |
| return result |
|
|
|
|
| |
| |
| |
|
|
|
|
| @router.post("/batch") |
| async def databus_batch(request: Request): |
| """#7 Batch DataBus β fetch multiple data types in one call. |
| |
| Body: { |
| "requests": [ |
| {"data_type": "token_price", "mint": "So111..."}, |
| {"data_type": "wallet_labels", "address": "0x..."}, |
| ... |
| ], |
| "admin_key": "", # optional, applies to all |
| "consumer_type": "", # optional, applies to all |
| } |
| Returns: array of results in same order as requests. |
| """ |
| body = await request.json() |
| requests_list = body.get("requests", []) |
| if not requests_list: |
| raise HTTPException(400, "requests array is required") |
| if len(requests_list) > 20: |
| raise HTTPException(400, "Maximum 20 requests per batch") |
|
|
| admin_key = body.get("admin_key", "") |
| consumer_type = body.get("consumer_type", "") |
|
|
| |
| for req in requests_list: |
| if admin_key and "admin_key" not in req: |
| req["admin_key"] = admin_key |
| if consumer_type and "consumer_type" not in req: |
| req["consumer_type"] = consumer_type |
| req["request"] = request |
|
|
| results = await databus.fetch_batch(requests_list) |
| return {"results": results, "count": len(results)} |
|
|
|
|
| @router.post("/warm/start") |
| async def databus_warm_start(request: Request): |
| """Start cache warm background task. Admin key required.""" |
| if not _verify_admin(request): |
| raise HTTPException(401, "Admin key required") |
| body = await request.json() if request.headers.get("content-type") == "application/json" else {} |
| interval = body.get("interval_seconds", 30) |
| await databus.start_cache_warm(interval_seconds=interval) |
| return {"status": "started", "interval_seconds": interval} |
|
|
|
|
| @router.post("/warm/stop") |
| async def databus_warm_stop(request: Request): |
| """Stop cache warm background task. Admin key required.""" |
| if not _verify_admin(request): |
| raise HTTPException(401, "Admin key required") |
| await databus.stop_cache_warm() |
| return {"status": "stopped"} |
|
|
|
|
| @router.get("/providers/health") |
| async def databus_providers_health(request: Request): |
| """#5 Provider Health Dashboard β per-provider success/failure/latency/circuit status.""" |
| if not _verify_admin(request): |
| raise HTTPException(401, "Admin key required") |
| if not databus._initialized: |
| await databus.initialize() |
| return { |
| "provider_health": databus._provider_health.all_health(), |
| "deduplication": databus._dedup.stats(), |
| } |
|
|
|
|
| @router.get("/cache/stats") |
| async def databus_cache_stats(request: Request): |
| """#10 Per-type cache stats with TTL tuning suggestions.""" |
| if not _verify_admin(request): |
| raise HTTPException(401, "Admin key required") |
| return { |
| "per_type": databus.cache.type_stats(), |
| "tuning_alerts": [ |
| {"data_type": dt, **stats} for dt, stats in databus.cache.type_stats().items() if stats.get("suggestion") |
| ], |
| } |
|
|
|
|
| @router.post("/schema/validate") |
| async def databus_schema_validate(request: Request): |
| """#15 Validate a DataBus response against expected schema. |
| |
| Body: {"data_type": "token_price", "data": {"price_usd": 1.23, ...}} |
| Returns: {valid: bool, missing_fields: [...]} |
| """ |
| body = await request.json() |
| data_type = body.get("data_type", "") |
| data = body.get("data", {}) |
| from app.databus.response_schema import schema_validator |
|
|
| is_valid, missing = schema_validator.validate(data_type, data) |
| return {"data_type": data_type, "valid": is_valid, "missing_fields": missing} |
|
|
|
|
| |
|
|
|
|
| @router.get("/social/x/profile/{username}") |
| async def social_x_profile(username: str): |
| """Get X/Twitter user profile β cached 24h.""" |
| from app.databus.social import SocialDataAggregator |
|
|
| agg = SocialDataAggregator(databus.cache) |
| result = await agg.x.get_user(username) |
| if not result: |
| raise HTTPException(404, f"User @{username} not found or API unavailable") |
| return {"source": "x_twitter", "username": username, "data": result} |
|
|
|
|
| @router.get("/social/x/tweets/{username}") |
| async def social_x_tweets(username: str, count: int = Query(20, ge=1, le=100)): |
| """Get recent tweets from user β cached 15min.""" |
| from app.databus.social import SocialDataAggregator |
|
|
| agg = SocialDataAggregator(databus.cache) |
| result = await agg.get_our_tweets(count=count) if username.lower() == "cryptorugmunch" else None |
| if result is None: |
| |
| profile = await agg.x.get_user(username) |
| if profile: |
| result = await agg.x.get_user_tweets(profile["id"], max_results=count) |
| if not result: |
| raise HTTPException(404, f"Tweets for @{username} not available") |
| return {"source": "x_twitter", "username": username, "count": len(result), "data": result} |
|
|
|
|
| @router.get("/social/x/mentions") |
| async def social_x_mentions(count: int = Query(20, ge=1, le=100)): |
| """Get mentions of @CryptoRugMunch β cached 15min.""" |
| from app.databus.social import SocialDataAggregator |
|
|
| agg = SocialDataAggregator(databus.cache) |
| result = await agg.get_our_mentions(count=count) |
| if not result: |
| raise HTTPException(404, "Mentions not available (API budget exhausted)") |
| return {"source": "x_twitter", "count": len(result), "data": result} |
|
|
|
|
| @router.get("/social/x/engagement") |
| async def social_x_engagement(tweet_ids: str = Query(..., description="Comma-separated tweet IDs")): |
| """Get engagement metrics for tweets β cached 1h.""" |
| from app.databus.social import SocialDataAggregator |
|
|
| agg = SocialDataAggregator(databus.cache) |
| ids = [tid.strip() for tid in tweet_ids.split(",")[:100]] |
| result = await agg.x.get_engagement_metrics(ids) |
| return {"source": "x_twitter", "count": len(result), "data": result} |
|
|
|
|
| @router.get("/social/x/search") |
| async def social_x_search(q: str = Query(..., description="Search query"), count: int = Query(10, ge=1, le=100)): |
| """Search X/Twitter β VERY expensive, cached 24h. Use sparingly.""" |
| from app.databus.social import SocialDataAggregator |
|
|
| agg = SocialDataAggregator(databus.cache) |
| result = await agg.search_mentions(q, count=count) |
| if not result: |
| raise HTTPException(404, "Search results not available (API budget exhausted or no results)") |
| return {"source": "x_twitter", "query": q, "count": len(result), "data": result} |
|
|
|
|
| @router.get("/social/kol/{username}") |
| async def social_kol_reputation(username: str): |
| """KOL reputation score β cached 24h.""" |
| from app.databus.social import SocialDataAggregator |
|
|
| agg = SocialDataAggregator(databus.cache) |
| return await agg.get_kol_reputation(username) |
|
|
|
|
| @router.get("/social/sentiment") |
| async def social_sentiment(username: str = Query("CryptoRugMunch")): |
| """Brand sentiment analysis β cached 1h.""" |
| from app.databus.social import SocialDataAggregator |
|
|
| agg = SocialDataAggregator(databus.cache) |
| return await agg.get_sentiment(username) |
|
|
|
|
| @router.get("/social/x/budget") |
| async def social_x_budget(): |
| """Current X API read budget status.""" |
| from app.databus.social import XTwitterProvider |
|
|
| provider = XTwitterProvider(databus.cache) |
| return { |
| "daily_reads_used": provider._daily_reads, |
| "daily_reads_remaining": X_DAILY_READ_BUDGET - provider._daily_reads, |
| "monthly_free_limit": X_FREE_MONTHLY_READ_LIMIT, |
| "daily_budget": X_DAILY_READ_BUDGET, |
| "note": "X Free tier: 10k reads/month. Cache aggressively.", |
| } |
|
|
|
|
| X_HANDLE = "CryptoRugMunch" |
|
|
|
|
| @router.get("/social/x/discover") |
| async def social_x_discover(handle: str = Query(X_HANDLE), limit: int = Query(50, ge=1, le=100)): |
| """Discover tweets via web search β no API needed, works for free.""" |
| from app.databus.social_scraper import XWebScraper |
|
|
| scraper = XWebScraper(databus.cache) |
| tweets = await scraper.discover_tweets(handle=handle, limit=limit) |
| return {"source": "web_search", "handle": handle, "count": len(tweets), "data": tweets} |
|
|
|
|
| @router.get("/social/x/engagement-report") |
| async def social_x_engagement_report(handle: str = Query(X_HANDLE)): |
| """Get engagement report for a handle β avg likes, best tweets, posting frequency.""" |
| from app.databus.social_scraper import XWebScraper |
|
|
| scraper = XWebScraper(databus.cache) |
| report = await scraper.get_engagement_report(handle=handle) |
| return report |
|
|
|
|
| @router.get("/social/x/mentions-discover") |
| async def social_x_mentions_discover(handle: str = Query(X_HANDLE), limit: int = Query(20, ge=1, le=50)): |
| """Find tweets mentioning a handle β via web search.""" |
| from app.databus.social_scraper import XWebScraper |
|
|
| scraper = XWebScraper(databus.cache) |
| mentions = await scraper.get_mentions(handle=handle, limit=limit) |
| return {"source": "web_search", "handle": handle, "count": len(mentions), "data": mentions} |
|
|
|
|
| @router.get("/social/x/trending") |
| async def social_x_trending(): |
| """Get current crypto trending topics from web search.""" |
| from app.databus.social_scraper import XWebScraper |
|
|
| scraper = XWebScraper(databus.cache) |
| topics = await scraper.get_trending_topics() |
| return {"source": "web_search", "count": len(topics), "data": topics} |
|
|
|
|
| |
|
|
|
|
| @router.get("/bitquery/health") |
| async def bitquery_health(): |
| """Check Bitquery provider health and billing status.""" |
| from app.databus.bitquery_provider import BitqueryProvider |
|
|
| provider = BitqueryProvider(databus.cache) |
| return await provider.health() |
|
|
|
|
| @router.get("/bitquery/token-price/{network}/{token_address}") |
| async def bitquery_token_price(network: str, token_address: str): |
| """Get DEX token price from Bitquery.""" |
| from app.databus.bitquery_provider import BitqueryProvider |
|
|
| provider = BitqueryProvider(databus.cache) |
| result = await provider.get_token_price(network, token_address) |
| if result and "error" in result: |
| raise HTTPException(status_code=503, detail=result) |
| return {"source": "bitquery", "network": network, "token": token_address, "data": result} |
|
|
|
|
| @router.get("/bitquery/holders/{network}/{token_address}") |
| async def bitquery_holders(network: str, token_address: str, limit: int = Query(100, ge=1, le=500)): |
| """Get token holder distribution from Bitquery.""" |
| from app.databus.bitquery_provider import BitqueryProvider |
|
|
| provider = BitqueryProvider(databus.cache) |
| result = await provider.get_holder_distribution(network, token_address, limit) |
| if result and "error" in result: |
| raise HTTPException(status_code=503, detail=result) |
| return {"source": "bitquery", "network": network, "token": token_address, "data": result} |
|
|
|
|
| @router.get("/bitquery/tx-trace/{network}/{tx_hash}") |
| async def bitquery_tx_trace(network: str, tx_hash: str): |
| """Get full transaction trace from Bitquery.""" |
| from app.databus.bitquery_provider import BitqueryProvider |
|
|
| provider = BitqueryProvider(databus.cache) |
| result = await provider.get_transaction_trace(network, tx_hash) |
| if result and "error" in result: |
| raise HTTPException(status_code=503, detail=result) |
| return {"source": "bitquery", "network": network, "tx": tx_hash, "data": result} |
|
|
|
|
| @router.get("/bitquery/dex-volume/{network}") |
| async def bitquery_dex_volume(network: str, pool: str = Query(None), timeframe: str = Query("24h")): |
| """Get DEX trading volume from Bitquery.""" |
| from app.databus.bitquery_provider import BitqueryProvider |
|
|
| provider = BitqueryProvider(databus.cache) |
| result = await provider.get_dex_volume(network, pool, timeframe) |
| if result and "error" in result: |
| raise HTTPException(status_code=503, detail=result) |
| return { |
| "source": "bitquery", |
| "network": network, |
| "pool": pool, |
| "timeframe": timeframe, |
| "data": result, |
| } |
|
|
|
|
| @router.get("/bitquery/balance/{network}/{address}") |
| async def bitquery_balance(network: str, address: str): |
| """Get address token balances from Bitquery.""" |
| from app.databus.bitquery_provider import BitqueryProvider |
|
|
| provider = BitqueryProvider(databus.cache) |
| result = await provider.get_address_balance(network, address) |
| if result and "error" in result: |
| raise HTTPException(status_code=503, detail=result) |
| return {"source": "bitquery", "network": network, "address": address, "data": result} |
|
|
|
|
| @router.get("/bitquery/cross-chain/{address}") |
| async def bitquery_cross_chain(address: str, networks: str = Query("ethereum,bsc,solana")): |
| """Track token transfers across chains from Bitquery.""" |
| from app.databus.bitquery_provider import BitqueryProvider |
|
|
| provider = BitqueryProvider(databus.cache) |
| net_list = [n.strip() for n in networks.split(",")] |
| result = await provider.get_cross_chain_transfers(address, net_list) |
| if result and "error" in result: |
| raise HTTPException(status_code=503, detail=result) |
| return {"source": "bitquery", "address": address, "networks": net_list, "data": result} |
|
|
|
|
| @router.get("/bitquery/contract-events/{network}/{contract}") |
| async def bitquery_contract_events( |
| network: str, contract: str, event: str = Query(None), limit: int = Query(50, ge=1, le=200) |
| ): |
| """Get smart contract events from Bitquery.""" |
| from app.databus.bitquery_provider import BitqueryProvider |
|
|
| provider = BitqueryProvider(databus.cache) |
| result = await provider.get_smart_contract_events(network, contract, event, limit) |
| if result and "error" in result: |
| raise HTTPException(status_code=503, detail=result) |
| return {"source": "bitquery", "network": network, "contract": contract, "data": result} |
|
|
|
|
| |
| |
| |
|
|
|
|
| @router.post("/webhooks/{service}") |
| async def receive_webhook(service: str, request: Request): |
| """Universal webhook receiver. Routes to correct handler based on service. |
| |
| Supported: arkham, helius, moralis, alchemy, custom |
| |
| Auto-validates signatures, deduplicates, caches in DataBus, |
| indexes in RAG, triggers premium scanner, pushes alerts. |
| """ |
| try: |
| from app.databus.webhooks import handle_webhook |
| except ImportError: |
| raise HTTPException(501, "Webhook system not available") |
|
|
| raw_body = await request.body() |
| headers = dict(request.headers) |
|
|
| try: |
| payload = await request.json() |
| except Exception: |
| payload = {} |
|
|
| result = await handle_webhook(service, payload, headers, raw_body) |
|
|
| if "error" in result and result.get("status") != "duplicate": |
| raise HTTPException(400, result["error"]) |
|
|
| return result |
|
|
|
|
| @router.get("/webhooks") |
| async def list_webhooks_endpoint(): |
| """List all recent webhook events.""" |
| try: |
| from app.databus.webhooks import list_webhooks |
|
|
| return await list_webhooks() |
| except ImportError: |
| raise HTTPException(501, "Webhook system not available") |
|
|
|
|
| @router.post("/webhooks/setup/{service}") |
| async def setup_webhook_endpoint(service: str, request: Request): |
| """Programmatically register a webhook with a service. |
| |
| Body: { |
| "webhook_url": "https://rugmunch.io/api/v1/databus/webhooks/helius", |
| "events": ["transaction", "token_transfer"], |
| "addresses": ["0x..."], |
| "api_key": "optional-override" |
| } |
| """ |
| try: |
| from app.databus.webhooks import setup_webhook |
| except ImportError: |
| raise HTTPException(501, "Webhook system not available") |
|
|
| body = await request.json() |
| result = await setup_webhook( |
| service=service, |
| webhook_url=body.get("webhook_url", ""), |
| events=body.get("events"), |
| api_key=body.get("api_key", ""), |
| addresses=body.get("addresses", []), |
| chains=body.get("chains", []), |
| ) |
| return result |
|
|
|
|
| |
| |
| |
|
|
|
|
| @router.get("/premium/bundles/{address}") |
| async def premium_bundle_detect(address: str, chain: str = "solana", request: Request = None): |
| """Bubblemaps-style bundle detection. Premium tier.""" |
| return await databus.fetch("bundle_detect", address=address, chain=chain) |
|
|
|
|
| @router.get("/premium/clusters/{address}") |
| async def premium_cluster_map(address: str, chain: str = "solana", depth: int = 3, request: Request = None): |
| """Full wallet cluster mapping with graph-ready nodes/edges. Premium tier.""" |
| return await databus.fetch("cluster_map", address=address, chain=chain, depth=depth) |
|
|
|
|
| @router.get("/premium/dev-finder/{token}") |
| async def premium_dev_finder(token: str, chain: str = "solana", request: Request = None): |
| """Find developer/creator wallets behind a token. Premium tier.""" |
| return await databus.fetch("dev_finder", token_address=token, chain=chain) |
|
|
|
|
| @router.get("/premium/snipers/{address}") |
| async def premium_sniper_detect(address: str, chain: str = "solana", request: Request = None): |
| """Detect snipers β first-block buyers with fast dumps. Premium tier.""" |
| return await databus.fetch("sniper_detect", address=address, chain=chain) |
|
|
|
|
| @router.get("/premium/bot-farms/{address}") |
| async def premium_bot_farms(address: str, chain: str = "solana", request: Request = None): |
| """Detect bot farms β identical behavior patterns. Premium tier.""" |
| return await databus.fetch("bot_farm_detect", address=address, chain=chain) |
|
|
|
|
| @router.get("/premium/copy-trading/{address}") |
| async def premium_copy_trading(address: str, chain: str = "solana", request: Request = None): |
| """Detect copy trading patterns. Premium tier.""" |
| return await databus.fetch("copy_trade_detect", address=address, chain=chain) |
|
|
|
|
| @router.get("/premium/insider-signals/{address}") |
| async def premium_insider(address: str, chain: str = "solana", request: Request = None): |
| """Detect insider trading signals. Premium tier.""" |
| return await databus.fetch("insider_detect", address=address, chain=chain) |
|
|
|
|
| @router.get("/premium/wash-trading/{address}") |
| async def premium_wash_trading(address: str, chain: str = "solana", request: Request = None): |
| """Detect wash trading patterns. Premium tier.""" |
| return await databus.fetch("wash_trade_detect", address=address, chain=chain) |
|
|
|
|
| @router.get("/premium/mev/{address}") |
| async def premium_mev(address: str, chain: str = "solana", request: Request = None): |
| """Detect MEV sandwich attacks. Premium tier.""" |
| return await databus.fetch("mev_detect", address=address, chain=chain) |
|
|
|
|
| @router.get("/premium/fresh-wallets/{address}") |
| async def premium_fresh_wallets(address: str, chain: str = "solana", request: Request = None): |
| """Analyze fresh wallet concentration β high new-wallet % = rug risk. Premium tier.""" |
| return await databus.fetch("fresh_wallet_analysis", address=address, chain=chain) |
|
|
|
|
| |
| |
| |
|
|
|
|
| @router.get("/premium/volume-authenticity/{address}") |
| async def premium_volume_auth(address: str, chain: str = "ethereum", request: Request = None): |
| """Fake volume % with bootstrap CI. The RugCharts moat β no one else does this.""" |
| return await databus.fetch( |
| "volume_authenticity", |
| address=address, |
| chain=chain, |
| volume_24h=request.query_params.get("volume_24h", "0"), |
| liquidity_usd=request.query_params.get("liquidity_usd", "0"), |
| unique_wallets=request.query_params.get("unique_wallets", "0"), |
| buy_count=request.query_params.get("buy_count", "0"), |
| sell_count=request.query_params.get("sell_count", "0"), |
| tx_count=request.query_params.get("tx_count", "0"), |
| ) |
|
|
|
|
| @router.get("/ohlcv/{address}") |
| async def ohlcv_candles( |
| address: str, |
| chain: str = "ethereum", |
| timeframe: str = "1h", |
| limit: int = 100, |
| request: Request = None, |
| ): |
| """OHLCV candlestick data with authenticity scoring baked in.""" |
| return await databus.fetch("ohlcv", token=address, chain=chain, timeframe=timeframe, limit=limit) |
|
|
|
|
| @router.get("/premium/security-scan/{address}") |
| async def premium_security_scan(address: str, chain: str = "ethereum", request: Request = None): |
| """37+ security checks β GoPlus, honeypot, contract, liquidity, holders, rug pull indicators.""" |
| return await databus.fetch("token_security", address=address, chain=chain) |
|
|
|
|
| @router.get("/security-checks") |
| async def security_check_matrix(): |
| """Full list of all 37+ security checks with weights and descriptions.""" |
| return await databus.fetch("token_security", action="matrix") |
|
|
|
|
| |
| |
| |
|
|
|
|
| @router.get("/premium/smart-money") |
| async def smart_money_endpoint(chain: str = "solana", limit: int = 20): |
| """What profitable wallets are buying right now β with entity labels.""" |
| return await databus.fetch("smart_money", chain=chain, limit=limit) |
|
|
|
|
| @router.get("/premium/whale-alerts/{address}") |
| async def whale_alerts_endpoint(address: str, chain: str = "solana", min_value: float = 100000): |
| """Real-time large transaction detection for a token or wallet.""" |
| return await databus.fetch("whale_alerts", address=address, chain=chain, min_value_usd=min_value) |
|
|
|
|
| @router.get("/premium/token-launches") |
| async def token_launches_endpoint(chain: str = "solana", limit: int = 50): |
| """Newly launched tokens with instant risk scoring by age.""" |
| return await databus.fetch("token_launches", chain=chain, limit=limit) |
|
|
|
|
| @router.get("/premium/insider-detection/{address}") |
| async def insider_detection_endpoint(address: str, chain: str = "solana"): |
| """Detect pre-pump accumulation β volume spikes before major price moves.""" |
| return await databus.fetch("insider_detection", address=address, chain=chain) |
|
|
|
|
| @router.get("/premium/liquidity-risk/{address}") |
| async def liquidity_risk_endpoint(address: str, chain: str = "solana"): |
| """LP health: concentration risk, lock status, holder entities via Arkham.""" |
| return await databus.fetch("liquidity_risk", address=address, chain=chain) |
|
|
|
|
| @router.get("/premium/holder-health/{address}") |
| async def holder_health_endpoint(address: str, chain: str = "solana"): |
| """Holder distribution: Gini coefficient, top concentration, decentralization score.""" |
| return await databus.fetch("holder_health", address=address, chain=chain) |
|
|
|
|
| @router.get("/premium/cross-chain/{address}") |
| async def cross_chain_endpoint(address: str): |
| """Trace entity across all chains via Arkham. Discover all related addresses.""" |
| return await databus.fetch("cross_chain_entity", address=address) |
|
|
|
|
| @router.get("/premium/rug-patterns/{address}") |
| async def rug_patterns_endpoint(address: str, chain: str = "solana"): |
| """Match token against 10 known rug pull patterns. Similarity scoring.""" |
| return await databus.fetch("rug_patterns", address=address, chain=chain) |
|
|
|
|
| @router.get("/premium/dev-reputation/{address}") |
| async def dev_reputation_endpoint(address: str, chain: str = "solana"): |
| """Deployer wallet history: token count, lifespan, entity resolution.""" |
| return await databus.fetch("dev_reputation", address=address, chain=chain) |
|
|
|
|
| @router.get("/premium/token-report/{address}") |
| async def token_report_endpoint(address: str, chain: str = "solana"): |
| """ONE-CALL enhanced token report. Smart verdicts, entity enrichment, |
| trust adjustments, tier-aware. THE endpoint powering RugCharts.""" |
| return await databus.fetch("token_report", address=address, chain=chain) |
|
|
|
|
| @router.get("/tiers") |
| async def tier_comparison_endpoint(): |
| """Competitive tier comparison: RugCharts vs DexScreener vs Nansen vs GMGN.""" |
| return await databus.fetch("tier_comparison") |
|
|
|
|
| |
| |
| |
|
|
|
|
| @router.get("/market/prices") |
| async def market_prices(coins: str = "bitcoin,ethereum,solana"): |
| """Live prices from CoinGecko. Free, no key needed.""" |
| return await databus.fetch("live_prices", coins=coins) |
|
|
|
|
| @router.get("/market/fear-greed") |
| async def fear_greed(): |
| """Crypto Fear & Greed Index. Free, no key needed.""" |
| return await databus.fetch("fear_greed") |
|
|
|
|
| @router.get("/market/trending") |
| async def market_trending(): |
| """Trending coins from CoinGecko.""" |
| return await databus.fetch("trending_coins") |
|
|
|
|
| @router.get("/market/prediction-markets") |
| async def prediction_markets(): |
| """Prediction markets from Polymarket. Free, no key needed.""" |
| return await databus.fetch("prediction_markets") |
|
|
|
|
| @router.get("/market/brief") |
| async def market_brief(): |
| """One-call market overview: prices + fear/greed + trending + predictions.""" |
| return await databus.fetch("market_brief") |
|
|
|
|
| @router.get("/news/full") |
| async def full_news(limit: int = 15): |
| """Complete news feed: headlines + market data + fear/greed + polymarket.""" |
| return await databus.fetch("full_news", limit=limit) |
|
|
|
|
| |
| |
| |
|
|
|
|
| @router.get("/news/intel") |
| async def news_intel(limit: int = 30): |
| """Complete news intelligence β 10+ sources, quality-scored, deduped, sentiment-tagged.""" |
| return await databus.fetch("news_intel", limit=limit) |
|
|
|
|
| @router.get("/news/weekly-best") |
| async def weekly_best(limit: int = 20): |
| """Curated weekly best β highest quality crypto journalism.""" |
| return await databus.fetch("weekly_best", limit=limit) |
|
|
|
|
| @router.get("/news/academic") |
| async def academic_papers(limit: int = 10): |
| """Academic crypto/blockchain research papers from arXiv.""" |
| return await databus.fetch("academic_papers", limit=limit) |
|
|
|
|
| @router.get("/news/social") |
| async def social_feed(limit: int = 30): |
| """Crypto social feed β X/Twitter + CryptoPanic sentiment.""" |
| return await databus.fetch("social_feed", limit=limit) |
|
|
|
|
| @router.post("/news/react/{content_hash}") |
| async def react_article(content_hash: str, request: Request): |
| """React to article: π₯ππ»ππ§ π€‘ππ""" |
| body = await request.json() |
| return await databus.fetch( |
| "article_reactions", |
| content_hash=content_hash, |
| reaction=body.get("reaction", "π₯"), |
| user=body.get("user", "anon"), |
| ) |
|
|
|
|
| @router.get("/news/reactions/{content_hash}") |
| async def get_article_reactions(content_hash: str): |
| """Get reactions and comments for an article.""" |
| return await databus.fetch("article_reactions", content_hash=content_hash) |
|
|
|
|
| @router.post("/news/comment/{content_hash}") |
| async def comment_article(content_hash: str, request: Request): |
| """Comment on an article.""" |
| body = await request.json() |
| return await databus.fetch( |
| "article_comments", |
| content_hash=content_hash, |
| user=body.get("user", "anon"), |
| text=body.get("text", ""), |
| ) |
|
|
|
|
| @router.post("/news/bb-post/{content_hash}") |
| async def create_bb_from_article(content_hash: str, request: Request): |
| """Turn article into Bulletin Board post.""" |
| body = await request.json() if request.headers.get("content-type") == "application/json" else {} |
| return await databus.fetch("bb_post", content_hash=content_hash, user=body.get("user", "system")) |
|
|
|
|
| |
| |
| |
|
|
|
|
| @router.get("/news/ct-rundown") |
| async def ct_rundown(limit: int = 20): |
| """CT Rundown β top 20 Crypto Twitter stories, AI-summarized, category-diverse.""" |
| return await databus.fetch("ct_rundown", limit=limit) |
|
|
|
|
| @router.get("/news/ct-accounts") |
| async def ct_accounts(): |
| """Curated CT account list β 150+ top accounts across 6 tiers.""" |
| return await databus.fetch("ct_accounts") |
|
|
|
|
| |
| |
| |
|
|
|
|
| @router.get("/social/kol/{handle}") |
| async def kol_profile(handle: str): |
| """KOL performance profile β trust score, call history, win rate.""" |
| return await databus.fetch("kol_profile", handle=handle) |
|
|
|
|
| @router.get("/social/kol-leaderboard") |
| async def kol_leaderboard(limit: int = 20): |
| """KOL leaderboard β ranked by trust score and accuracy.""" |
| return await databus.fetch("kol_leaderboard", limit=limit) |
|
|
|
|
| @router.get("/social/shill-alerts") |
| async def shill_alerts(): |
| """Active shill campaigns β coordinated promotion, pump-and-dump patterns.""" |
| return await databus.fetch("shill_detector") |
|
|
|
|
| @router.get("/social/scam-monitor") |
| async def scam_monitor(): |
| """Scam channel monitoring β Telegram/Discord scam pattern detection.""" |
| return await databus.fetch("scam_monitor") |
|
|
|
|
| @router.get("/social/metrics") |
| async def social_metrics(): |
| """Social metrics β trending topics, sentiment, KOL activity.""" |
| return await databus.fetch("social_metrics") |
|
|
|
|
| @router.get("/intel/daily") |
| async def daily_intel(): |
| """Daily Intelligence Report β Groq AI-powered market briefing with all data sources.""" |
| return await databus.fetch("daily_intel") |
|
|