
Token Economics
- 201 installs
- 257 repo stars
- Updated June 24, 2026
- agiprolabs/claude-trading-skills
token-economics is a Claude Code skill for analyzing token supply dynamics, vesting, inflation modeling, and valuation frameworks for crypto tokens.
About
token-economics is a Claude Code skill for analyzing crypto token supply dynamics, vesting, inflation, and valuation. It covers total versus circulating supply, market cap versus fully diluted valuation, net inflation rate, unlock-impact modeling, selling-pressure estimation, and token distribution scoring with red-flag thresholds. A developer uses it to estimate dilution risk and anticipate price-moving unlock events when evaluating a token.
- Supply, FDV/MCap, and net inflation-rate analysis
- Vesting/unlock impact modeling and selling-pressure estimation
- Token distribution quality scoring with red-flag thresholds
Token Economics by the numbers
- 201 all-time installs (skills.sh)
- Ranked #466 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
token-economics capabilities & compatibility
- Capabilities
- tokenomics analysis · supply modeling · valuation · dilution risk
- Use cases
- trading · data analysis
- Pricing
- Free
What token-economics says it does
Tokenomics — the study of token supply dynamics, distribution, and value accrual — is one of the most important factors in crypto asset analysis.
A large unlock releasing 10% of circulating supply in one day often causes 5-20% drawdowns
npx skills add https://github.com/agiprolabs/claude-trading-skills --skill token-economicsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 201 |
|---|---|
| repo stars | ★ 257 |
| Last updated | June 24, 2026 |
| Repository | agiprolabs/claude-trading-skills ↗ |
What it does
Analyze token supply, vesting, inflation, and valuation to estimate dilution risk and unlock impact.
Who is it for?
Estimating dilution risk, unlock impact, and valuation when evaluating a crypto token.
Skip if: Live token price feeds or on-chain data retrieval, which it defers to data sources and other skills.
When should I use this skill?
You need to assess a token's supply, vesting schedule, inflation, or distribution before trading.
What you get
A dilution-risk, unlock-impact, and distribution-quality assessment for a crypto token.
- A dilution-risk and unlock-impact assessment
- A token distribution quality score
By the numbers
- 4-band FDV/MCap dilution-risk table
- 4-band unlock-impact model vs daily volume
- Distribution red-flag thresholds per allocation category
Files
Token Economics
Tokenomics — the study of token supply dynamics, distribution, and value accrual — is one of the most important factors in crypto asset analysis. Supply changes directly affect price: new tokens entering circulation create selling pressure, while burns and locks reduce it. Understanding these dynamics lets you estimate dilution risk, identify overvalued or undervalued tokens, and anticipate price-moving unlock events.
Why Tokenomics Matters
Price is a function of demand and supply. In crypto, supply is programmable and constantly changing:
- A token inflating at 50%/year needs 50% demand growth just to maintain price
- A large unlock releasing 10% of circulating supply in one day often causes 5-20% drawdowns
- Tokens with >80% of supply locked have extreme dilution risk ahead
- Protocols that burn fees can become net deflationary, creating structural price support
Key Supply Concepts
Total Supply vs Circulating Supply
total_supply = maximum tokens that will ever exist (or current total minted)
circulating_supply = tokens currently available for trading
locked_supply = total_supply - circulating_supply
circulating_pct = circulating_supply / total_supply * 100Market Cap vs Fully Diluted Valuation
market_cap = price * circulating_supply
fdv = price * total_supply
fdv_mcap_ratio = fdv / market_capThe FDV/MCap ratio measures future dilution risk:
| FDV/MCap | Dilution Risk | Interpretation |
|---|---|---|
| 1.0-1.5 | Low | Most supply already circulating |
| 1.5-3.0 | Moderate | Significant supply still locked |
| 3.0-5.0 | High | Majority of supply not yet released |
| >5.0 | Very High | Token will face massive dilution |
Net Inflation Rate
annual_new_tokens = emissions + vesting_unlocks + rewards
annual_burned = fee_burns + buyback_burns
net_new_tokens = annual_new_tokens - annual_burned
net_inflation_rate = net_new_tokens / circulating_supply * 100 # percent per yearSupply Dynamics
Inflationary Pressure (tokens entering circulation)
- Emissions: Block rewards, liquidity mining, staking rewards
- Vesting unlocks: Team, investor, and advisor tokens unlocking on schedule
- Unlock events: Large one-time releases (cliff expirations)
- Treasury spending: DAO or foundation distributing tokens
Deflationary Pressure (tokens leaving circulation)
- Fee burns: Protocol burns a portion of transaction fees (like EIP-1559)
- Buyback and burn: Protocol uses revenue to buy and permanently destroy tokens
- Staking locks: Tokens locked in staking (temporarily removed from circulation)
- Lost tokens: Permanently inaccessible tokens (lost keys, burn addresses)
Selling Pressure Estimation
daily_emissions_usd = daily_new_tokens * token_price
percent_sold = 0.50 # assume 50% of new tokens are sold (conservative)
daily_sell_pressure = daily_emissions_usd * percent_sold
sell_pressure_ratio = daily_sell_pressure / daily_volume
# > 0.05 (5%) = significant selling pressure
# > 0.10 (10%) = heavy selling pressureVesting and Unlock Schedules
Key Concepts
- Cliff: Period before any tokens unlock (typically 6-12 months)
- Linear vesting: Constant rate of unlock after cliff (monthly or daily)
- Stepped vesting: Periodic unlocks at set intervals (quarterly)
- TGE unlock: Percentage released at Token Generation Event
Analyzing Unlock Impact
unlock_amount_tokens = 10_000_000
avg_daily_volume_tokens = 5_000_000
unlock_volume_ratio = unlock_amount_tokens / avg_daily_volume_tokens
# Impact assessment:
# < 1x daily volume: minor impact
# 1-5x daily volume: moderate impact, expect 2-5% drawdown
# 5-10x daily volume: major impact, expect 5-15% drawdown
# > 10x daily volume: severe impact, expect 10-30% drawdownTracking Sources
- CoinGecko / CoinMarketCap: Basic supply data
- Token Terminal: Revenue and valuation metrics
- Token Unlocks (token.unlocks.app): Detailed unlock schedules
- Project documentation: Whitepapers, tokenomics pages
- On-chain: Vesting contract state, treasury balances
Token Distribution Analysis
Typical Allocation Ranges
| Category | Typical Range | Red Flag |
|---|---|---|
| Team/Founders | 15-25% | >30% |
| Investors (Seed+Series) | 10-30% | >40% |
| Community/Ecosystem | 20-40% | <15% |
| Treasury/DAO | 10-20% | <5% |
| Public Sale | 5-20% | <2% |
| Advisors | 2-5% | >10% |
Distribution Red Flags
- >50% insider allocation (team + investors): Insiders control price
- Short vesting (<1 year): Quick dump risk
- No cliff: Immediate selling from day one
- Large single wallets: Concentration risk (use
token-holder-analysisskill) - Unlabeled large allocations: Hidden insider holdings
Distribution Quality Score
def distribution_score(team_pct: float, investor_pct: float,
community_pct: float, cliff_months: int,
vesting_months: int) -> str:
"""Rate token distribution quality."""
score = 0
insider_pct = team_pct + investor_pct
if insider_pct < 30: score += 3
elif insider_pct < 50: score += 1
if community_pct > 30: score += 2
elif community_pct > 20: score += 1
if cliff_months >= 12: score += 2
elif cliff_months >= 6: score += 1
if vesting_months >= 36: score += 2
elif vesting_months >= 24: score += 1
if score >= 8: return "Excellent"
if score >= 6: return "Good"
if score >= 4: return "Moderate"
return "Poor"Valuation Frameworks
Revenue-Based Metrics
# Price-to-Earnings (for fee-generating protocols)
pe_ratio = fdv / annualized_net_revenue
# Price-to-Sales
ps_ratio = fdv / annualized_total_volume
# Price-to-Fees
pf_ratio = fdv / annualized_protocol_fees
# Revenue Multiple (adjusted for token value accrual)
rev_multiple = fdv / (annualized_fees * fee_share_to_token_holders)Typical ranges (crypto, highly variable):
- P/E: 10x-100x+ (DeFi protocols)
- P/S: 0.5x-50x
- P/F: 20x-500x
Network Value Metrics
# Network Value to Transactions (NVT)
nvt = market_cap / daily_transaction_volume_usd
# High NVT (>100): potentially overvalued or store-of-value
# Low NVT (<20): potentially undervalued or high activity
# Market Value to Realized Value (MVRV)
# realized_value = sum of each token at its last-moved price
mvrv = market_cap / realized_value
# MVRV > 3.0: historically overvalued zone
# MVRV < 1.0: historically undervalued zoneComparable Analysis
def comparable_analysis(target: dict, peers: list[dict]) -> dict:
"""Compare target token metrics against peer group.
Each dict has: name, fdv, revenue, tvl, users
Returns premium/discount percentages.
"""
peer_fdv_rev = [p["fdv"] / p["revenue"] for p in peers if p["revenue"] > 0]
peer_fdv_tvl = [p["fdv"] / p["tvl"] for p in peers if p["tvl"] > 0]
avg_fdv_rev = sum(peer_fdv_rev) / len(peer_fdv_rev) if peer_fdv_rev else 0
avg_fdv_tvl = sum(peer_fdv_tvl) / len(peer_fdv_tvl) if peer_fdv_tvl else 0
target_fdv_rev = target["fdv"] / target["revenue"] if target["revenue"] > 0 else 0
target_fdv_tvl = target["fdv"] / target["tvl"] if target["tvl"] > 0 else 0
return {
"fdv_rev_premium": (target_fdv_rev / avg_fdv_rev - 1) * 100 if avg_fdv_rev else None,
"fdv_tvl_premium": (target_fdv_tvl / avg_fdv_tvl - 1) * 100 if avg_fdv_tvl else None,
}Token Value Accrual Mechanisms
| Mechanism | Description | Valuation Impact |
|---|---|---|
| Fee sharing | Holders receive protocol revenue | Direct cash flow, use DCF |
| Governance | Voting rights on protocol | Hard to value, often overpriced |
| Utility | Required for protocol use | Demand scales with usage |
| Buyback & burn | Protocol buys and burns | Reduces supply, structural bid |
| Staking rewards | Yield from staking | Inflationary if from emissions |
| veToken model | Lock for boosted rewards + governance | Reduces circulating supply |
PumpFun Token Economics
PumpFun tokens on Solana have simplified tokenomics:
- Fixed supply: 1,000,000,000 tokens (1 billion)
- No vesting: All tokens available immediately at launch
- No team allocation: 100% available on bonding curve
- Bonding curve pricing: Price determined by curve math, not supply changes
- Post-graduation: After bonding curve completes, supply is fully liquid on Raydium
- No inflation: No emissions, no staking rewards, no additional minting
Analysis focus for PumpFun tokens shifts from supply dynamics to:
- Holder concentration (use
token-holder-analysis) - Volume sustainability
- Liquidity depth (use
liquidity-analysis) - Dev wallet behavior
Integration with Other Skills
| Skill | Integration |
|---|---|
defillama-api | Fetch TVL, revenue, fees for valuation metrics |
token-holder-analysis | Analyze holder concentration and whale behavior |
coingecko-api | Fetch supply data, market cap, FDV |
liquidity-analysis | Assess trading liquidity relative to supply |
risk-management | Supply dilution as risk factor |
position-sizing | Adjust size for dilution risk |
Files
References
references/supply_analysis.md— Circulating supply tracking, inflation modeling, unlock analysis, burn mechanicsreferences/valuation_frameworks.md— Revenue-based valuation, NVT, MVRV, comparable analysis, value accrual
Scripts
scripts/tokenomics_analyzer.py— Fetch and analyze token supply metrics from CoinGecko, calculate dilution risk and basic valuationsscripts/supply_modeler.py— Project token supply over 12 months given emission and burn parameters, scenario analysis
Supply Analysis Reference
Comprehensive guide to tracking circulating supply, modeling inflation, analyzing unlock schedules, and evaluating burn mechanisms.
Circulating Supply Tracking
Data Sources
CoinGecko API (free tier):
import httpx
resp = httpx.get(
"https://api.coingecko.com/api/v3/coins/solana",
params={"localization": "false", "tickers": "false",
"community_data": "false", "developer_data": "false"}
)
data = resp.json()
circulating = data["market_data"]["circulating_supply"]
total = data["market_data"]["total_supply"]
max_supply = data["market_data"]["max_supply"] # None if unlimitedOn-chain calculation (Solana SPL tokens):
circulating = total_minted - burned - locked_in_vesting - treasury_held - staked_lockedCommon locked account types:
- Team vesting contracts (PDA accounts with time-lock)
- DAO treasury multisigs
- Staking contracts (may or may not count as circulating)
- Burn address (
1111111111111111111111111111111111)
Supply Classification
| Category | Counts as Circulating? | Notes |
|---|---|---|
| Trading on DEX/CEX | Yes | Active market supply |
| In user wallets | Yes | Potentially tradeable |
| Staked (liquid staking) | Yes | Can be unstaked and sold |
| Staked (locked period) | Depends | Locked for fixed term |
| Team vesting (locked) | No | Cannot be sold yet |
| Treasury (DAO-controlled) | Depends | Requires governance vote |
| Burned | No | Permanently removed |
Inflation Modeling
Emission Sources
1. Block rewards / Staking rewards: Continuous issuance to validators/stakers 2. Liquidity mining: Token incentives for LPs (often largest source) 3. Ecosystem grants: Foundation distributing to builders 4. Vesting unlocks: Scheduled release of locked tokens
Calculating Current Inflation Rate
def annual_inflation_rate(
daily_staking_rewards: float,
daily_lm_emissions: float,
monthly_vesting_unlocks: float,
annual_grants: float,
circulating_supply: float
) -> float:
"""Calculate annualized inflation rate as a percentage.
Args:
daily_staking_rewards: Tokens issued as staking rewards per day.
daily_lm_emissions: Tokens emitted for liquidity mining per day.
monthly_vesting_unlocks: Tokens unlocking from vesting per month.
annual_grants: Tokens distributed as ecosystem grants per year.
circulating_supply: Current circulating supply.
Returns:
Annualized inflation rate as percentage.
"""
annual_new = (
daily_staking_rewards * 365
+ daily_lm_emissions * 365
+ monthly_vesting_unlocks * 12
+ annual_grants
)
return (annual_new / circulating_supply) * 100Inflation Impact Tiers
| Annual Inflation | Impact | Examples |
|---|---|---|
| 0-2% | Minimal | Bitcoin (post-2024 halving) |
| 2-5% | Low | Mature L1s, established protocols |
| 5-15% | Moderate | Growing protocols with LM programs |
| 15-30% | High | Early-stage with aggressive emissions |
| >30% | Severe | Yield farms, Ponzi-adjacent |
Projecting Supply Over Time
def project_supply(
current_circulating: float,
total_supply: float,
monthly_emissions: float,
monthly_burns: float,
vesting_schedule: list[float], # monthly unlock amounts, 12 entries
months: int = 12
) -> list[dict]:
"""Project circulating supply month by month.
Args:
current_circulating: Starting circulating supply.
total_supply: Maximum total supply cap.
monthly_emissions: New tokens from emissions per month.
monthly_burns: Tokens burned per month.
vesting_schedule: List of monthly vesting unlock amounts.
months: Number of months to project.
Returns:
List of monthly projections with supply and inflation data.
"""
projections = []
circ = current_circulating
for m in range(months):
vest = vesting_schedule[m] if m < len(vesting_schedule) else 0
new_tokens = monthly_emissions + vest
net_new = new_tokens - monthly_burns
circ = min(circ + net_new, total_supply)
monthly_rate = net_new / (circ - net_new) * 100 if circ > net_new else 0
projections.append({
"month": m + 1,
"circulating": circ,
"circulating_pct": circ / total_supply * 100,
"monthly_inflation_pct": monthly_rate,
"net_new_tokens": net_new,
})
return projectionsUnlock Schedule Analysis
Unlock Types and Impact
Cliff unlocks (one-time large release):
- Typically 10-25% of allocation at cliff expiration
- Often the highest-impact event
- Price impact: correlates with unlock_amount / daily_volume ratio
Linear vesting (continuous drip):
- Steady daily/monthly release after cliff
- Creates consistent but manageable sell pressure
- Impact spread over time
Stepped unlocks (periodic releases):
- Quarterly or monthly batch releases
- Each step is a mini-cliff event
- Plan for each step individually
Estimating Price Impact
def estimate_unlock_impact(
unlock_tokens: float,
token_price: float,
avg_daily_volume_usd: float,
sell_assumption: float = 0.30
) -> dict:
"""Estimate the price impact of a token unlock event.
Args:
unlock_tokens: Number of tokens being unlocked.
token_price: Current token price in USD.
avg_daily_volume_usd: Average daily trading volume in USD.
sell_assumption: Fraction of unlocked tokens expected to be sold.
Returns:
Impact assessment with severity rating.
"""
unlock_value = unlock_tokens * token_price
expected_sell = unlock_value * sell_assumption
days_to_absorb = expected_sell / avg_daily_volume_usd if avg_daily_volume_usd > 0 else float("inf")
if days_to_absorb < 0.5:
severity = "Minor"
est_drawdown = "0-2%"
elif days_to_absorb < 2:
severity = "Moderate"
est_drawdown = "2-5%"
elif days_to_absorb < 5:
severity = "Significant"
est_drawdown = "5-15%"
else:
severity = "Severe"
est_drawdown = "15-30%+"
return {
"unlock_value_usd": unlock_value,
"expected_sell_usd": expected_sell,
"days_to_absorb": round(days_to_absorb, 2),
"severity": severity,
"estimated_drawdown": est_drawdown,
}Historical Unlock Behavior
Empirical observations from major token unlocks:
- Team/founder tokens: ~30-50% sold within 30 days of unlock
- VC/investor tokens: ~40-70% sold within 30 days (profit-taking)
- Ecosystem/community: ~10-20% sold (often redistributed, not dumped)
- Staking unlocks: ~5-15% sold (most restake)
Burn Mechanisms
Types of Burns
| Mechanism | Predictability | Examples |
|---|---|---|
| Fee burn (per-tx) | High (scales with usage) | Ethereum EIP-1559, Solana partial burns |
| Buyback & burn | Medium (depends on revenue) | BNB quarterly burns, MKR surplus auctions |
| Manual/scheduled burn | High (announced schedule) | Periodic planned burns |
| Deflationary tax | High (on every transfer) | Reflection tokens (often unsustainable) |
Net Inflation Calculation
def net_inflation(
annual_emissions: float,
annual_burns: float,
circulating_supply: float
) -> dict:
"""Calculate net inflation accounting for burns.
Args:
annual_emissions: Total new tokens emitted per year.
annual_burns: Total tokens burned per year.
circulating_supply: Current circulating supply.
Returns:
Net inflation metrics.
"""
net = annual_emissions - annual_burns
rate = (net / circulating_supply) * 100
return {
"gross_inflation_pct": (annual_emissions / circulating_supply) * 100,
"burn_rate_pct": (annual_burns / circulating_supply) * 100,
"net_inflation_pct": rate,
"is_deflationary": net < 0,
"years_to_double": 72 / rate if rate > 0 else None, # Rule of 72
}Worked Example: Analyzing SOL Supply Dynamics
Solana (SOL) as of early 2025:
- Total supply: ~590M SOL
- Circulating supply: ~430M SOL
- Circulating %: ~73%
- FDV/MCap ratio: ~1.37 (low dilution risk)
- Staking rewards: ~5.5% APY (inflationary but offset by 50% fee burn)
- Annual inflation: ~5% gross, ~4.5% net (after fee burns)
- No vesting events remaining (all early vesting completed)
Assessment:
- Low dilution risk (FDV/MCap near 1)
- Moderate net inflation (~4.5%/year)
- Fee burns increasing with network usage
- Path to net deflation if transaction volume grows significantly
Valuation Frameworks Reference
Comprehensive guide to revenue-based valuation, network value metrics, comparable analysis, and token value accrual assessment for crypto assets.
Revenue-Based Valuation
Price-to-Earnings (P/E)
P/E = FDV / annualized_net_revenue- Net revenue = protocol fees retained (not distributed to LPs or users)
- Most directly comparable to traditional P/E
- Only applicable to fee-generating protocols
Crypto P/E ranges (as of 2025, highly variable):
| Range | Interpretation | Typical Protocols |
|---|---|---|
| <10x | Potentially undervalued or declining | Legacy protocols losing share |
| 10-30x | Reasonable for mature protocols | Established DEXs, lending |
| 30-100x | Growth premium | Fast-growing protocols |
| 100-500x | High growth expectations | New category leaders |
| >500x | Speculative or minimal revenue | Most tokens |
Price-to-Sales (P/S)
P/S = FDV / annualized_total_volume- Uses total volume, not just protocol revenue
- Better for comparing protocols with different fee structures
- Lower P/S suggests better value per unit of activity
Price-to-Fees (P/F)
P/F = FDV / annualized_total_fees- Total fees generated (including LP fees, not just protocol take)
- Measures total economic activity, not just protocol capture
- Useful for comparing DEXs with different fee splits
Revenue Quality Assessment
Not all revenue is equal:
| Revenue Type | Quality | Sustainability |
|---|---|---|
| Trading fees (organic) | High | Scales with market activity |
| Lending interest | High | Consistent in all markets |
| Liquidation fees | Medium | Spikes in volatility, zero otherwise |
| LM-incentivized volume | Low | Disappears when incentives end |
| MEV revenue | Medium | Structural but variable |
| Bridge fees | Medium | Dependent on cross-chain activity |
def revenue_quality_score(
organic_fees_pct: float,
incentivized_fees_pct: float,
one_time_pct: float,
months_of_data: int
) -> dict:
"""Score revenue quality for valuation purposes.
Args:
organic_fees_pct: Percentage of fees from organic activity.
incentivized_fees_pct: Percentage from LM-incentivized activity.
one_time_pct: Percentage from one-time events.
months_of_data: How many months of revenue history.
Returns:
Quality assessment with adjustment factor.
"""
base = organic_fees_pct * 1.0 + incentivized_fees_pct * 0.3 + one_time_pct * 0.1
history_factor = min(months_of_data / 12, 1.0) # penalize short history
score = base * history_factor
if score > 70:
quality = "High"
valuation_discount = 0 # no discount needed
elif score > 40:
quality = "Medium"
valuation_discount = 20 # apply 20% discount to revenue multiple
else:
quality = "Low"
valuation_discount = 50 # apply 50% discount
return {
"quality": quality,
"score": round(score, 1),
"valuation_discount_pct": valuation_discount,
}Network Value to Transactions (NVT)
Calculation
NVT = market_cap / daily_on_chain_transaction_volume_usdNVT is the crypto equivalent of P/E for networks without traditional revenue. It measures how much the market values each dollar of on-chain activity.
Interpretation
| NVT Range | Signal | Context |
|---|---|---|
| <20 | Potentially undervalued | High activity relative to valuation |
| 20-50 | Fair value zone | Balanced activity and valuation |
| 50-100 | Potentially overvalued | Low activity relative to valuation |
| >100 | Overvalued or store-of-value | May be justified for BTC-like assets |
NVT Signal (smoothed)
def nvt_signal(
market_cap: float,
daily_volumes: list[float],
window: int = 90
) -> float:
"""Calculate smoothed NVT Signal using moving average of volume.
Args:
market_cap: Current market capitalization.
daily_volumes: List of daily on-chain volumes (most recent last).
window: Moving average window in days.
Returns:
NVT Signal value.
"""
if len(daily_volumes) < window:
window = len(daily_volumes)
avg_volume = sum(daily_volumes[-window:]) / window
return market_cap / avg_volume if avg_volume > 0 else float("inf")NVT Signal smooths out daily volume noise, giving a more reliable valuation indicator than raw NVT.
Market Value to Realized Value (MVRV)
Concept
Realized value sums each token at the price it last moved on-chain, representing the aggregate cost basis of all holders.
MVRV = market_cap / realized_valueInterpretation
| MVRV | Zone | Historical Meaning |
|---|---|---|
| <0.8 | Deep undervalue | Aggregate holders at a loss, capitulation |
| 0.8-1.0 | Undervalue | Most holders near breakeven or slightly down |
| 1.0-2.0 | Fair value | Moderate unrealized gains |
| 2.0-3.0 | Overvalue caution | Significant unrealized gains, profit-taking likely |
| >3.0 | Overvalue danger | Historical tops often occur here |
Limitations
- Only available for UTXO chains (BTC) and some account-based chains
- Not applicable to most DeFi tokens (insufficient on-chain transfer data)
- Realized value can be skewed by exchange movements
- Works best as a macro cycle indicator, not for short-term trading
Comparable Analysis
Methodology
1. Select peer group: Tokens in the same sector (DEX, lending, L1, L2, etc.) 2. Gather metrics: FDV, revenue, TVL, users, volume for each 3. Calculate ratios: FDV/Revenue, FDV/TVL, FDV/DAU 4. Rank: Where does the target fall vs peers? 5. Assess premium/discount: Is it justified by growth, moat, risk?
Standard Comparison Metrics
def build_comp_table(protocols: list[dict]) -> list[dict]:
"""Build comparable analysis table for a set of protocols.
Args:
protocols: List of dicts with keys: name, fdv, revenue, tvl, dau, volume.
Returns:
List of dicts with calculated valuation ratios.
"""
result = []
for p in protocols:
row = {"name": p["name"], "fdv": p["fdv"]}
row["fdv_revenue"] = p["fdv"] / p["revenue"] if p.get("revenue", 0) > 0 else None
row["fdv_tvl"] = p["fdv"] / p["tvl"] if p.get("tvl", 0) > 0 else None
row["fdv_dau"] = p["fdv"] / p["dau"] if p.get("dau", 0) > 0 else None
row["revenue_per_tvl"] = p.get("revenue", 0) / p["tvl"] if p.get("tvl", 0) > 0 else None
result.append(row)
return resultSector-Specific Benchmarks (approximate ranges, 2025)
| Sector | FDV/Revenue | FDV/TVL | Notes |
|---|---|---|---|
| DEX | 20-100x | 1-10x | Higher for growing DEXs |
| Lending | 30-150x | 0.5-5x | Stable revenue, lower multiples |
| L1 | 50-500x | 5-50x | Premium for ecosystem size |
| L2 | 100-1000x | 10-100x | Early stage, growth premium |
| Perpetuals | 15-80x | 2-15x | High revenue efficiency |
Token Value Accrual
Fee Sharing
Tokens that distribute protocol revenue directly to holders:
def fee_share_yield(
annualized_fees: float,
fee_share_pct: float,
token_fdv: float
) -> float:
"""Calculate implied yield from fee sharing.
Args:
annualized_fees: Total protocol fees per year in USD.
fee_share_pct: Percentage of fees distributed to token holders.
token_fdv: Fully diluted valuation of the token.
Returns:
Implied annual yield as a percentage.
"""
distributed = annualized_fees * (fee_share_pct / 100)
return (distributed / token_fdv) * 100Buyback & Burn Valuation
def buyback_burn_impact(
annual_burn_usd: float,
circulating_supply: float,
token_price: float
) -> dict:
"""Calculate the supply reduction from buyback and burn.
Args:
annual_burn_usd: USD value of annual buyback and burn.
circulating_supply: Current circulating supply.
token_price: Current token price.
Returns:
Annual supply reduction metrics.
"""
tokens_burned = annual_burn_usd / token_price
supply_reduction_pct = (tokens_burned / circulating_supply) * 100
return {
"tokens_burned_annually": tokens_burned,
"supply_reduction_pct": supply_reduction_pct,
"implied_yield_pct": supply_reduction_pct, # equivalent to dividend yield
}veToken Model
Tokens locked for governance + boosted rewards (e.g., CRV -> veCRV):
Value drivers:
- Reduces circulating supply (locked for 1-4 years typically)
- Generates yield from fees + bribes
- Governance power over emissions
- Longer lock = more power (incentivizes long-term holding)
Assessment:
def ve_token_metrics(total_supply: float, locked_in_ve: float,
annual_fees_to_ve: float, annual_bribes: float,
ve_token_price: float) -> dict:
"""Calculate veToken model metrics."""
lock_pct = (locked_in_ve / total_supply) * 100
ve_value = locked_in_ve * ve_token_price
total_yield = annual_fees_to_ve + annual_bribes
yield_pct = (total_yield / ve_value) * 100 if ve_value > 0 else 0
return {
"lock_pct": round(lock_pct, 2),
"ve_yield_pct": round(yield_pct, 2),
"health": "Strong" if lock_pct > 50 and yield_pct > 5 else "Moderate" if lock_pct > 30 else "Weak",
}Summary: Valuation Workflow
1. Supply: FDV/MCap ratio, inflation rate, upcoming unlocks 2. Revenue: P/E, P/F — is there real, sustainable revenue? 3. Peers: Compare ratios to similar protocols 4. Accrual: How does the token capture value (fees, burns, utility)? 5. Risks: Dilution, insider allocation, revenue sustainability
#!/usr/bin/env python3
"""Supply Modeler — Project token supply dynamics over 12 months.
Takes supply parameters (total supply, circulating, emissions, burns, vesting)
and projects monthly supply changes, inflation rates, and FDV/MCap evolution.
Includes scenario analysis with and without burn mechanisms.
Usage:
python scripts/supply_modeler.py # runs demo scenario
python scripts/supply_modeler.py --interactive # prompts for parameters
Dependencies:
None (pure math, no external packages)
Environment Variables:
None required
"""
import argparse
import sys
from typing import Optional
# ── Data Models ─────────────────────────────────────────────────────
class SupplyParams:
"""Parameters for supply projection model."""
def __init__(
self,
name: str,
total_supply: float,
circulating_supply: float,
token_price: float,
monthly_emissions: float,
monthly_burns: float,
vesting_schedule: Optional[list[float]] = None,
label: str = "Base Case",
) -> None:
"""Initialize supply parameters.
Args:
name: Token name or identifier.
total_supply: Maximum total supply of the token.
circulating_supply: Current circulating supply.
token_price: Current token price in USD.
monthly_emissions: New tokens emitted per month (staking, LM).
monthly_burns: Tokens burned per month.
vesting_schedule: List of 12 monthly vesting unlock amounts.
If None, assumes zero vesting.
label: Scenario label for display.
"""
self.name = name
self.total_supply = total_supply
self.circulating_supply = circulating_supply
self.token_price = token_price
self.monthly_emissions = monthly_emissions
self.monthly_burns = monthly_burns
self.vesting_schedule = vesting_schedule or [0.0] * 12
self.label = label
# Pad vesting schedule to 12 months if shorter
while len(self.vesting_schedule) < 12:
self.vesting_schedule.append(0.0)
class MonthlyProjection:
"""Single month supply projection result."""
def __init__(
self,
month: int,
circulating: float,
total_supply: float,
emissions: float,
vesting_unlock: float,
burns: float,
net_new: float,
token_price: float,
) -> None:
self.month = month
self.circulating = circulating
self.total_supply = total_supply
self.emissions = emissions
self.vesting_unlock = vesting_unlock
self.burns = burns
self.net_new = net_new
self.token_price = token_price
@property
def circulating_pct(self) -> float:
"""Percentage of total supply in circulation."""
return (self.circulating / self.total_supply * 100) if self.total_supply > 0 else 0
@property
def monthly_inflation_pct(self) -> float:
"""Monthly inflation rate as percentage."""
prev = self.circulating - self.net_new
return (self.net_new / prev * 100) if prev > 0 else 0
@property
def market_cap(self) -> float:
"""Estimated market cap."""
return self.circulating * self.token_price
@property
def fdv(self) -> float:
"""Fully diluted valuation."""
return self.total_supply * self.token_price
@property
def fdv_mcap_ratio(self) -> float:
"""FDV to market cap ratio."""
return self.fdv / self.market_cap if self.market_cap > 0 else 0
# ── Projection Engine ──────────────────────────────────────────────
def project_supply(params: SupplyParams, months: int = 12) -> list[MonthlyProjection]:
"""Project token supply over specified months.
Args:
params: Supply parameters for the projection.
months: Number of months to project (default 12).
Returns:
List of MonthlyProjection objects for each month.
"""
projections: list[MonthlyProjection] = []
circ = params.circulating_supply
for m in range(months):
vest = params.vesting_schedule[m] if m < len(params.vesting_schedule) else 0.0
emissions = params.monthly_emissions
burns = params.monthly_burns
net_new = emissions + vest - burns
# Cannot exceed total supply
if circ + net_new > params.total_supply:
net_new = params.total_supply - circ
circ += net_new
projections.append(
MonthlyProjection(
month=m + 1,
circulating=circ,
total_supply=params.total_supply,
emissions=emissions,
vesting_unlock=vest,
burns=burns,
net_new=net_new,
token_price=params.token_price,
)
)
return projections
def find_milestones(
projections: list[MonthlyProjection],
milestones: list[float] = [50.0, 75.0, 90.0, 100.0],
) -> list[dict]:
"""Find when circulating supply reaches key percentage milestones.
Args:
projections: List of monthly projections.
milestones: Percentage milestones to track.
Returns:
List of milestone events with month and details.
"""
results: list[dict] = []
remaining = list(milestones)
# Check starting point
if projections:
start_pct = (
(projections[0].circulating - projections[0].net_new)
/ projections[0].total_supply
* 100
)
# Remove milestones already passed
remaining = [m for m in remaining if m > start_pct]
for proj in projections:
hit = [m for m in remaining if proj.circulating_pct >= m]
for milestone in hit:
results.append({
"milestone_pct": milestone,
"month": proj.month,
"circulating": proj.circulating,
"circulating_pct": proj.circulating_pct,
})
remaining.remove(milestone)
# Report milestones not reached
for m in remaining:
results.append({
"milestone_pct": m,
"month": None,
"circulating": None,
"circulating_pct": None,
})
return results
def compare_scenarios(
base: list[MonthlyProjection],
alt: list[MonthlyProjection],
) -> list[dict]:
"""Compare two projection scenarios month by month.
Args:
base: Base case projections.
alt: Alternative scenario projections.
Returns:
Monthly comparison with differences.
"""
comparisons: list[dict] = []
for b, a in zip(base, alt):
comparisons.append({
"month": b.month,
"base_circulating_pct": round(b.circulating_pct, 2),
"alt_circulating_pct": round(a.circulating_pct, 2),
"diff_pct": round(a.circulating_pct - b.circulating_pct, 2),
"base_inflation_pct": round(b.monthly_inflation_pct, 3),
"alt_inflation_pct": round(a.monthly_inflation_pct, 3),
})
return comparisons
# ── Display ─────────────────────────────────────────────────────────
def format_num(n: float) -> str:
"""Format number with K/M/B suffix.
Args:
n: Number to format.
Returns:
Formatted string.
"""
if abs(n) >= 1_000_000_000:
return f"{n / 1_000_000_000:.2f}B"
if abs(n) >= 1_000_000:
return f"{n / 1_000_000:.2f}M"
if abs(n) >= 1_000:
return f"{n / 1_000:.1f}K"
return f"{n:.0f}"
def print_projection_table(
params: SupplyParams,
projections: list[MonthlyProjection],
) -> None:
"""Print formatted projection table.
Args:
params: Supply parameters used.
projections: Projection results.
"""
print(f"\n{'=' * 80}")
print(f" SUPPLY PROJECTION: {params.name} — {params.label}")
print(f"{'=' * 80}")
print(f" Total Supply: {format_num(params.total_supply)}")
print(f" Starting Circ: {format_num(params.circulating_supply)}")
start_pct = params.circulating_supply / params.total_supply * 100
print(f" Starting Circ %: {start_pct:.1f}%")
print(f" Token Price: ${params.token_price:,.4f}")
print(f" Monthly Emissions: {format_num(params.monthly_emissions)}")
print(f" Monthly Burns: {format_num(params.monthly_burns)}")
total_vest = sum(params.vesting_schedule[:12])
if total_vest > 0:
print(f" Total Vesting (12mo): {format_num(total_vest)}")
# Table header
print(f"\n {'Mo':>3} | {'Circulating':>12} | {'Circ %':>7} | {'Net New':>10} "
f"| {'Mo.Infl%':>8} | {'FDV/MCap':>8} | {'Est MCap':>12}")
print(f" {'-' * 3}-+-{'-' * 12}-+-{'-' * 7}-+-{'-' * 10}-"
f"+-{'-' * 8}-+-{'-' * 8}-+-{'-' * 12}")
for p in projections:
mcap_str = f"${p.market_cap / 1e9:.2f}B" if p.market_cap >= 1e9 else f"${p.market_cap / 1e6:.1f}M"
print(
f" {p.month:>3} | {format_num(p.circulating):>12} | "
f"{p.circulating_pct:>6.1f}% | {format_num(p.net_new):>10} | "
f"{p.monthly_inflation_pct:>7.2f}% | {p.fdv_mcap_ratio:>7.2f}x | "
f"{mcap_str:>12}"
)
# Summary
first = projections[0]
last = projections[-1]
total_new = sum(p.net_new for p in projections)
total_inflation = (total_new / params.circulating_supply) * 100
print(f"\n 12-Month Summary:")
print(f" Net new tokens: {format_num(total_new)}")
print(f" Cumulative inflation: {total_inflation:.1f}%")
print(f" Circ % change: {start_pct:.1f}% -> {last.circulating_pct:.1f}%")
print(f" FDV/MCap change: {params.total_supply / params.circulating_supply:.2f}x -> {last.fdv_mcap_ratio:.2f}x")
def print_milestones(milestones: list[dict]) -> None:
"""Print milestone achievement summary.
Args:
milestones: List of milestone results.
"""
print(f"\n Supply Milestones:")
for m in milestones:
if m["month"] is not None:
print(f" {m['milestone_pct']:.0f}% circulating: reached in month {m['month']}")
else:
print(f" {m['milestone_pct']:.0f}% circulating: not reached within projection period")
def print_scenario_comparison(
base_params: SupplyParams,
alt_params: SupplyParams,
comparisons: list[dict],
) -> None:
"""Print side-by-side scenario comparison.
Args:
base_params: Base scenario parameters.
alt_params: Alternative scenario parameters.
comparisons: Monthly comparison data.
"""
print(f"\n{'=' * 70}")
print(f" SCENARIO COMPARISON: {base_params.label} vs {alt_params.label}")
print(f"{'=' * 70}")
print(f" Base burns/mo: {format_num(base_params.monthly_burns)}")
print(f" Alt burns/mo: {format_num(alt_params.monthly_burns)}")
print(f"\n {'Mo':>3} | {'Base Circ%':>10} | {'Alt Circ%':>10} | {'Diff':>7} "
f"| {'Base Infl%':>10} | {'Alt Infl%':>10}")
print(f" {'-' * 3}-+-{'-' * 10}-+-{'-' * 10}-+-{'-' * 7}-+-{'-' * 10}-+-{'-' * 10}")
for c in comparisons:
print(
f" {c['month']:>3} | {c['base_circulating_pct']:>9.1f}% | "
f"{c['alt_circulating_pct']:>9.1f}% | {c['diff_pct']:>+6.1f}% | "
f"{c['base_inflation_pct']:>9.2f}% | {c['alt_inflation_pct']:>9.2f}%"
)
# Final difference
last = comparisons[-1]
print(f"\n After 12 months:")
print(f" Base scenario: {last['base_circulating_pct']:.1f}% circulating")
print(f" Alt scenario: {last['alt_circulating_pct']:.1f}% circulating")
print(f" Difference: {last['diff_pct']:+.1f} percentage points")
# ── Demo Scenarios ──────────────────────────────────────────────────
def get_demo_params() -> SupplyParams:
"""Create demo parameters resembling a typical DeFi protocol token.
Returns:
SupplyParams with realistic example values.
"""
# Vesting schedule: team cliff at month 6, then linear
vesting = [
0, 0, 0, 0, 0, # months 1-5: no vesting
25_000_000, # month 6: cliff unlock (25M)
5_000_000, # months 7-12: linear vesting
5_000_000,
5_000_000,
5_000_000,
5_000_000,
5_000_000,
]
return SupplyParams(
name="ExampleProtocol (EXMP)",
total_supply=1_000_000_000, # 1B total
circulating_supply=300_000_000, # 300M circulating (30%)
token_price=2.50,
monthly_emissions=8_000_000, # 8M/mo from staking + LM
monthly_burns=2_000_000, # 2M/mo from fee burns
vesting_schedule=vesting,
label="Base Case (with burns)",
)
def get_demo_no_burns() -> SupplyParams:
"""Create demo params without burn mechanism for comparison.
Returns:
SupplyParams identical to demo but with zero burns.
"""
params = get_demo_params()
params.monthly_burns = 0
params.label = "No Burns"
return params
# ── Interactive Mode ────────────────────────────────────────────────
def get_interactive_params() -> SupplyParams:
"""Prompt user for supply parameters interactively.
Returns:
SupplyParams from user input.
"""
print("\n Enter token supply parameters:")
print(" (Press Enter for defaults shown in brackets)\n")
def prompt_float(label: str, default: float) -> float:
val = input(f" {label} [{default:,.0f}]: ").strip()
if not val:
return default
try:
return float(val.replace(",", "").replace("_", ""))
except ValueError:
print(f" Invalid number, using default: {default:,.0f}")
return default
name = input(" Token name [MyToken]: ").strip() or "MyToken"
total = prompt_float("Total supply", 1_000_000_000)
circ = prompt_float("Circulating supply", total * 0.3)
price = prompt_float("Token price (USD)", 1.0)
emissions = prompt_float("Monthly emissions", total * 0.008)
burns = prompt_float("Monthly burns", total * 0.002)
vest_input = input(" Include vesting schedule? (y/n) [n]: ").strip().lower()
vesting: list[float] = [0.0] * 12
if vest_input == "y":
cliff_month = int(input(" Cliff month (1-12) [6]: ").strip() or "6")
cliff_amount = prompt_float("Cliff unlock amount", total * 0.05)
monthly_vest = prompt_float("Monthly vesting after cliff", total * 0.005)
for m in range(12):
if m + 1 == cliff_month:
vesting[m] = cliff_amount
elif m + 1 > cliff_month:
vesting[m] = monthly_vest
return SupplyParams(
name=name,
total_supply=total,
circulating_supply=circ,
token_price=price,
monthly_emissions=emissions,
monthly_burns=burns,
vesting_schedule=vesting,
label="Custom Scenario",
)
# ── Main ────────────────────────────────────────────────────────────
def main() -> None:
"""Entry point for the supply modeler."""
parser = argparse.ArgumentParser(
description="Model token supply dynamics over 12 months"
)
parser.add_argument(
"--demo",
action="store_true",
default=False,
help="Run with demo parameters (default if no other option specified)",
)
parser.add_argument(
"--interactive",
action="store_true",
default=False,
help="Interactively input supply parameters",
)
parser.add_argument(
"--no-compare",
action="store_true",
default=False,
help="Skip burn vs no-burn comparison",
)
args = parser.parse_args()
if args.interactive:
params = get_interactive_params()
else:
if not args.demo:
print("No parameters specified, running demo mode.")
print("Use --interactive for custom parameters.\n")
params = get_demo_params()
# Run base projection
projections = project_supply(params)
print_projection_table(params, projections)
# Milestones
milestones = find_milestones(projections)
print_milestones(milestones)
# Scenario comparison (burn vs no-burn)
if not args.no_compare:
if args.interactive:
no_burn = SupplyParams(
name=params.name,
total_supply=params.total_supply,
circulating_supply=params.circulating_supply,
token_price=params.token_price,
monthly_emissions=params.monthly_emissions,
monthly_burns=0,
vesting_schedule=list(params.vesting_schedule),
label="No Burns",
)
else:
no_burn = get_demo_no_burns()
alt_projections = project_supply(no_burn)
comparisons = compare_scenarios(projections, alt_projections)
print_scenario_comparison(params, no_burn, comparisons)
print(f"\n{'=' * 70}")
print(" NOTE: Projections assume constant price and emission rates.")
print(" Actual supply dynamics depend on governance, market conditions,")
print(" and protocol changes. This is informational, not financial advice.")
print(f"{'=' * 70}\n")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Tokenomics Analyzer — Fetch and analyze token supply metrics.
Fetches token data from CoinGecko's free API and calculates key tokenomics
metrics including dilution risk, supply dynamics, and basic valuation ratios.
Usage:
python scripts/tokenomics_analyzer.py # uses --demo mode
python scripts/tokenomics_analyzer.py --token solana
python scripts/tokenomics_analyzer.py --token uniswap
TOKEN_ID=raydium python scripts/tokenomics_analyzer.py
Dependencies:
uv pip install httpx
Environment Variables:
TOKEN_ID: CoinGecko token ID (optional, can use --token flag instead)
"""
import argparse
import sys
import time
from typing import Optional
try:
import httpx
except ImportError:
print("Missing dependency. Install with: uv pip install httpx")
sys.exit(1)
# ── Configuration ───────────────────────────────────────────────────
COINGECKO_BASE = "https://api.coingecko.com/api/v3"
REQUEST_TIMEOUT = 30
RATE_LIMIT_WAIT = 65 # CoinGecko free tier: wait if rate-limited
# ── Data Models ─────────────────────────────────────────────────────
class TokenData:
"""Container for token data fetched from CoinGecko."""
def __init__(self, raw: dict) -> None:
self.name: str = raw.get("name", "Unknown")
self.symbol: str = raw.get("symbol", "???").upper()
self.coingecko_id: str = raw.get("id", "")
md = raw.get("market_data", {})
self.price: float = md.get("current_price", {}).get("usd", 0.0)
self.market_cap: float = md.get("market_cap", {}).get("usd", 0.0)
self.fdv: float = md.get("fully_diluted_valuation", {}).get("usd", 0.0)
self.total_volume_24h: float = md.get("total_volume", {}).get("usd", 0.0)
self.circulating_supply: float = md.get("circulating_supply") or 0.0
self.total_supply: float = md.get("total_supply") or 0.0
self.max_supply: Optional[float] = md.get("max_supply")
self.price_change_24h: float = md.get("price_change_percentage_24h") or 0.0
self.price_change_7d: float = md.get("price_change_percentage_7d") or 0.0
self.price_change_30d: float = md.get("price_change_percentage_30d") or 0.0
self.ath: float = md.get("ath", {}).get("usd", 0.0)
self.ath_change_pct: float = md.get("ath_change_percentage", {}).get("usd", 0.0)
class TokenomicsReport:
"""Calculated tokenomics metrics and risk assessment."""
def __init__(self, data: TokenData) -> None:
self.data = data
self.metrics = self._calculate_metrics()
self.risks = self._assess_risks()
self.valuation = self._basic_valuation()
def _calculate_metrics(self) -> dict:
"""Calculate core tokenomics metrics."""
d = self.data
circulating_pct = (
(d.circulating_supply / d.total_supply * 100) if d.total_supply > 0 else 0.0
)
fdv_mcap_ratio = d.fdv / d.market_cap if d.market_cap > 0 else 0.0
locked_supply = d.total_supply - d.circulating_supply
locked_value = locked_supply * d.price if d.price > 0 else 0.0
return {
"circulating_pct": round(circulating_pct, 2),
"fdv_mcap_ratio": round(fdv_mcap_ratio, 2),
"locked_supply": locked_supply,
"locked_value_usd": locked_value,
"has_max_supply": d.max_supply is not None,
"max_supply_pct": (
round(d.circulating_supply / d.max_supply * 100, 2)
if d.max_supply and d.max_supply > 0
else None
),
}
def _assess_risks(self) -> list[dict]:
"""Identify tokenomics risk factors."""
risks = []
m = self.metrics
d = self.data
# Dilution risk
if m["fdv_mcap_ratio"] > 5.0:
risks.append({
"category": "Dilution",
"severity": "HIGH",
"detail": (
f"FDV/MCap ratio is {m['fdv_mcap_ratio']:.1f}x — "
f"significant future dilution ahead"
),
})
elif m["fdv_mcap_ratio"] > 3.0:
risks.append({
"category": "Dilution",
"severity": "MEDIUM",
"detail": (
f"FDV/MCap ratio is {m['fdv_mcap_ratio']:.1f}x — "
f"moderate dilution risk"
),
})
elif m["fdv_mcap_ratio"] > 1.5:
risks.append({
"category": "Dilution",
"severity": "LOW",
"detail": (
f"FDV/MCap ratio is {m['fdv_mcap_ratio']:.1f}x — "
f"some dilution expected but manageable"
),
})
# Low circulating supply
if m["circulating_pct"] < 20:
risks.append({
"category": "Supply",
"severity": "HIGH",
"detail": (
f"Only {m['circulating_pct']:.1f}% of supply circulating — "
f"extreme dilution risk as tokens unlock"
),
})
elif m["circulating_pct"] < 50:
risks.append({
"category": "Supply",
"severity": "MEDIUM",
"detail": (
f"{m['circulating_pct']:.1f}% of supply circulating — "
f"majority of tokens still locked"
),
})
# Low volume relative to market cap
if d.market_cap > 0 and d.total_volume_24h > 0:
volume_mcap_ratio = d.total_volume_24h / d.market_cap
if volume_mcap_ratio < 0.01:
risks.append({
"category": "Liquidity",
"severity": "MEDIUM",
"detail": (
f"24h volume is only {volume_mcap_ratio:.2%} of market cap — "
f"low liquidity relative to valuation"
),
})
# Distance from ATH
if d.ath_change_pct < -90:
risks.append({
"category": "Price",
"severity": "INFO",
"detail": f"Token is {d.ath_change_pct:.1f}% from ATH — deeply discounted or declining",
})
# No max supply
if not m["has_max_supply"]:
risks.append({
"category": "Inflation",
"severity": "INFO",
"detail": "No max supply cap — potential for unlimited inflation",
})
return risks
def _basic_valuation(self) -> dict:
"""Calculate basic valuation metrics."""
d = self.data
metrics: dict = {}
# FDV per volume (lower = more activity per dollar of valuation)
if d.total_volume_24h > 0:
fdv_volume = d.fdv / (d.total_volume_24h * 365)
metrics["fdv_annualized_volume"] = round(fdv_volume, 2)
if fdv_volume < 1:
metrics["fdv_volume_assessment"] = "High activity relative to FDV"
elif fdv_volume < 10:
metrics["fdv_volume_assessment"] = "Moderate activity"
else:
metrics["fdv_volume_assessment"] = "Low activity relative to FDV"
# Market cap per volume (daily turnover)
if d.market_cap > 0 and d.total_volume_24h > 0:
daily_turnover = d.total_volume_24h / d.market_cap * 100
metrics["daily_turnover_pct"] = round(daily_turnover, 2)
return metrics
# ── API Functions ───────────────────────────────────────────────────
def fetch_token_data(token_id: str) -> TokenData:
"""Fetch token data from CoinGecko free API.
Args:
token_id: CoinGecko token identifier (e.g., 'solana', 'uniswap').
Returns:
TokenData object with parsed market data.
Raises:
httpx.HTTPStatusError: On non-2xx response after retries.
ValueError: If token not found.
"""
url = f"{COINGECKO_BASE}/coins/{token_id}"
params = {
"localization": "false",
"tickers": "false",
"market_data": "true",
"community_data": "false",
"developer_data": "false",
"sparkline": "false",
}
for attempt in range(3):
try:
resp = httpx.get(url, params=params, timeout=REQUEST_TIMEOUT)
if resp.status_code == 429:
print(f" Rate limited, waiting {RATE_LIMIT_WAIT}s...")
time.sleep(RATE_LIMIT_WAIT)
continue
if resp.status_code == 404:
raise ValueError(
f"Token '{token_id}' not found on CoinGecko. "
f"Check the ID at https://www.coingecko.com/en/coins/{token_id}"
)
resp.raise_for_status()
return TokenData(resp.json())
except httpx.ConnectError:
if attempt < 2:
print(f" Connection failed, retrying ({attempt + 1}/3)...")
time.sleep(2)
else:
raise
raise RuntimeError("Failed to fetch data after 3 attempts")
def search_token(query: str) -> Optional[str]:
"""Search for a token on CoinGecko and return its ID.
Args:
query: Search query (token name or symbol).
Returns:
CoinGecko token ID if found, None otherwise.
"""
url = f"{COINGECKO_BASE}/search"
try:
resp = httpx.get(url, params={"query": query}, timeout=REQUEST_TIMEOUT)
resp.raise_for_status()
coins = resp.json().get("coins", [])
if coins:
return coins[0]["id"]
return None
except httpx.HTTPError:
return None
# ── Demo Data ───────────────────────────────────────────────────────
def get_demo_data() -> TokenData:
"""Return demo data for illustration (based on a well-known L1 token).
Returns:
TokenData populated with example values.
"""
demo_raw = {
"id": "demo-token",
"name": "DemoChain",
"symbol": "DEMO",
"market_data": {
"current_price": {"usd": 150.0},
"market_cap": {"usd": 65_000_000_000},
"fully_diluted_valuation": {"usd": 89_000_000_000},
"total_volume": {"usd": 2_500_000_000},
"circulating_supply": 433_000_000,
"total_supply": 590_000_000,
"max_supply": None,
"price_change_percentage_24h": -2.5,
"price_change_percentage_7d": 5.1,
"price_change_percentage_30d": -8.3,
"ath": {"usd": 260.0},
"ath_change_percentage": {"usd": -42.3},
},
}
return TokenData(demo_raw)
# ── Display ─────────────────────────────────────────────────────────
def format_number(n: float, decimals: int = 2) -> str:
"""Format large numbers with K/M/B suffixes.
Args:
n: Number to format.
decimals: Decimal places.
Returns:
Formatted string.
"""
if abs(n) >= 1_000_000_000:
return f"${n / 1_000_000_000:.{decimals}f}B"
if abs(n) >= 1_000_000:
return f"${n / 1_000_000:.{decimals}f}M"
if abs(n) >= 1_000:
return f"${n / 1_000:.{decimals}f}K"
return f"${n:.{decimals}f}"
def format_supply(n: float) -> str:
"""Format supply numbers with M/B suffixes.
Args:
n: Supply number.
Returns:
Formatted string without dollar sign.
"""
if abs(n) >= 1_000_000_000:
return f"{n / 1_000_000_000:.2f}B"
if abs(n) >= 1_000_000:
return f"{n / 1_000_000:.2f}M"
if abs(n) >= 1_000:
return f"{n / 1_000:.2f}K"
return f"{n:.0f}"
def print_report(report: TokenomicsReport) -> None:
"""Print formatted tokenomics report.
Args:
report: TokenomicsReport to display.
"""
d = report.data
m = report.metrics
print("\n" + "=" * 65)
print(f" TOKENOMICS REPORT: {d.name} ({d.symbol})")
print("=" * 65)
# Price overview
print(f"\n Price: ${d.price:,.4f}")
print(f" Market Cap: {format_number(d.market_cap)}")
print(f" FDV: {format_number(d.fdv)}")
print(f" 24h Volume: {format_number(d.total_volume_24h)}")
# Price changes
print(f"\n Price Change:")
print(f" 24h: {d.price_change_24h:+.1f}%")
print(f" 7d: {d.price_change_7d:+.1f}%")
print(f" 30d: {d.price_change_30d:+.1f}%")
print(f" ATH: {d.ath_change_pct:+.1f}% (ATH: ${d.ath:,.2f})")
# Supply analysis
print(f"\n Supply:")
print(f" Circulating: {format_supply(d.circulating_supply)}")
print(f" Total: {format_supply(d.total_supply)}")
if d.max_supply:
print(f" Max: {format_supply(d.max_supply)}")
else:
print(f" Max: No cap")
print(f" Circulating %: {m['circulating_pct']:.1f}%")
print(f" Locked Supply: {format_supply(m['locked_supply'])}")
print(f" Locked Value: {format_number(m['locked_value_usd'])}")
# Dilution metrics
print(f"\n Dilution:")
print(f" FDV/MCap Ratio: {m['fdv_mcap_ratio']:.2f}x")
if m["max_supply_pct"] is not None:
print(f" % of Max Minted: {m['max_supply_pct']:.1f}%")
# Valuation
if report.valuation:
print(f"\n Valuation:")
if "fdv_annualized_volume" in report.valuation:
print(f" FDV/Ann.Volume: {report.valuation['fdv_annualized_volume']:.2f}x")
print(f" Assessment: {report.valuation.get('fdv_volume_assessment', 'N/A')}")
if "daily_turnover_pct" in report.valuation:
print(f" Daily Turnover: {report.valuation['daily_turnover_pct']:.2f}%")
# Risk flags
if report.risks:
print(f"\n Risk Flags:")
for risk in report.risks:
icon = {"HIGH": "[!]", "MEDIUM": "[*]", "LOW": "[-]", "INFO": "[i]"}.get(
risk["severity"], "[?]"
)
print(f" {icon} {risk['severity']:6s} | {risk['category']:10s} | {risk['detail']}")
else:
print(f"\n Risk Flags: None identified")
print("\n" + "=" * 65)
print(" NOTE: This is informational analysis, not financial advice.")
print("=" * 65 + "\n")
# ── Main ────────────────────────────────────────────────────────────
def main() -> None:
"""Entry point for the tokenomics analyzer."""
parser = argparse.ArgumentParser(
description="Analyze token supply dynamics and valuation metrics"
)
parser.add_argument(
"--token", "-t",
type=str,
default=None,
help="CoinGecko token ID (e.g., 'solana', 'uniswap', 'raydium')",
)
parser.add_argument(
"--demo",
action="store_true",
help="Run with demo data (no API call needed)",
)
parser.add_argument(
"--search", "-s",
type=str,
default=None,
help="Search for a token by name or symbol",
)
args = parser.parse_args()
import os
token_id = args.token or os.getenv("TOKEN_ID", "")
if args.demo or (not token_id and not args.search):
print("Running in demo mode (use --token <id> for live data)")
data = get_demo_data()
elif args.search:
print(f"Searching for '{args.search}'...")
found_id = search_token(args.search)
if not found_id:
print(f"No token found for '{args.search}'")
sys.exit(1)
print(f"Found: {found_id}")
print(f"Fetching data for '{found_id}'...")
data = fetch_token_data(found_id)
else:
print(f"Fetching data for '{token_id}'...")
try:
data = fetch_token_data(token_id)
except ValueError as e:
print(f"Error: {e}")
sys.exit(1)
except httpx.HTTPError as e:
print(f"HTTP error: {e}")
sys.exit(1)
report = TokenomicsReport(data)
print_report(report)
if __name__ == "__main__":
main()
Related skills
FAQ
What does the FDV/MCap ratio tell you?
It measures future dilution risk: 1.0-1.5 is low (most supply circulating), 1.5-3.0 moderate, 3.0-5.0 high, and above 5.0 very high, meaning the token faces massive dilution.
How is unlock impact estimated?
By dividing the unlock amount by average daily volume; under 1x is minor, 1-5x moderate (2-5% drawdown), 5-10x major (5-15%), and over 10x severe (10-30% drawdown).