
Lunarcrush
- 196 installs
- 21 repo stars
- Updated August 3, 2026
- starchild-ai-agent/official-skills
Helps with ai & agent building tasks during AI-assisted development.
About
lunarcrush is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- lunarcrush
- AI & Agent Building
- AI-coding skill
Lunarcrush by the numbers
- 196 all-time installs (skills.sh)
- +5 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #2,885 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/starchild-ai-agent/official-skills --skill lunarcrushAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 196 |
|---|---|
| repo stars | ★ 21 |
| Last updated | August 3, 2026 |
| Repository | starchild-ai-agent/official-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Script Usage
Script-mode skill — read this file, then invoke from a bash block:
python3 - <<'EOF'
import sys, json
sys.path.insert(0, "/data/workspace/skills/lunarcrush")
from exports import lunar_coin, lunar_coin_time_series, lunar_topic
# BTC social metrics
btc = lunar_coin(coin="BTC")
print(json.dumps(btc, indent=2))
# 30-day BTC galaxy_score timeseries
ts = lunar_coin_time_series(coin="BTC", bucket="day", interval="1m")
print(json.dumps(ts.get("data", [])[:3], indent=2))
EOFAvailable functions in exports.py: lunar_coin, lunar_coin_time_series, lunar_coin_meta, lunar_topic, lunar_topic_posts, lunar_creator. Read exports.py directly for exact signatures.
LunarCrush
LunarCrush provides social intelligence and sentiment data including Galaxy Score, AltRank, social volume, influencer activity, and trending topics. This is the crowd-mood layer.
Function Reference (signatures)
All functions are in exports.py. Coin symbols are uppercase (BTC, ETH, SOL).
| Function | Description |
|---|---|
lunar_coin(coin) | Current social metrics: galaxy_score, alt_rank, social_volume, social_dominance, price, market_cap. Returns dict (not wrapped in {data: ...}). |
lunar_coin_time_series(coin, bucket='day', interval='1m') | Historical timeseries. bucket = hour/day/week. interval = 1d/1w/1m/3m/6m/1y. |
lunar_coin_meta(coin) | Coin metadata (description, links, categories). |
lunar_topic(topic) | Social metrics for a topic/keyword (e.g. bitcoin, defi). |
lunar_topic_posts(topic, limit=20) | Recent posts mentioning the topic, sorted by engagement. |
lunar_creator(network, id) | Creator/influencer data. network = twitter / youtube / reddit / tiktok. id = handle without @. |
When to Use LunarCrush
Use LunarCrush for:
- Social sentiment - What is the crowd saying?
- Galaxy Score - Overall social momentum (0-100)
- AltRank - Social ranking (lower is better)
- Social volume - Number of social mentions
- Influencer activity - What are key voices saying?
- Trending topics - What's gaining attention?
Common Workflows
Coin Social Data
lunar_coin(symbol="BTC") # Current social metrics
lunar_coin(symbol="ETH") # Ethereum social data
lunar_coin_time_series(symbol="SOL", interval="1d", bucket="day") # Historical
lunar_coin_meta(symbol="BTC") # Metadata and linksTopic Analysis
lunar_topic(topic="defi") # DeFi topic metrics
lunar_topic_posts(topic="nft") # Recent posts about NFTsCreator/Influencer
lunar_creator(creator_id="123456") # Specific influencer dataKey Metrics
Galaxy Score (0-100)
| Score | Read |
|---|---|
| 80–100 | Exceptional social momentum — watch for tops |
| 60–79 | Strong sustained interest |
| 40–59 | Normal activity |
| 20–39 | Declining interest |
| 0–19 | Dead or bottoming |
Divergence signal: High Galaxy Score + negative price = potential reversal.
AltRank
Lower is better. #1–50 is top tier social presence. Useful for finding altcoins gaining attention before price moves.
- Rank 1-10: Highest social attention
- Rank 11-50: Strong social presence
- Rank 51-100: Moderate attention
- Rank 100+: Low social attention
Social Volume
Number of social mentions across platforms (Twitter, Reddit, etc.). Rising social volume often precedes price moves.
Social Dominance
Percentage of total crypto social volume. High dominance means the coin is dominating crypto conversations.
Analysis Patterns
Social alpha: Rising Galaxy Score + flat price = potential breakout setup. Social attention building before price moves.
Divergence signals:
- High Galaxy Score + falling price = bearish divergence
- Low Galaxy Score + rising price = weak rally
Sentiment confirmation: Combine Galaxy Score + social volume + AltRank. All three moving in same direction = strong signal.
Influencer tracking: Monitor what key voices are saying via creator tools. Influencer attention can drive retail interest.
Time Intervals
1h- Hourly1d- Daily1w- Weekly
Important Notes
- API Key: Requires LUNARCRUSH_API_KEY environment variable
- Symbols: Use standard symbols (BTC, ETH, SOL, etc.)
- Social platforms: Data aggregated from Twitter, Reddit, Medium, YouTube, and more
- Real-time: Social metrics update in near real-time
- Leading indicator: Social data often leads price - attention builds before price moves
Workflow Examples
Find Rising Coins
1. Use lunar_coin to check AltRank and Galaxy Score 2. Look for coins with rising AltRank (lower number = better) and Galaxy Score > 60 3. Check if price is flat or rising - social attention building is a leading signal
Sentiment Check
1. Get current Galaxy Score and social volume 2. Compare to historical data via lunar_coin_time_series 3. High score + rising volume = strong momentum 4. Low score + falling volume = weak interest
Topic Trends
1. Use lunar_topic to find trending topics (DeFi, NFT, Gaming, etc.) 2. Use lunar_topic_posts to see what people are saying 3. Cross-reference with price action for early trend signals
"""
LunarCrush Extension - Social Intelligence and Sentiment Data
Provides social intelligence and sentiment data including:
- Galaxy Score (social momentum)
- AltRank (social ranking)
- Social volume and dominance
- Influencer activity
- Trending topics
Environment Variables Required:
- LUNARCRUSH_API_KEY: LunarCrush API key
Usage:
This extension is auto-loaded by the ExtensionLoader.
Tools are available to agents configured with these tools in agents.yaml.
"""
import os
import sys
import logging
from typing import List
try:
from core.tool import ToolRegistry
except Exception:
ToolRegistry = None # Standalone script usage
logger = logging.getLogger(__name__)
# Add local tools directory to path for imports
TOOLS_DIR = os.path.join(os.path.dirname(__file__), 'tools')
if TOOLS_DIR not in sys.path:
sys.path.insert(0, TOOLS_DIR)
def register(api) -> List[str]:
"""
Extension entry point - register all LunarCrush tools.
Args:
api: ExtensionApi instance with registry and config
Returns:
List of registered tool names
"""
registered = []
try:
from .lunarcrush import (
LunarCoinTool,
LunarCoinTimeSeriesTool,
LunarCoinMetaTool,
LunarTopicTool,
LunarTopicPostsTool,
LunarTopicNewsTool,
LunarCategoryPostsTool,
LunarCategoryNewsTool,
LunarContentFeedTool,
LunarSearchContentTool,
LunarCreatorTool,
)
api.register_tool(LunarCoinTool())
api.register_tool(LunarCoinTimeSeriesTool())
api.register_tool(LunarCoinMetaTool())
api.register_tool(LunarTopicTool())
api.register_tool(LunarTopicPostsTool())
api.register_tool(LunarTopicNewsTool())
api.register_tool(LunarCategoryPostsTool())
api.register_tool(LunarCategoryNewsTool())
api.register_tool(LunarContentFeedTool())
api.register_tool(LunarSearchContentTool())
api.register_tool(LunarCreatorTool())
registered.extend([
"lunar_coin",
"lunar_coin_time_series",
"lunar_coin_meta",
"lunar_topic",
"lunar_topic_posts",
"lunar_topic_news",
"lunar_category_posts",
"lunar_category_news",
"lunar_content_feed",
"lunar_search_content",
"lunar_creator",
])
logger.info("Registered LunarCrush tools (11 tools)")
except Exception as e:
logger.warning(f"Failed to load LunarCrush tools: {e}")
return registered
# Extension metadata
EXTENSION_INFO = {
"name": "lunarcrush",
"version": "1.2.0",
"description": "LunarCrush social intelligence, news aggregation, and content search",
"tools": [
"lunar_coin",
"lunar_coin_time_series",
"lunar_coin_meta",
"lunar_topic",
"lunar_topic_posts",
"lunar_topic_news",
"lunar_category_posts",
"lunar_category_news",
"lunar_content_feed",
"lunar_search_content",
"lunar_creator",
],
"env_vars": [
"LUNARCRUSH_API_KEY",
],
}
"""
LunarCrush skill exports — tool names match SKILL.md frontmatter.
Usage in task scripts:
from core.skill_tools import lunarcrush
btc = lunarcrush.lunar_coin(coin="BTC")
ts = lunarcrush.lunar_coin_time_series(coin="BTC", bucket="day", interval="1m")
meta = lunarcrush.lunar_coin_meta(coin="ETH")
topic = lunarcrush.lunar_topic(topic="bitcoin")
posts = lunarcrush.lunar_topic_posts(topic="defi", limit=10)
creator = lunarcrush.lunar_creator(network="twitter", id="elonmusk")
"""
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "tools"))
from coins import get_coin, get_coin_time_series, get_coin_meta
from topics import (
get_topic,
get_topic_posts,
get_topic_news,
get_category_posts,
get_category_news,
get_content_feed,
search_content,
)
from creators import get_creator
def lunar_coin(coin):
"""Get detailed social metrics for a single coin."""
return get_coin(coin=coin)
def lunar_coin_time_series(coin, bucket="day", interval="1m"):
"""Get coin time-series data (price + social metrics over time)."""
return get_coin_time_series(coin=coin, bucket=bucket, interval=interval)
def lunar_coin_meta(coin):
"""Get coin metadata (links, description, social accounts)."""
return get_coin_meta(coin=coin)
def lunar_topic(topic):
"""Get metrics for a specific topic (24h aggregation)."""
return get_topic(topic=topic)
def lunar_topic_posts(topic, limit=20):
"""Get top posts for a topic."""
return get_topic_posts(topic=topic, limit=limit)
def lunar_topic_news(topic, limit=20):
"""Get topic news feed."""
return get_topic_news(topic=topic, limit=limit)
def lunar_category_posts(category, limit=20):
"""Get top posts for a category."""
return get_category_posts(category=category, limit=limit)
def lunar_category_news(category, limit=20):
"""Get category news feed."""
return get_category_news(category=category, limit=limit)
def lunar_content_feed(feed_type, scope_type, scope, limit=20):
"""Unified content feed: topic/category × news/posts."""
return get_content_feed(feed_type=feed_type, scope_type=scope_type, scope=scope, limit=limit)
def lunar_search_content(
query="",
topics=None,
categories=None,
feed_types=None,
time_window="24h",
limit=50,
):
"""Cross-topic/category content search + sentiment summary."""
return search_content(
query=query,
topics=topics,
categories=categories,
feed_types=feed_types,
time_window=time_window,
limit=limit,
)
def lunar_creator(network, id):
"""Get details for a specific influencer."""
return get_creator(network=network, id=id)
"""
LunarCrush Tool Wrappers
Wraps tools from /tools/lunarcrush/ for use in Agent framework.
Provides social intelligence: Galaxy Score, sentiment, influencers, trending topics.
Note: Some endpoints require higher API tiers or have strict rate limits.
Only the most reliable endpoints are exposed here.
"""
import asyncio
import logging
from core.tool import BaseTool, ToolContext, ToolResult
logger = logging.getLogger(__name__)
# Import original tools from local tools directory
try:
from .tools.coins import (
get_coin,
get_coin_time_series,
get_coin_meta,
)
from .tools.topics import (
get_topic,
get_topic_posts,
get_topic_news,
get_category_posts,
get_category_news,
get_content_feed,
search_content,
)
from .tools.creators import (
get_creator,
)
LUNARCRUSH_AVAILABLE = True
except ImportError as e:
logger.warning(f"LunarCrush tools not available: {e}")
LUNARCRUSH_AVAILABLE = False
# ==================== Coin Tools ====================
class LunarCoinTool(BaseTool):
"""
Get detailed metrics for a single coin.
"""
@property
def name(self) -> str:
return "lunar_coin"
@property
def description(self) -> str:
return """Get detailed social metrics for a single coin.
Returns Galaxy Score, AltRank, social volume, sentiment, and more.
Examples:
- Get BTC social metrics: lunar_coin(coin="BTC")
- Get ETH metrics: lunar_coin(coin="ETH")"""
@property
def parameters(self) -> dict:
return {
"type": "object",
"properties": {
"coin": {
"type": "string",
"description": "Coin symbol (BTC, ETH, SOL)"
}
},
"required": ["coin"]
}
async def execute(
self,
ctx: ToolContext,
coin: str
) -> ToolResult:
if not LUNARCRUSH_AVAILABLE:
return ToolResult(
success=False,
output=None,
error="LunarCrush tools not available."
)
try:
result = await asyncio.to_thread(get_coin, coin)
return ToolResult(success=True, output=result)
except Exception as e:
return ToolResult(success=False, output=None, error=str(e))
# ==================== Content Search ====================
class LunarContentFeedTool(BaseTool):
"""Unified content feed: topic/category × news/posts."""
@property
def name(self) -> str:
return "lunar_content_feed"
@property
def description(self) -> str:
return """Unified content feed — one call, any scope.
Examples:
- Topic news: lunar_content_feed(feed_type="news", scope_type="topic", scope="bitcoin")
- Category posts: lunar_content_feed(feed_type="posts", scope_type="category", scope="defi")"""
@property
def parameters(self) -> dict:
return {
"type": "object",
"properties": {
"feed_type": {
"type": "string",
"description": "Content type: news or posts",
"enum": ["news", "posts"]
},
"scope_type": {
"type": "string",
"description": "Scope: topic or category",
"enum": ["topic", "category"]
},
"scope": {
"type": "string",
"description": "Topic or category value (e.g. bitcoin, defi, gaming)"
},
"limit": {
"type": "integer",
"description": "Max items (max 100)",
"default": 20
}
},
"required": ["feed_type", "scope_type", "scope"]
}
async def execute(
self,
ctx: ToolContext,
feed_type: str,
scope_type: str,
scope: str,
limit: int = 20
) -> ToolResult:
if not LUNARCRUSH_AVAILABLE:
return ToolResult(success=False, output=None, error="LunarCrush tools not available.")
try:
result = await asyncio.to_thread(get_content_feed, feed_type, scope_type, scope, limit)
return ToolResult(success=True, output=result)
except Exception as e:
return ToolResult(success=False, output=None, error=str(e))
class LunarTopicNewsTool(BaseTool):
"""Get news feed for a specific topic."""
@property
def name(self) -> str:
return "lunar_topic_news"
@property
def description(self) -> str:
return """Get news articles for a topic.
Examples:
- Get Bitcoin news: lunar_topic_news(topic="bitcoin", limit=10)
- Get DeFi news: lunar_topic_news(topic="defi")"""
@property
def parameters(self) -> dict:
return {
"type": "object",
"properties": {
"topic": {
"type": "string",
"description": "Topic slug (bitcoin, defi, nft, etc.)"
},
"limit": {
"type": "integer",
"description": "Max items (max 100)",
"default": 20
}
},
"required": ["topic"]
}
async def execute(
self,
ctx: ToolContext,
topic: str,
limit: int = 20
) -> ToolResult:
if not LUNARCRUSH_AVAILABLE:
return ToolResult(success=False, output=None, error="LunarCrush tools not available.")
try:
result = await asyncio.to_thread(get_topic_news, topic, limit)
return ToolResult(success=True, output=result)
except Exception as e:
return ToolResult(success=False, output=None, error=str(e))
class LunarCategoryPostsTool(BaseTool):
"""Get social posts for a category."""
@property
def name(self) -> str:
return "lunar_category_posts"
@property
def description(self) -> str:
return """Get social posts for a category.
Examples:
- Get gaming posts: lunar_category_posts(category="gaming", limit=10)
- Get memecoin posts: lunar_category_posts(category="memecoin")"""
@property
def parameters(self) -> dict:
return {
"type": "object",
"properties": {
"category": {
"type": "string",
"description": "Category slug (defi, gaming, memecoin, etc.)"
},
"limit": {
"type": "integer",
"description": "Max items (max 100)",
"default": 20
}
},
"required": ["category"]
}
async def execute(
self,
ctx: ToolContext,
category: str,
limit: int = 20
) -> ToolResult:
if not LUNARCRUSH_AVAILABLE:
return ToolResult(success=False, output=None, error="LunarCrush tools not available.")
try:
result = await asyncio.to_thread(get_category_posts, category, limit)
return ToolResult(success=True, output=result)
except Exception as e:
return ToolResult(success=False, output=None, error=str(e))
class LunarCategoryNewsTool(BaseTool):
"""Get news feed for a category."""
@property
def name(self) -> str:
return "lunar_category_news"
@property
def description(self) -> str:
return """Get news articles for a category.
Examples:
- Get DeFi news: lunar_category_news(category="defi", limit=10)
- Get gaming news: lunar_category_news(category="gaming")"""
@property
def parameters(self) -> dict:
return {
"type": "object",
"properties": {
"category": {
"type": "string",
"description": "Category slug (defi, gaming, memecoin, etc.)"
},
"limit": {
"type": "integer",
"description": "Max items (max 100)",
"default": 20
}
},
"required": ["category"]
}
async def execute(
self,
ctx: ToolContext,
category: str,
limit: int = 20
) -> ToolResult:
if not LUNARCRUSH_AVAILABLE:
return ToolResult(success=False, output=None, error="LunarCrush tools not available.")
try:
result = await asyncio.to_thread(get_category_news, category, limit)
return ToolResult(success=True, output=result)
except Exception as e:
return ToolResult(success=False, output=None, error=str(e))
class LunarSearchContentTool(BaseTool):
"""Cross-scope content search + sentiment summary."""
@property
def name(self) -> str:
return "lunar_search_content"
@property
def description(self) -> str:
return """Crypto content search engine — searches news + posts across topics/categories.
Aggregates, deduplicates, sorts by engagement, and provides sentiment summary.
Examples:
- Search "ETF" across default scopes: lunar_search_content(query="ETF")
- Search "halving" in bitcoin+solana topics: lunar_search_content(query="halving", topics=["bitcoin","solana"])
- Get all recent DeFi news+posts: lunar_search_content(topics=["defi"], feed_types=["news","posts"])
- Search with time window: lunar_search_content(query="regulation", time_window="7d", limit=30)"""
@property
def parameters(self) -> dict:
return {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Keyword to filter by (title, description, body, author). Empty = no filter."
},
"topics": {
"type": "array",
"items": {"type": "string"},
"description": "Topic slugs to search. Default: [bitcoin, ethereum, solana]"
},
"categories": {
"type": "array",
"items": {"type": "string"},
"description": "Category slugs to search. Default: [defi]"
},
"feed_types": {
"type": "array",
"items": {"type": "string", "enum": ["news", "posts"]},
"description": "Content types. Default: [news, posts]"
},
"time_window": {
"type": "string",
"description": "Time scope: 1h, 24h, 7d, 30d. Default: 24h"
},
"limit": {
"type": "integer",
"description": "Max items returned (max 200). Default: 50"
}
}
}
async def execute(
self,
ctx: ToolContext,
query: str = "",
topics: list | None = None,
categories: list | None = None,
feed_types: list | None = None,
time_window: str = "24h",
limit: int = 50
) -> ToolResult:
if not LUNARCRUSH_AVAILABLE:
return ToolResult(success=False, output=None, error="LunarCrush tools not available.")
try:
result = await asyncio.to_thread(
search_content, query, topics, categories, feed_types, time_window, limit
)
return ToolResult(success=True, output=result)
except Exception as e:
return ToolResult(success=False, output=None, error=str(e))
class LunarCoinTimeSeriesTool(BaseTool):
"""
Get historical social + market data for a coin.
"""
@property
def name(self) -> str:
return "lunar_coin_time_series"
@property
def description(self) -> str:
return """Get historical social and market data for a coin.
Includes Galaxy Score, sentiment, social volume over time.
Examples:
- Get BTC daily data for 1 month: lunar_coin_time_series(coin="BTC", bucket="day", interval="1m")
- Get ETH hourly for 1 week: lunar_coin_time_series(coin="ETH", bucket="hour", interval="1w")"""
@property
def parameters(self) -> dict:
return {
"type": "object",
"properties": {
"coin": {
"type": "string",
"description": "Coin symbol (BTC, ETH)"
},
"bucket": {
"type": "string",
"description": "Time bucket: hour, day, week",
"default": "day"
},
"interval": {
"type": "string",
"description": "Time interval: 1w, 1m, 3m, 6m, 1y, all",
"default": "1m"
}
},
"required": ["coin"]
}
async def execute(
self,
ctx: ToolContext,
coin: str,
bucket: str = "day",
interval: str = "1m"
) -> ToolResult:
if not LUNARCRUSH_AVAILABLE:
return ToolResult(
success=False,
output=None,
error="LunarCrush tools not available."
)
try:
result = await asyncio.to_thread(get_coin_time_series, coin, bucket, interval)
return ToolResult(success=True, output=result)
except Exception as e:
return ToolResult(success=False, output=None, error=str(e))
class LunarCoinMetaTool(BaseTool):
"""
Get coin metadata including links, description, social accounts.
"""
@property
def name(self) -> str:
return "lunar_coin_meta"
@property
def description(self) -> str:
return """Get coin metadata: links, description, social accounts.
Examples:
- Get BTC metadata: lunar_coin_meta(coin="BTC")
- Get SOL project info: lunar_coin_meta(coin="SOL")"""
@property
def parameters(self) -> dict:
return {
"type": "object",
"properties": {
"coin": {
"type": "string",
"description": "Coin symbol (BTC, ETH)"
}
},
"required": ["coin"]
}
async def execute(
self,
ctx: ToolContext,
coin: str
) -> ToolResult:
if not LUNARCRUSH_AVAILABLE:
return ToolResult(
success=False,
output=None,
error="LunarCrush tools not available."
)
try:
result = await asyncio.to_thread(get_coin_meta, coin)
return ToolResult(success=True, output=result)
except Exception as e:
return ToolResult(success=False, output=None, error=str(e))
# ==================== Topic Tools ====================
class LunarTopicTool(BaseTool):
"""
Get metrics for a specific topic.
"""
@property
def name(self) -> str:
return "lunar_topic"
@property
def description(self) -> str:
return """Get metrics for a specific topic (24h aggregation).
Examples:
- Get DeFi topic metrics: lunar_topic(topic="defi")
- Get NFT topic metrics: lunar_topic(topic="nft")"""
@property
def parameters(self) -> dict:
return {
"type": "object",
"properties": {
"topic": {
"type": "string",
"description": "Topic name or slug (defi, nft, bitcoin)"
}
},
"required": ["topic"]
}
async def execute(
self,
ctx: ToolContext,
topic: str
) -> ToolResult:
if not LUNARCRUSH_AVAILABLE:
return ToolResult(
success=False,
output=None,
error="LunarCrush tools not available."
)
try:
result = await asyncio.to_thread(get_topic, topic)
return ToolResult(success=True, output=result)
except Exception as e:
return ToolResult(success=False, output=None, error=str(e))
class LunarTopicPostsTool(BaseTool):
"""
Get top posts for a topic.
"""
@property
def name(self) -> str:
return "lunar_topic_posts"
@property
def description(self) -> str:
return """Get top posts for a topic.
Examples:
- Get top DeFi posts: lunar_topic_posts(topic="defi")
- Get top 10 Bitcoin posts: lunar_topic_posts(topic="bitcoin", limit=10)"""
@property
def parameters(self) -> dict:
return {
"type": "object",
"properties": {
"topic": {
"type": "string",
"description": "Topic name or slug"
},
"limit": {
"type": "integer",
"description": "Number of posts (max 100)",
"default": 20
}
},
"required": ["topic"]
}
async def execute(
self,
ctx: ToolContext,
topic: str,
limit: int = 20
) -> ToolResult:
if not LUNARCRUSH_AVAILABLE:
return ToolResult(
success=False,
output=None,
error="LunarCrush tools not available."
)
try:
result = await asyncio.to_thread(get_topic_posts, topic, limit)
return ToolResult(success=True, output=result)
except Exception as e:
return ToolResult(success=False, output=None, error=str(e))
# ==================== Creator Tools ====================
class LunarCreatorTool(BaseTool):
"""
Get details for a specific influencer.
"""
@property
def name(self) -> str:
return "lunar_creator"
@property
def description(self) -> str:
return """Get details for a specific crypto influencer.
Examples:
- Get Twitter influencer: lunar_creator(network="twitter", id="VitalikButerin")
- Get YouTube creator: lunar_creator(network="youtube", id="@CoinBureau")"""
@property
def parameters(self) -> dict:
return {
"type": "object",
"properties": {
"network": {
"type": "string",
"description": "Social network: twitter, youtube"
},
"id": {
"type": "string",
"description": "Creator ID or username"
}
},
"required": ["network", "id"]
}
async def execute(
self,
ctx: ToolContext,
network: str,
id: str
) -> ToolResult:
if not LUNARCRUSH_AVAILABLE:
return ToolResult(
success=False,
output=None,
error="LunarCrush tools not available."
)
try:
result = await asyncio.to_thread(get_creator, network, id)
return ToolResult(success=True, output=result)
except Exception as e:
return ToolResult(success=False, output=None, error=str(e))
"""
LunarCrush Tools
Social intelligence for crypto: Galaxy Score, sentiment, influencers, trending topics.
"""
from .coins import (
get_coins_list,
get_coin,
get_coin_time_series,
get_coin_meta,
)
from .topics import (
get_topics,
get_topic,
get_topic_summary,
get_topic_posts,
get_topic_news,
get_category_posts,
get_category_news,
get_content_feed,
)
from .creators import (
get_creators,
get_creator,
get_creator_posts,
)
from .nfts import (
get_nfts,
get_nft,
)
__all__ = [
# Coins
"get_coins_list",
"get_coin",
"get_coin_time_series",
"get_coin_meta",
# Topics
"get_topics",
"get_topic",
"get_topic_summary",
"get_topic_posts",
"get_topic_news",
"get_category_posts",
"get_category_news",
"get_content_feed",
# Creators
"get_creators",
"get_creator",
"get_creator_posts",
# NFTs
"get_nfts",
"get_nft",
]
#!/usr/bin/env python3
"""
LunarCrush Coins API Tools
Tools for fetching coin metrics, time series, and metadata from LunarCrush.
Provides Galaxy Score, AltRank, and social sentiment data.
"""
import json
import argparse
from typing import Dict, Any
try:
from .utils import make_request, normalize_symbol, parse_time_series_bucket
except ImportError:
from utils import make_request, normalize_symbol, parse_time_series_bucket
# MCP Tool Schemas
MCP_COINS_LIST_SCHEMA = {
"name": "lunar_coins_list",
"title": "LunarCrush Trending Coins",
"description": "Get trending coins with Galaxy Score, AltRank, and social metrics.",
"inputSchema": {
"type": "object",
"properties": {
"sort": {
"type": "string",
"description": "Sort field (galaxy_score, alt_rank, market_cap, volume)",
"default": "galaxy_score",
"enum": ["galaxy_score", "alt_rank", "market_cap", "volume", "social_volume"]
},
"limit": {
"type": "integer",
"description": "Number of results (max 100)",
"default": 50,
"minimum": 1,
"maximum": 100
},
"desc": {
"type": "boolean",
"description": "Sort descending",
"default": True
}
},
"additionalProperties": False
}
}
MCP_COIN_SCHEMA = {
"name": "lunar_coin",
"title": "LunarCrush Single Coin",
"description": "Get detailed metrics for a single coin including Galaxy Score, AltRank, sentiment.",
"inputSchema": {
"type": "object",
"properties": {
"coin": {
"type": "string",
"description": "Coin symbol (e.g., BTC, ETH, SOL)"
}
},
"required": ["coin"],
"additionalProperties": False
}
}
MCP_COIN_TIME_SERIES_SCHEMA = {
"name": "lunar_coin_time_series",
"title": "LunarCrush Coin Time Series",
"description": "Get historical social + market data for a coin.",
"inputSchema": {
"type": "object",
"properties": {
"coin": {
"type": "string",
"description": "Coin symbol (e.g., BTC, ETH)"
},
"bucket": {
"type": "string",
"description": "Time bucket: hour, day, week",
"default": "day",
"enum": ["hour", "day", "week"]
},
"interval": {
"type": "string",
"description": "Time interval: 1w, 1m, 3m, 6m, 1y, all",
"default": "1m",
"enum": ["1w", "1m", "3m", "6m", "1y", "all"]
}
},
"required": ["coin"],
"additionalProperties": False
}
}
MCP_COIN_META_SCHEMA = {
"name": "lunar_coin_meta",
"title": "LunarCrush Coin Metadata",
"description": "Get coin metadata including links, description, social accounts.",
"inputSchema": {
"type": "object",
"properties": {
"coin": {
"type": "string",
"description": "Coin symbol (e.g., BTC, ETH)"
}
},
"required": ["coin"],
"additionalProperties": False
}
}
def get_coins_list(
sort: str = "galaxy_score",
limit: int = 50,
desc: bool = True
) -> Dict[str, Any]:
"""
Get trending coins with Galaxy Score and social metrics.
Args:
sort: Sort field (galaxy_score, alt_rank, market_cap, volume, social_volume)
limit: Number of results (max 100)
desc: Sort descending
Returns:
Dictionary with list of coins and their metrics
"""
params = {
"sort": sort,
"limit": min(limit, 100),
"desc": str(desc).lower()
}
data = make_request("/public/coins/list/v2", params)
# Extract and format coin data
coins = data.get("data", [])
formatted = []
for coin in coins:
formatted.append({
"symbol": coin.get("symbol", ""),
"name": coin.get("name", ""),
"price": coin.get("price"),
"price_change_24h": coin.get("percent_change_24h"),
"market_cap": coin.get("market_cap"),
"galaxy_score": coin.get("galaxy_score"),
"alt_rank": coin.get("alt_rank"),
"social_volume": coin.get("social_volume"),
"social_dominance": coin.get("social_dominance"),
"sentiment": coin.get("sentiment"),
"categories": coin.get("categories", [])
})
return {
"coins": formatted,
"count": len(formatted),
"sort_by": sort,
"sort_desc": desc
}
def get_coin(coin: str) -> Dict[str, Any]:
"""
Get detailed metrics for a single coin.
Args:
coin: Coin symbol (e.g., BTC, ETH, SOL)
Returns:
Dictionary with coin metrics including Galaxy Score, AltRank, sentiment
"""
symbol = normalize_symbol(coin)
data = make_request(f"/public/coins/{symbol}/v1")
coin_data = data.get("data", {})
return {
"symbol": coin_data.get("symbol", symbol),
"name": coin_data.get("name", ""),
"price": coin_data.get("price"),
"price_change_24h": coin_data.get("percent_change_24h"),
"price_change_7d": coin_data.get("percent_change_7d"),
"market_cap": coin_data.get("market_cap"),
"volume_24h": coin_data.get("volume_24h"),
"galaxy_score": coin_data.get("galaxy_score"),
"alt_rank": coin_data.get("alt_rank"),
"social_volume": coin_data.get("social_volume"),
"social_volume_change_24h": coin_data.get("social_volume_24h_percent_change"),
"social_dominance": coin_data.get("social_dominance"),
"sentiment": coin_data.get("sentiment"),
"social_contributors": coin_data.get("social_contributors"),
"news_articles": coin_data.get("news"),
"tweets": coin_data.get("tweets"),
"categories": coin_data.get("categories", []),
"timeSeries": coin_data.get("timeSeries")
}
def get_coin_time_series(
coin: str,
bucket: str = "day",
interval: str = "1m"
) -> Dict[str, Any]:
"""
Get historical social and market data for a coin.
Args:
coin: Coin symbol (e.g., BTC, ETH)
bucket: Time bucket (hour, day, week)
interval: Time interval (1w, 1m, 3m, 6m, 1y, all)
Returns:
Dictionary with time series data
"""
symbol = normalize_symbol(coin)
bucket = parse_time_series_bucket(bucket)
params = {
"bucket": bucket,
"interval": interval
}
data = make_request(f"/public/coins/{symbol}/time-series/v2", params)
time_series = data.get("data", [])
# Format time series data
formatted = []
for point in time_series:
formatted.append({
"time": point.get("time"),
"open": point.get("open"),
"high": point.get("high"),
"low": point.get("low"),
"close": point.get("close"),
"volume": point.get("volume"),
"market_cap": point.get("market_cap"),
"galaxy_score": point.get("galaxy_score"),
"alt_rank": point.get("alt_rank"),
"sentiment": point.get("sentiment"),
"social_volume": point.get("social_volume"),
"social_dominance": point.get("social_dominance"),
"tweets": point.get("tweets"),
"news": point.get("news")
})
return {
"symbol": symbol,
"bucket": bucket,
"interval": interval,
"data_points": len(formatted),
"time_series": formatted
}
def get_coin_meta(coin: str) -> Dict[str, Any]:
"""
Get coin metadata including links, description, social accounts.
Args:
coin: Coin symbol (e.g., BTC, ETH)
Returns:
Dictionary with coin metadata
"""
symbol = normalize_symbol(coin)
data = make_request(f"/public/coins/{symbol}/meta/v1")
meta = data.get("data", {})
return {
"symbol": meta.get("symbol", symbol),
"name": meta.get("name", ""),
"description": meta.get("description", ""),
"website": meta.get("website"),
"whitepaper": meta.get("whitepaper"),
"twitter": meta.get("twitter"),
"telegram": meta.get("telegram"),
"discord": meta.get("discord"),
"reddit": meta.get("reddit"),
"github": meta.get("github"),
"medium": meta.get("medium"),
"youtube": meta.get("youtube"),
"blockchain": meta.get("blockchain"),
"contract_address": meta.get("contract_address"),
"categories": meta.get("categories", []),
"logo": meta.get("logo")
}
def main():
"""CLI interface for LunarCrush coins tools."""
parser = argparse.ArgumentParser(
description="LunarCrush Coins API Tools",
epilog="""
Commands:
list Get trending coins with Galaxy Score
coin Get single coin metrics
series Get coin time series data
meta Get coin metadata
Examples:
python coins.py list --limit 10
python coins.py coin --symbol BTC
python coins.py series --symbol ETH --bucket day --interval 1m
python coins.py meta --symbol SOL
""",
formatter_class=argparse.RawDescriptionHelpFormatter
)
subparsers = parser.add_subparsers(dest="command", help="Available commands")
# List command
list_parser = subparsers.add_parser("list", help="Get trending coins")
list_parser.add_argument("--sort", default="galaxy_score",
choices=["galaxy_score", "alt_rank", "market_cap", "volume", "social_volume"])
list_parser.add_argument("--limit", type=int, default=50)
list_parser.add_argument("--asc", action="store_true", help="Sort ascending")
# Coin command
coin_parser = subparsers.add_parser("coin", help="Get single coin metrics")
coin_parser.add_argument("--symbol", required=True, help="Coin symbol (BTC, ETH)")
# Time series command
series_parser = subparsers.add_parser("series", help="Get coin time series")
series_parser.add_argument("--symbol", required=True, help="Coin symbol")
series_parser.add_argument("--bucket", default="day", choices=["hour", "day", "week"])
series_parser.add_argument("--interval", default="1m", choices=["1w", "1m", "3m", "6m", "1y", "all"])
# Meta command
meta_parser = subparsers.add_parser("meta", help="Get coin metadata")
meta_parser.add_argument("--symbol", required=True, help="Coin symbol")
# Schema output
parser.add_argument("--schema", action="store_true", help="Output MCP schema")
args = parser.parse_args()
if args.schema:
schemas = {
"coins_list": MCP_COINS_LIST_SCHEMA,
"coin": MCP_COIN_SCHEMA,
"time_series": MCP_COIN_TIME_SERIES_SCHEMA,
"meta": MCP_COIN_META_SCHEMA
}
print(json.dumps(schemas, indent=2))
return 0
try:
if args.command == "list":
result = get_coins_list(sort=args.sort, limit=args.limit, desc=not args.asc)
elif args.command == "coin":
result = get_coin(args.symbol)
elif args.command == "series":
result = get_coin_time_series(args.symbol, args.bucket, args.interval)
elif args.command == "meta":
result = get_coin_meta(args.symbol)
else:
parser.print_help()
return 0
print(json.dumps(result, indent=2, ensure_ascii=False))
return 0
except Exception as e:
print(json.dumps({"error": str(e)}, indent=2))
return 1
if __name__ == "__main__":
exit(main())
#!/usr/bin/env python3
"""
LunarCrush Creators/Influencers API Tools
Tools for fetching crypto influencer data including rankings, metrics, and posts.
"""
import json
import argparse
from typing import Dict, Any
try:
from .utils import make_request
except ImportError:
from utils import make_request
# MCP Tool Schemas
MCP_CREATORS_SCHEMA = {
"name": "lunar_creators",
"title": "LunarCrush Top Creators",
"description": "Get top crypto influencers/creators.",
"inputSchema": {
"type": "object",
"properties": {
"sort": {
"type": "string",
"description": "Sort field",
"default": "influence_rank",
"enum": ["influence_rank", "followers", "engagement", "interactions"]
},
"network": {
"type": "string",
"description": "Filter by social network",
"enum": ["twitter", "youtube", "all"],
"default": "all"
},
"limit": {
"type": "integer",
"description": "Number of results (max 100)",
"default": 50,
"minimum": 1,
"maximum": 100
}
},
"additionalProperties": False
}
}
MCP_CREATOR_SCHEMA = {
"name": "lunar_creator",
"title": "LunarCrush Single Creator",
"description": "Get details for a specific influencer.",
"inputSchema": {
"type": "object",
"properties": {
"network": {
"type": "string",
"description": "Social network (twitter, youtube)",
"enum": ["twitter", "youtube"]
},
"id": {
"type": "string",
"description": "Creator ID or username"
}
},
"required": ["network", "id"],
"additionalProperties": False
}
}
MCP_CREATOR_POSTS_SCHEMA = {
"name": "lunar_creator_posts",
"title": "LunarCrush Creator Posts",
"description": "Get top posts from a specific influencer.",
"inputSchema": {
"type": "object",
"properties": {
"network": {
"type": "string",
"description": "Social network (twitter, youtube)",
"enum": ["twitter", "youtube"]
},
"id": {
"type": "string",
"description": "Creator ID or username"
},
"limit": {
"type": "integer",
"description": "Number of posts (max 100)",
"default": 20,
"minimum": 1,
"maximum": 100
}
},
"required": ["network", "id"],
"additionalProperties": False
}
}
def get_creators(
sort: str = "influence_rank",
network: str = "all",
limit: int = 50
) -> Dict[str, Any]:
"""
Get top crypto influencers/creators.
Args:
sort: Sort field (influence_rank, followers, engagement, interactions)
network: Filter by network (twitter, youtube, all)
limit: Number of results (max 100)
Returns:
Dictionary with list of top creators
"""
params = {
"sort": sort,
"limit": min(limit, 100)
}
if network != "all":
params["network"] = network
data = make_request("/public/creators/list/v1", params)
creators = data.get("data", [])
formatted = []
for creator in creators:
formatted.append({
"id": creator.get("id"),
"username": creator.get("username", ""),
"display_name": creator.get("display_name", ""),
"network": creator.get("network", ""),
"followers": creator.get("followers"),
"influence_rank": creator.get("influence_rank"),
"engagement_rate": creator.get("engagement_rate"),
"interactions_24h": creator.get("interactions_24h"),
"posts_24h": creator.get("posts_24h"),
"avg_sentiment": creator.get("avg_sentiment"),
"profile_url": creator.get("profile_url", ""),
"profile_image": creator.get("profile_image", ""),
"bio": creator.get("bio", ""),
"verified": creator.get("verified", False),
"categories": creator.get("categories", [])
})
return {
"creators": formatted,
"count": len(formatted),
"sort_by": sort,
"network_filter": network
}
def get_creator(network: str, id: str) -> Dict[str, Any]:
"""
Get details for a specific influencer.
Args:
network: Social network (twitter, youtube)
id: Creator ID or username
Returns:
Dictionary with creator details
"""
network = network.lower().strip()
if network not in ["twitter", "youtube"]:
raise ValueError(f"Invalid network: {network}. Must be 'twitter' or 'youtube'")
data = make_request(f"/public/creator/{network}/{id}/v1")
creator = data.get("data", {})
return {
"id": creator.get("id", id),
"username": creator.get("username", ""),
"display_name": creator.get("display_name", ""),
"network": network,
"followers": creator.get("followers"),
"following": creator.get("following"),
"influence_rank": creator.get("influence_rank"),
"engagement_rate": creator.get("engagement_rate"),
"interactions_24h": creator.get("interactions_24h"),
"interactions_7d": creator.get("interactions_7d"),
"posts_24h": creator.get("posts_24h"),
"posts_7d": creator.get("posts_7d"),
"avg_sentiment": creator.get("avg_sentiment"),
"profile_url": creator.get("profile_url", ""),
"profile_image": creator.get("profile_image", ""),
"bio": creator.get("bio", ""),
"verified": creator.get("verified", False),
"categories": creator.get("categories", []),
"top_coins_mentioned": creator.get("top_coins_mentioned", []),
"top_topics": creator.get("top_topics", []),
"joined": creator.get("joined")
}
def get_creator_posts(network: str, id: str, limit: int = 20) -> Dict[str, Any]:
"""
Get top posts from a specific influencer.
Args:
network: Social network (twitter, youtube)
id: Creator ID or username
limit: Number of posts (max 100)
Returns:
Dictionary with creator's top posts
"""
network = network.lower().strip()
if network not in ["twitter", "youtube"]:
raise ValueError(f"Invalid network: {network}. Must be 'twitter' or 'youtube'")
params = {
"limit": min(limit, 100)
}
data = make_request(f"/public/creator/{network}/{id}/posts/v1", params)
posts = data.get("data", [])
formatted = []
for post in posts:
formatted.append({
"id": post.get("id"),
"body": post.get("body", ""),
"title": post.get("title", ""),
"url": post.get("url", ""),
"interactions": post.get("interactions"),
"likes": post.get("likes"),
"retweets": post.get("retweets") if network == "twitter" else None,
"replies": post.get("replies"),
"views": post.get("views"),
"sentiment": post.get("sentiment"),
"coins_mentioned": post.get("coins_mentioned", []),
"topics": post.get("topics", []),
"created_at": post.get("created_at")
})
return {
"network": network,
"creator_id": id,
"posts": formatted,
"count": len(formatted)
}
def main():
"""CLI interface for LunarCrush creators tools."""
parser = argparse.ArgumentParser(
description="LunarCrush Creators API Tools",
epilog="""
Commands:
list Get top crypto influencers
creator Get single creator details
posts Get creator's top posts
Examples:
python creators.py list --limit 10
python creators.py creator --network twitter --id elonmusk
python creators.py posts --network twitter --id VitalikButerin --limit 10
""",
formatter_class=argparse.RawDescriptionHelpFormatter
)
subparsers = parser.add_subparsers(dest="command", help="Available commands")
# List command
list_parser = subparsers.add_parser("list", help="Get top creators")
list_parser.add_argument("--sort", default="influence_rank",
choices=["influence_rank", "followers", "engagement", "interactions"])
list_parser.add_argument("--network", default="all",
choices=["twitter", "youtube", "all"])
list_parser.add_argument("--limit", type=int, default=50)
# Creator command
creator_parser = subparsers.add_parser("creator", help="Get single creator")
creator_parser.add_argument("--network", required=True, choices=["twitter", "youtube"])
creator_parser.add_argument("--id", required=True, help="Creator ID or username")
# Posts command
posts_parser = subparsers.add_parser("posts", help="Get creator posts")
posts_parser.add_argument("--network", required=True, choices=["twitter", "youtube"])
posts_parser.add_argument("--id", required=True, help="Creator ID or username")
posts_parser.add_argument("--limit", type=int, default=20)
# Schema output
parser.add_argument("--schema", action="store_true", help="Output MCP schema")
args = parser.parse_args()
if args.schema:
schemas = {
"creators": MCP_CREATORS_SCHEMA,
"creator": MCP_CREATOR_SCHEMA,
"creator_posts": MCP_CREATOR_POSTS_SCHEMA
}
print(json.dumps(schemas, indent=2))
return 0
try:
if args.command == "list":
result = get_creators(sort=args.sort, network=args.network, limit=args.limit)
elif args.command == "creator":
result = get_creator(args.network, args.id)
elif args.command == "posts":
result = get_creator_posts(args.network, args.id, args.limit)
else:
parser.print_help()
return 0
print(json.dumps(result, indent=2, ensure_ascii=False))
return 0
except Exception as e:
print(json.dumps({"error": str(e)}, indent=2))
return 1
if __name__ == "__main__":
exit(main())
#!/usr/bin/env python3
"""
LunarCrush NFTs API Tools
Tools for fetching NFT collections with social metrics.
"""
import json
import argparse
from typing import Dict, Any
try:
from .utils import make_request
except ImportError:
from utils import make_request
# MCP Tool Schemas
MCP_NFTS_SCHEMA = {
"name": "lunar_nfts",
"title": "LunarCrush NFT Collections",
"description": "Get NFT collections with social metrics and Galaxy Score.",
"inputSchema": {
"type": "object",
"properties": {
"sort": {
"type": "string",
"description": "Sort field",
"default": "galaxy_score",
"enum": ["galaxy_score", "social_volume", "floor_price", "market_cap"]
},
"limit": {
"type": "integer",
"description": "Number of results (max 100)",
"default": 50,
"minimum": 1,
"maximum": 100
}
},
"additionalProperties": False
}
}
MCP_NFT_SCHEMA = {
"name": "lunar_nft",
"title": "LunarCrush Single NFT",
"description": "Get detailed metrics for a single NFT collection.",
"inputSchema": {
"type": "object",
"properties": {
"nft": {
"type": "string",
"description": "NFT collection slug or ID (e.g., 'bored-ape-yacht-club', 'cryptopunks')"
}
},
"required": ["nft"],
"additionalProperties": False
}
}
def get_nfts(
sort: str = "galaxy_score",
limit: int = 50
) -> Dict[str, Any]:
"""
Get NFT collections with social metrics and Galaxy Score.
Args:
sort: Sort field (galaxy_score, social_volume, floor_price, market_cap)
limit: Number of results (max 100)
Returns:
Dictionary with list of NFT collections
"""
params = {
"sort": sort,
"limit": min(limit, 100)
}
data = make_request("/public/nfts/list/v2", params)
nfts = data.get("data", [])
formatted = []
for nft in nfts:
formatted.append({
"id": nft.get("id"),
"name": nft.get("name", ""),
"symbol": nft.get("symbol", ""),
"slug": nft.get("slug", ""),
"floor_price": nft.get("floor_price"),
"floor_price_usd": nft.get("floor_price_usd"),
"floor_price_change_24h": nft.get("floor_price_24h_percent_change"),
"market_cap": nft.get("market_cap"),
"volume_24h": nft.get("volume_24h"),
"holders": nft.get("holders"),
"total_supply": nft.get("total_supply"),
"galaxy_score": nft.get("galaxy_score"),
"alt_rank": nft.get("alt_rank"),
"social_volume": nft.get("social_volume"),
"social_dominance": nft.get("social_dominance"),
"sentiment": nft.get("sentiment"),
"blockchain": nft.get("blockchain", ""),
"categories": nft.get("categories", []),
"image": nft.get("image", "")
})
return {
"nfts": formatted,
"count": len(formatted),
"sort_by": sort
}
def get_nft(nft: str) -> Dict[str, Any]:
"""
Get detailed metrics for a single NFT collection.
Args:
nft: NFT collection slug or ID (e.g., 'bored-ape-yacht-club')
Returns:
Dictionary with NFT collection details
"""
nft_slug = nft.lower().strip().replace(" ", "-")
data = make_request(f"/public/nfts/{nft_slug}/v1")
nft_data = data.get("data", {})
return {
"id": nft_data.get("id"),
"name": nft_data.get("name", ""),
"symbol": nft_data.get("symbol", ""),
"slug": nft_data.get("slug", nft_slug),
"description": nft_data.get("description", ""),
"floor_price": nft_data.get("floor_price"),
"floor_price_usd": nft_data.get("floor_price_usd"),
"floor_price_change_24h": nft_data.get("floor_price_24h_percent_change"),
"floor_price_change_7d": nft_data.get("floor_price_7d_percent_change"),
"market_cap": nft_data.get("market_cap"),
"volume_24h": nft_data.get("volume_24h"),
"volume_7d": nft_data.get("volume_7d"),
"sales_24h": nft_data.get("sales_24h"),
"holders": nft_data.get("holders"),
"total_supply": nft_data.get("total_supply"),
"galaxy_score": nft_data.get("galaxy_score"),
"alt_rank": nft_data.get("alt_rank"),
"social_volume": nft_data.get("social_volume"),
"social_volume_change_24h": nft_data.get("social_volume_24h_percent_change"),
"social_dominance": nft_data.get("social_dominance"),
"sentiment": nft_data.get("sentiment"),
"tweets": nft_data.get("tweets"),
"news_articles": nft_data.get("news"),
"blockchain": nft_data.get("blockchain", ""),
"contract_address": nft_data.get("contract_address", ""),
"website": nft_data.get("website", ""),
"twitter": nft_data.get("twitter", ""),
"discord": nft_data.get("discord", ""),
"opensea": nft_data.get("opensea", ""),
"categories": nft_data.get("categories", []),
"image": nft_data.get("image", ""),
"banner": nft_data.get("banner", "")
}
def main():
"""CLI interface for LunarCrush NFTs tools."""
parser = argparse.ArgumentParser(
description="LunarCrush NFTs API Tools",
epilog="""
Commands:
list Get NFT collections with social metrics
nft Get single NFT collection details
Examples:
python nfts.py list --limit 10
python nfts.py nft --slug bored-ape-yacht-club
python nfts.py nft --slug cryptopunks
""",
formatter_class=argparse.RawDescriptionHelpFormatter
)
subparsers = parser.add_subparsers(dest="command", help="Available commands")
# List command
list_parser = subparsers.add_parser("list", help="Get NFT collections")
list_parser.add_argument("--sort", default="galaxy_score",
choices=["galaxy_score", "social_volume", "floor_price", "market_cap"])
list_parser.add_argument("--limit", type=int, default=50)
# NFT command
nft_parser = subparsers.add_parser("nft", help="Get single NFT collection")
nft_parser.add_argument("--slug", required=True, help="NFT collection slug")
# Schema output
parser.add_argument("--schema", action="store_true", help="Output MCP schema")
args = parser.parse_args()
if args.schema:
schemas = {
"nfts": MCP_NFTS_SCHEMA,
"nft": MCP_NFT_SCHEMA
}
print(json.dumps(schemas, indent=2))
return 0
try:
if args.command == "list":
result = get_nfts(sort=args.sort, limit=args.limit)
elif args.command == "nft":
result = get_nft(args.slug)
else:
parser.print_help()
return 0
print(json.dumps(result, indent=2, ensure_ascii=False))
return 0
except Exception as e:
print(json.dumps({"error": str(e)}, indent=2))
return 1
if __name__ == "__main__":
exit(main())
#!/usr/bin/env python3
"""
LunarCrush Topics API Tools
Tools for fetching trending topics, news summaries, and top posts.
Provides social sentiment analysis for crypto-related topics.
"""
import json
import time
import argparse
from typing import Dict, Any
try:
from .utils import make_request
except ImportError:
from utils import make_request
# MCP Tool Schemas
MCP_TOPICS_SCHEMA = {
"name": "lunar_topics",
"title": "LunarCrush Trending Topics",
"description": "Get trending social topics in crypto.",
"inputSchema": {
"type": "object",
"properties": {
"sort": {
"type": "string",
"description": "Sort field",
"default": "interactions_24h",
"enum": ["interactions_24h", "social_dominance", "num_contributors"]
},
"limit": {
"type": "integer",
"description": "Number of results (max 100)",
"default": 50,
"minimum": 1,
"maximum": 100
}
},
"additionalProperties": False
}
}
MCP_TOPIC_SCHEMA = {
"name": "lunar_topic",
"title": "LunarCrush Single Topic",
"description": "Get metrics for a specific topic (24h aggregation).",
"inputSchema": {
"type": "object",
"properties": {
"topic": {
"type": "string",
"description": "Topic name or slug (e.g., 'bitcoin', 'defi', 'nft')"
}
},
"required": ["topic"],
"additionalProperties": False
}
}
MCP_TOPIC_SUMMARY_SCHEMA = {
"name": "lunar_topic_summary",
"title": "LunarCrush Topic Summary",
"description": "Get AI-generated news summary for a topic.",
"inputSchema": {
"type": "object",
"properties": {
"topic": {
"type": "string",
"description": "Topic name or slug"
}
},
"required": ["topic"],
"additionalProperties": False
}
}
MCP_TOPIC_POSTS_SCHEMA = {
"name": "lunar_topic_posts",
"title": "LunarCrush Topic Posts",
"description": "Get top posts for a topic.",
"inputSchema": {
"type": "object",
"properties": {
"topic": {
"type": "string",
"description": "Topic name or slug"
},
"limit": {
"type": "integer",
"description": "Number of posts (max 100)",
"default": 20,
"minimum": 1,
"maximum": 100
}
},
"required": ["topic"],
"additionalProperties": False
}
}
def get_topics(
sort: str = "interactions_24h",
limit: int = 50
) -> Dict[str, Any]:
"""
Get trending social topics in crypto.
Args:
sort: Sort field (interactions_24h, social_dominance, num_contributors)
limit: Number of results (max 100)
Returns:
Dictionary with list of trending topics
"""
params = {
"sort": sort,
"limit": min(limit, 100)
}
data = make_request("/public/topics/list/v1", params)
topics = data.get("data", [])
formatted = []
for topic in topics:
formatted.append({
"topic": topic.get("topic", ""),
"title": topic.get("title", ""),
"interactions_24h": topic.get("interactions_24h"),
"social_dominance": topic.get("social_dominance"),
"num_contributors": topic.get("num_contributors"),
"sentiment": topic.get("sentiment"),
"trend": topic.get("trend"),
"categories": topic.get("categories", [])
})
return {
"topics": formatted,
"count": len(formatted),
"sort_by": sort
}
def get_topic(topic: str) -> Dict[str, Any]:
"""
Get metrics for a specific topic (24h aggregation).
Args:
topic: Topic name or slug (e.g., 'bitcoin', 'defi', 'nft')
Returns:
Dictionary with topic metrics
"""
topic_slug = topic.lower().strip().replace(" ", "-")
data = make_request(f"/public/topic/{topic_slug}/v1")
topic_data = data.get("data", {})
return {
"topic": topic_data.get("topic", topic_slug),
"title": topic_data.get("title", ""),
"interactions_24h": topic_data.get("interactions_24h"),
"social_dominance": topic_data.get("social_dominance"),
"num_contributors": topic_data.get("num_contributors"),
"sentiment": topic_data.get("sentiment"),
"trend": topic_data.get("trend"),
"average_sentiment": topic_data.get("average_sentiment"),
"tweets": topic_data.get("tweets"),
"reddit_posts": topic_data.get("reddit_posts"),
"news_articles": topic_data.get("news"),
"youtube_videos": topic_data.get("youtube"),
"categories": topic_data.get("categories", []),
"related_topics": topic_data.get("related_topics", []),
"related_coins": topic_data.get("related_coins", [])
}
def get_topic_summary(topic: str) -> Dict[str, Any]:
"""
Get AI-generated news summary for a topic.
Args:
topic: Topic name or slug
Returns:
Dictionary with AI-generated summary and key points
"""
topic_slug = topic.lower().strip().replace(" ", "-")
data = make_request(f"/public/topic/{topic_slug}/whatsup/v1")
summary_data = data.get("data", {})
return {
"topic": topic_slug,
"summary": summary_data.get("summary", ""),
"key_points": summary_data.get("key_points", []),
"sentiment_summary": summary_data.get("sentiment_summary", ""),
"trending_narratives": summary_data.get("trending_narratives", []),
"notable_news": summary_data.get("notable_news", []),
"generated_at": summary_data.get("generated_at")
}
def _slug(value: str) -> str:
"""Normalize topic/category string to slug format used by API paths."""
return value.lower().strip().replace(" ", "-")
def _format_content_items(items):
"""Normalize news/post payload fields into a stable shape."""
formatted = []
for item in items:
formatted.append({
"id": item.get("id"),
"post_type": item.get("post_type"),
"network": item.get("network", ""),
"title": item.get("post_title") or item.get("title", ""),
"description": item.get("post_description") or item.get("description", ""),
"body": item.get("body", ""),
"url": item.get("post_link") or item.get("url", ""),
"image": item.get("post_image") or item.get("image", ""),
"author": item.get("creator_name") or item.get("author", ""),
"author_display_name": item.get("creator_display_name", ""),
"author_followers": item.get("creator_followers") or item.get("author_followers"),
"interactions_24h": item.get("interactions_24h"),
"interactions_total": item.get("interactions_total") or item.get("interactions"),
"sentiment": item.get("post_sentiment") if item.get("post_sentiment") is not None else item.get("sentiment"),
"created_at": item.get("post_created") if item.get("post_created") is not None else item.get("created_at")
})
return formatted
def get_topic_posts(topic: str, limit: int = 20) -> Dict[str, Any]:
"""
Get top posts for a topic.
Args:
topic: Topic name or slug
limit: Number of posts (max 100)
Returns:
Dictionary with top posts for the topic
"""
topic_slug = _slug(topic)
params = {"limit": min(limit, 100)}
data = make_request(f"/public/topic/{topic_slug}/posts/v1", params)
posts = _format_content_items(data.get("data", []))[:min(limit, 100)]
return {
"topic": topic_slug,
"posts": posts,
"count": len(posts)
}
def get_topic_news(topic: str, limit: int = 20) -> Dict[str, Any]:
"""
Get news feed for a topic.
Args:
topic: Topic name or slug
limit: Number of news items returned in output (max 100)
Returns:
Dictionary with topic news items
"""
topic_slug = _slug(topic)
# API quirk: /topic/:topic/news/v1 does not accept `limit` query param.
data = make_request(f"/public/topic/{topic_slug}/news/v1")
news = _format_content_items(data.get("data", []))[:min(limit, 100)]
return {
"topic": topic_slug,
"news": news,
"count": len(news)
}
def get_category_posts(category: str, limit: int = 20) -> Dict[str, Any]:
"""
Get top social posts for a category.
Args:
category: Category name or slug (e.g. defi, gaming, memecoin)
limit: Number of posts returned in output (max 100)
Returns:
Dictionary with category posts
"""
category_slug = _slug(category)
# API quirk: /category/:category/posts/v1 does not accept `limit` query param.
data = make_request(f"/public/category/{category_slug}/posts/v1")
posts = _format_content_items(data.get("data", []))[:min(limit, 100)]
return {
"category": category_slug,
"posts": posts,
"count": len(posts)
}
def get_category_news(category: str, limit: int = 20) -> Dict[str, Any]:
"""
Get news feed for a category.
Args:
category: Category name or slug (e.g. defi, gaming, memecoin)
limit: Number of news items returned in output (max 100)
Returns:
Dictionary with category news items
"""
category_slug = _slug(category)
# API quirk: /category/:category/news/v1 does not accept `limit` query param.
data = make_request(f"/public/category/{category_slug}/news/v1")
news = _format_content_items(data.get("data", []))[:min(limit, 100)]
return {
"category": category_slug,
"news": news,
"count": len(news)
}
def get_content_feed(feed_type: str, scope_type: str, scope: str, limit: int = 20) -> Dict[str, Any]:
"""
Unified content feed entry point for agent use.
Args:
feed_type: "news" or "posts"
scope_type: "topic" or "category"
scope: Topic/category value
limit: Number of items (max 100)
Returns:
Dictionary with normalized feed output
"""
feed_type = feed_type.lower().strip()
scope_type = scope_type.lower().strip()
if feed_type not in {"news", "posts"}:
raise ValueError("feed_type must be one of: news, posts")
if scope_type not in {"topic", "category"}:
raise ValueError("scope_type must be one of: topic, category")
if scope_type == "topic" and feed_type == "news":
data = get_topic_news(scope, limit)
elif scope_type == "topic" and feed_type == "posts":
data = get_topic_posts(scope, limit)
elif scope_type == "category" and feed_type == "news":
data = get_category_news(scope, limit)
else:
data = get_category_posts(scope, limit)
return {
"scope_type": scope_type,
"scope": _slug(scope),
"feed_type": feed_type,
"count": data.get("count", 0),
"items": data.get(feed_type, []) if feed_type in data else data.get("posts", [])
}
def _matches_query(item: dict, query: str) -> bool:
"""Check if a content item contains the query keyword (case-insensitive)."""
q = query.lower()
searchable = " ".join(str(v) for v in [
item.get("title", ""),
item.get("description", ""),
item.get("body", ""),
item.get("author", ""),
item.get("author_display_name", ""),
] if v)
return q in searchable.lower()
def search_content(
query: str = "",
topics: list | None = None,
categories: list | None = None,
feed_types: list | None = None,
time_window: str = "24h",
limit: int = 50,
) -> Dict[str, Any]:
"""
Cross-topic/category content search for Agent use.
Aggregates news + posts from multiple scopes, deduplicates by URL,
sorts by interactions (engagement), and optionally filters by keyword.
Args:
query: Keyword filter (empty = no filter, returns all). Searches
title + description + body + author fields.
topics: List of topic slugs to search (e.g. ["bitcoin", "defi"]).
Default: ["bitcoin", "ethereum", "solana"]
categories: List of category slugs (e.g. ["defi", "gaming"]).
Default: ["defi"]
feed_types: Which content types: "news", "posts", or both.
Default: ["news", "posts"]
time_window: Time scope filter placeholder (API returns recent content).
Accepted values: "1h", "24h", "7d", "30d". Default: "24h"
limit: Max items returned (max 200). Default: 50
Returns:
{
"query": str,
"total_fetched": int,
"items": [deduplicated, sorted items],
"count": int,
"sentiment_summary": {avg, bullish_pct, bearish_pct, neutral_pct}
}
"""
# Defaults
if topics is None:
topics = ["bitcoin", "ethereum", "solana"]
if categories is None:
categories = ["defi"]
if feed_types is None:
feed_types = ["news", "posts"]
if time_window not in {"1h", "24h", "7d", "30d"}:
time_window = "24h"
all_items: list[dict] = []
call_count = 0
def _fetch_with_retry(fn, retries=3, backoff=5):
"""Fetch with retry on rate limit (429)."""
for attempt in range(retries + 1):
try:
result = fn()
return result
except Exception as e:
err_str = str(e)
if "429" in err_str and attempt < retries:
wait = backoff * (attempt + 1)
time.sleep(wait)
else:
raise
def _safe_fetch(fn):
"""Fetch with rate-limit spacing and retry."""
nonlocal call_count
if call_count > 0:
time.sleep(1.0)
call_count += 1
try:
return _fetch_with_retry(fn)
except Exception:
return None
# Fetch from topics
for t in topics:
for ft in feed_types:
if ft == "news":
data = _safe_fetch(lambda topic=t: get_topic_news(topic, limit=100))
if data:
all_items.extend(data.get("news", []))
else:
data = _safe_fetch(lambda topic=t: get_topic_posts(topic, limit=100))
if data:
all_items.extend(data.get("posts", []))
# Fetch from categories
for c in categories:
for ft in feed_types:
if ft == "news":
data = _safe_fetch(lambda cat=c: get_category_news(cat, limit=100))
if data:
all_items.extend(data.get("news", []))
else:
data = _safe_fetch(lambda cat=c: get_category_posts(cat, limit=100))
if data:
all_items.extend(data.get("posts", []))
# Deduplicate by URL
seen_urls: set[str] = set()
deduped: list[dict] = []
for item in all_items:
url = item.get("url", "")
if url and url not in seen_urls:
seen_urls.add(url)
deduped.append(item)
elif not url:
deduped.append(item)
# Keyword filter
if query.strip():
deduped = [item for item in deduped if _matches_query(item, query)]
total_fetched = len(deduped)
# Sort by interactions (engagement-first)
deduped.sort(
key=lambda x: x.get("interactions_24h") or x.get("interactions_total") or 0,
reverse=True,
)
# Slice
result_items = deduped[:min(limit, 200)]
# Sentiment summary
sentiments = []
for item in result_items:
s = item.get("sentiment")
if s is not None:
sentiments.append(s)
sentiment_summary = {}
if sentiments:
avg = sum(sentiments) / len(sentiments)
sentiment_summary = {
"average_sentiment": round(avg, 3),
"bullish_pct": round(sum(1 for s in sentiments if s > 0) / len(sentiments) * 100, 1),
"bearish_pct": round(sum(1 for s in sentiments if s < 0) / len(sentiments) * 100, 1),
"neutral_pct": round(sum(1 for s in sentiments if s == 0) / len(sentiments) * 100, 1),
"sample_count": len(sentiments),
}
return {
"query": query,
"time_window": time_window,
"total_fetched": total_fetched,
"items": result_items,
"count": len(result_items),
"sentiment_summary": sentiment_summary,
}
def main():
"""CLI interface for LunarCrush topics tools."""
parser = argparse.ArgumentParser(
description="LunarCrush Topics API Tools",
epilog="""
Commands:
list Get trending topics
topic Get single topic metrics
summary Get AI-generated topic summary
posts Get top posts for topic
Examples:
python topics.py list --limit 10
python topics.py topic --name bitcoin
python topics.py summary --name defi
python topics.py posts --name nft --limit 20
""",
formatter_class=argparse.RawDescriptionHelpFormatter
)
subparsers = parser.add_subparsers(dest="command", help="Available commands")
# List command
list_parser = subparsers.add_parser("list", help="Get trending topics")
list_parser.add_argument("--sort", default="interactions_24h",
choices=["interactions_24h", "social_dominance", "num_contributors"])
list_parser.add_argument("--limit", type=int, default=50)
# Topic command
topic_parser = subparsers.add_parser("topic", help="Get single topic metrics")
topic_parser.add_argument("--name", required=True, help="Topic name or slug")
# Summary command
summary_parser = subparsers.add_parser("summary", help="Get topic AI summary")
summary_parser.add_argument("--name", required=True, help="Topic name")
# Posts command
posts_parser = subparsers.add_parser("posts", help="Get topic posts")
posts_parser.add_argument("--name", required=True, help="Topic name")
posts_parser.add_argument("--limit", type=int, default=20)
# Schema output
parser.add_argument("--schema", action="store_true", help="Output MCP schema")
args = parser.parse_args()
if args.schema:
schemas = {
"topics": MCP_TOPICS_SCHEMA,
"topic": MCP_TOPIC_SCHEMA,
"topic_summary": MCP_TOPIC_SUMMARY_SCHEMA,
"topic_posts": MCP_TOPIC_POSTS_SCHEMA
}
print(json.dumps(schemas, indent=2))
return 0
try:
if args.command == "list":
result = get_topics(sort=args.sort, limit=args.limit)
elif args.command == "topic":
result = get_topic(args.name)
elif args.command == "summary":
result = get_topic_summary(args.name)
elif args.command == "posts":
result = get_topic_posts(args.name, args.limit)
else:
parser.print_help()
return 0
print(json.dumps(result, indent=2, ensure_ascii=False))
return 0
except Exception as e:
print(json.dumps({"error": str(e)}, indent=2))
return 1
if __name__ == "__main__":
exit(main())
#!/usr/bin/env python3
"""
LunarCrush API Utilities
Shared utilities for LunarCrush API tools including:
- API request handling with authentication
- Rate limiting and error handling
- Response parsing
"""
import os
import time
import requests
from typing import Dict, Any, Optional
from dotenv import load_dotenv
from core.http_client import proxied_get
# Load environment variables
load_dotenv()
# LunarCrush API base URL
LUNARCRUSH_API_BASE = "https://lunarcrush.com/api4"
def get_api_key() -> str:
"""Get LunarCrush API key from environment."""
api_key = os.getenv("LUNARCRUSH_API_KEY")
if not api_key:
raise ValueError(
"LUNARCRUSH_API_KEY environment variable is required. "
"Get your API key from https://lunarcrush.com/developers"
)
return api_key
def make_request(
endpoint: str,
params: Optional[Dict[str, Any]] = None,
timeout: int = 30,
retries: int = 2,
backoff_seconds: float = 1.2,
) -> Dict[str, Any]:
"""
Make authenticated request to LunarCrush API.
Args:
endpoint: API endpoint path (e.g., "/public/coins/list/v2")
params: Optional query parameters
timeout: Request timeout in seconds
Returns:
Parsed JSON response
Raises:
ValueError: If API key is missing
requests.RequestException: If request fails
"""
api_key = get_api_key()
url = f"{LUNARCRUSH_API_BASE}{endpoint}"
headers = {
"Authorization": f"Bearer {api_key}",
"Accept": "application/json"
}
for attempt in range(retries + 1):
try:
response = proxied_get(
url,
headers=headers,
params=params,
timeout=timeout
)
response.raise_for_status()
return response.json()
except requests.exceptions.Timeout:
if attempt < retries:
time.sleep(backoff_seconds * (attempt + 1))
continue
raise requests.RequestException("Request timeout - LunarCrush API may be slow")
except requests.exceptions.ConnectionError:
if attempt < retries:
time.sleep(backoff_seconds * (attempt + 1))
continue
raise requests.RequestException("Connection error - check internet connection")
except requests.exceptions.HTTPError as e:
if e.response.status_code == 401:
raise ValueError("Invalid LUNARCRUSH_API_KEY - check your API key")
elif e.response.status_code == 429:
if attempt < retries:
time.sleep(backoff_seconds * (attempt + 1))
continue
raise requests.RequestException("Rate limit exceeded - please wait before retrying")
raise requests.RequestException(f"API request failed: {e}")
raise requests.RequestException("API request failed after retries")
def format_galaxy_score(score: Optional[float]) -> str:
"""
Format Galaxy Score with interpretation.
Galaxy Score (0-100):
- 80-100: Exceptional momentum
- 60-79: Strong engagement
- 40-59: Moderate activity
- 0-39: Low presence
"""
if score is None:
return "N/A"
if score >= 80:
return f"{score:.1f} (Exceptional)"
elif score >= 60:
return f"{score:.1f} (Strong)"
elif score >= 40:
return f"{score:.1f} (Moderate)"
else:
return f"{score:.1f} (Low)"
def format_alt_rank(rank: Optional[int], total: Optional[int] = None) -> str:
"""
Format AltRank with interpretation.
AltRank measures relative social performance vs other alts.
Lower = better relative performance.
"""
if rank is None:
return "N/A"
if total:
return f"#{rank} of {total}"
return f"#{rank}"
def normalize_symbol(symbol: str) -> str:
"""Normalize coin symbol for API requests."""
return symbol.upper().strip()
def parse_time_series_bucket(bucket: str) -> str:
"""
Validate and normalize time series bucket parameter.
Valid buckets: hour, day, week
"""
valid_buckets = ["hour", "day", "week"]
bucket = bucket.lower().strip()
if bucket not in valid_buckets:
raise ValueError(f"Invalid bucket: {bucket}. Must be one of: {valid_buckets}")
return bucket