
Liquidity Analysis
- 197 installs
- 257 repo stars
- Updated June 24, 2026
- agiprolabs/claude-trading-skills
liquidity-analysis is a Claude Code skill that assesses DEX liquidity depth, slippage, and pool composition for Solana tokens.
About
liquidity-analysis assesses DEX liquidity depth, estimates slippage, and analyzes pool composition for Solana tokens. A developer uses it before a trade to check whether they can enter and exit at a reasonable price, size a position against pool depth, and detect rug risk. It compares DexScreener, Jupiter, Birdeye, and on-chain data sources.
- Assesses DEX liquidity depth, slippage, and pool composition for Solana tokens
- Ships analyze_liquidity.py and pool_comparison.py plus data-source and slippage-curve references
- Flags rug risk from thin liquidity, single pools, and unlocked LP tokens
Liquidity Analysis by the numbers
- 197 all-time installs (skills.sh)
- Ranked #99 of 479 Web3 & Blockchain skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
liquidity-analysis capabilities & compatibility
- Capabilities
- liquidity analysis · slippage modeling · risk management
- Use cases
- data analysis · research · trading
What liquidity-analysis says it does
Liquidity analysis answers three critical questions before every trade: **Can I get in at a reasonable price?** **Can I get out when I need to?** and **Is this pool safe?**
keep trade size under 2% of pool depth to limit slippage below 1%.
npx skills add https://github.com/agiprolabs/claude-trading-skills --skill liquidity-analysisAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 197 |
|---|---|
| repo stars | ★ 257 |
| Last updated | June 24, 2026 |
| Repository | agiprolabs/claude-trading-skills ↗ |
What it does
Assess DEX liquidity depth, slippage, and rug risk before trading a Solana token.
Who is it for?
Checking depth, slippage, and pool safety before entering or exiting a Solana token.
Skip if: Executing the trade or computing impermanent loss (use impermanent-loss).
When should I use this skill?
You need to gauge liquidity depth, slippage, or rug risk before trading a Solana token.
By the numbers
- 4 data sources compared
- rule of thumb: trade under 2% of pool depth
Files
Liquidity Analysis — DEX Depth Assessment for Solana Tokens
Liquidity analysis answers three critical questions before every trade: Can I get in at a reasonable price? Can I get out when I need to? and Is this pool safe? Without it, you risk excessive slippage, failed exits, and rug pulls.
Why Liquidity Analysis Matters
Position sizing: Maximum position size is bounded by available liquidity. A $10K position in a pool with $20K TVL will move the price significantly. Rule of thumb: keep trade size under 2% of pool depth to limit slippage below 1%.
Execution cost: Slippage is a direct cost. On a 5 SOL buy, the difference between 0.3% and 3% slippage is real money lost on every entry and exit.
Rug risk detection: Thin liquidity, single pools, unlocked LP tokens, and newly created pools are warning signs. Liquidity analysis catches these before you enter.
Exit planning: Entry liquidity may differ from exit liquidity. If LP is unlocked and owned by one wallet, it can be pulled at any time.
Key Concepts
Total Value Locked (TVL)
Total value of assets deposited in a pool. For a SOL/TOKEN pool with 100 SOL and 1M TOKEN at $0.01 each, TVL = 100 SOL_price + 1M $0.01. TVL alone is insufficient — you need depth at the current price range.
Liquidity Depth
How much can be traded before moving the price X%. In constant-product AMMs, depth is uniform. In concentrated liquidity (CLMM), depth varies by price range — thick near the current price, thin or zero outside active ranges.
Concentration Factor (CLMM)
Concentrated liquidity pools focus capital in a narrow price range, providing deeper liquidity within that range but nothing outside it. A pool with $50K TVL concentrated in a +/-5% range provides the same depth as a $500K constant-product pool within that range, but zero depth beyond it.
Slippage Curve
Slippage is not linear. Plotting slippage against trade size produces a curve that's gentle for small trades and steep for large ones. The shape depends on pool type, TVL, and concentration.
Pool Composition
Who provides liquidity matters. Locked LP tokens cannot be withdrawn (safer). Single-sided liquidity means the pool is imbalanced. Pool age indicates stability — pools older than 7 days with consistent TVL are more reliable.
Data Sources
Four complementary data sources, from free to comprehensive:
| Source | Auth Required | Best For | Limitations |
|---|---|---|---|
| DexScreener | None | Quick pool lookup, liquidity.usd | No on-chain pool details |
| Jupiter Quote API | None | Empirical slippage at any size | Aggregate across pools |
| Birdeye | API key | Detailed pool data, trade history | Rate limited on free tier |
| On-chain | RPC only | LP lock status, exact reserves | Requires program knowledge |
See references/data_sources.md for complete endpoint documentation and usage examples.
Core Analysis Pipeline
Step 1: Identify Pools
Fetch all pools for a token. Most Solana tokens have multiple pools across Raydium, Orca, and Meteora.
import httpx
def get_pools(mint: str) -> list[dict]:
"""Fetch all DEX pools for a token from DexScreener."""
resp = httpx.get(f"https://api.dexscreener.com/tokens/v1/solana/{mint}")
resp.raise_for_status()
pairs = resp.json()
return [p for p in pairs if p.get("liquidity", {}).get("usd", 0) > 0]Step 2: Measure Depth
For each pool, extract liquidity metrics:
def extract_depth(pool: dict) -> dict:
"""Extract liquidity metrics from a DexScreener pool."""
return {
"dex": pool.get("dexId", "unknown"),
"liquidity_usd": pool.get("liquidity", {}).get("usd", 0),
"volume_24h": pool.get("volume", {}).get("h24", 0),
"pool_age_hours": _pool_age_hours(pool.get("pairCreatedAt", 0)),
"pair_address": pool.get("pairAddress", ""),
}Step 3: Estimate Slippage
Use Jupiter quotes at multiple sizes to build an empirical slippage curve. This captures real routing across all pools:
import httpx
SOL_MINT = "So11111111111111111111111111111111111111112"
LAMPORTS = 1_000_000_000
async def estimate_slippage(token_mint: str, sol_amounts: list[float]) -> list[dict]:
"""Query Jupiter for slippage at multiple trade sizes.
Args:
token_mint: Token mint address to buy.
sol_amounts: List of SOL amounts to test (e.g., [0.1, 0.5, 1, 5, 10]).
Returns:
List of dicts with sol_amount, output_tokens, price_per_token, slippage_bps.
"""
results = []
base_price = None
async with httpx.AsyncClient() as client:
for sol in sol_amounts:
lamports = int(sol * LAMPORTS)
resp = await client.get(
"https://api.jup.ag/quote/v1",
params={
"inputMint": SOL_MINT,
"outputMint": token_mint,
"amount": str(lamports),
"slippageBps": 5000,
},
)
if resp.status_code != 200:
continue
data = resp.json()
out_amount = int(data["outAmount"])
price = sol / out_amount if out_amount > 0 else 0
if base_price is None:
base_price = price
slippage_bps = int((price - base_price) / base_price * 10000) if base_price > 0 else 0
results.append({
"sol_amount": sol,
"output_tokens": out_amount,
"price_per_token": price,
"slippage_bps": max(0, slippage_bps),
})
return resultsStep 4: Assess Concentration
For CLMM pools (Orca Whirlpool, Raydium CLMM, Meteora DLMM), liquidity may be concentrated in a narrow range. Check if the current price is within the active range and how deep liquidity extends:
def assess_concentration(pools: list[dict]) -> dict:
"""Assess concentration risk from pool data."""
clmm_pools = [p for p in pools if p.get("dexId") in ("raydium", "orca") and "clmm" in p.get("labels", [])]
cpmm_pools = [p for p in pools if p not in clmm_pools]
total_clmm = sum(p.get("liquidity", {}).get("usd", 0) for p in clmm_pools)
total_cpmm = sum(p.get("liquidity", {}).get("usd", 0) for p in cpmm_pools)
total = total_clmm + total_cpmm
return {
"clmm_ratio": total_clmm / total if total > 0 else 0,
"cpmm_liquidity": total_cpmm,
"clmm_liquidity": total_clmm,
"concentration_risk": "high" if total_clmm / total > 0.8 and total > 0 else "low",
}Step 5: Compute Liquidity Score
Composite score from 0 (dangerous) to 100 (deep, safe liquidity):
def compute_liquidity_score(
total_liquidity_usd: float,
pool_count: int,
largest_pool_pct: float,
oldest_pool_hours: float,
max_slippage_bps_at_1sol: int,
) -> int:
"""Compute composite liquidity score (0-100).
Components:
Depth (40%): log-scaled TVL from $1K (0) to $1M+ (40)
Diversity (15%): more pools = more resilient
Concentration (15%): penalty if one pool dominates
Age (15%): older pools are more reliable
Slippage (15%): lower slippage = better
"""
import math
# Depth: 0-40 points
depth = min(40, int(40 * math.log10(max(total_liquidity_usd, 1)) / 6))
# Diversity: 0-15 points
diversity = min(15, pool_count * 3)
# Concentration: 0-15 points (penalty for single-pool dominance)
concentration = int(15 * (1 - largest_pool_pct))
# Age: 0-15 points (7+ days = full marks)
age = min(15, int(15 * oldest_pool_hours / 168))
# Slippage: 0-15 points
slippage = max(0, 15 - max_slippage_bps_at_1sol // 10)
return max(0, min(100, depth + diversity + concentration + age + slippage))Risk Flags
Flag these conditions before entering any position:
| Flag | Condition | Risk Level |
|---|---|---|
| Single Pool | Only 1 DEX pool exists | High |
| Thin Liquidity | Total TVL < $10,000 | Critical |
| New Pool | Pool created < 2 hours ago | High |
| Unlocked LP | LP tokens not burned/locked | Medium |
| Volume Mismatch | Volume >> TVL (wash trading) | Medium |
| Price Deviation | >5% price difference across pools | High |
| Concentrated CLMM | >80% liquidity in CLMM with narrow range | Medium |
def detect_risk_flags(pools: list[dict]) -> list[str]:
"""Detect liquidity risk flags from pool data."""
flags = []
if len(pools) < 2:
flags.append("SINGLE_POOL: Only 1 pool exists — exit may be difficult")
total_liq = sum(p.get("liquidity", {}).get("usd", 0) for p in pools)
if total_liq < 10_000:
flags.append(f"THIN_LIQUIDITY: Total TVL ${total_liq:,.0f} < $10,000")
for p in pools:
age_ms = p.get("pairCreatedAt", 0)
if age_ms > 0:
import time
age_hours = (time.time() * 1000 - age_ms) / 3_600_000
if age_hours < 2:
flags.append(f"NEW_POOL: {p.get('dexId')} pool is {age_hours:.1f}h old")
volumes = [p.get("volume", {}).get("h24", 0) for p in pools]
liqs = [p.get("liquidity", {}).get("usd", 0) for p in pools]
for v, l, p in zip(volumes, liqs, pools):
if l > 0 and v / l > 10:
flags.append(f"VOLUME_MISMATCH: {p.get('dexId')} volume/TVL = {v/l:.1f}x")
prices = [float(p.get("priceUsd", 0)) for p in pools if float(p.get("priceUsd", 0)) > 0]
if len(prices) >= 2:
deviation = (max(prices) - min(prices)) / min(prices)
if deviation > 0.05:
flags.append(f"PRICE_DEVIATION: {deviation:.1%} across pools")
return flagsPosition Sizing from Liquidity
Maximum position size should keep slippage under your threshold:
| Trade Type | Max Slippage | Max Position % of TVL |
|---|---|---|
| Scalp | 0.5% (50 bps) | 1% |
| Swing | 2% (200 bps) | 2-5% |
| Position | 5% (500 bps) | 5-10% |
def max_position_from_liquidity(
total_liquidity_usd: float,
max_slippage_pct: float = 1.0,
trade_type: str = "swing",
) -> float:
"""Estimate maximum position size in USD based on liquidity.
Uses rule-of-thumb: max_position = tvl_fraction * total_liquidity.
For constant-product AMM, 1% of TVL produces ~2% slippage.
Args:
total_liquidity_usd: Total liquidity across all pools.
max_slippage_pct: Maximum acceptable slippage percentage.
trade_type: "scalp", "swing", or "position".
Returns:
Maximum position size in USD.
"""
fractions = {"scalp": 0.01, "swing": 0.03, "position": 0.07}
base_fraction = fractions.get(trade_type, 0.03)
adjusted = base_fraction * (max_slippage_pct / 2.0)
return total_liquidity_usd * adjustedSlippage Estimation
For detailed slippage mathematics including constant-product formulas, CLMM models, and empirical curve fitting, see references/slippage_curves.md.
Key formula for constant-product AMM:
slippage = Δx / (x + Δx)Where Δx is trade size and x is pool reserve of the input token. For a 1 SOL trade on a pool with 100 SOL reserve, slippage = 1/101 = 0.99%.
Pool Types
Solana DEXes use different AMM designs with different liquidity characteristics. See references/pool_types.md for comprehensive coverage including:
- Constant Product (Raydium V4, Orca Legacy): Uniform liquidity, predictable slippage
- Concentrated Liquidity (Raydium CLMM, Orca Whirlpool): Deep at current price, zero outside range
- Dynamic AMM (Meteora DLMM): Adaptive fees, bin-based liquidity
Integration with Other Skills
token-holder-analysis: Check LP token holder distribution before entering. If one wallet holds >50% of LP tokens and they are unlocked, exit risk is high.
position-sizing: Feed max_position_from_liquidity() output into position sizing models as an upper bound.
slippage-modeling: Use the empirical slippage curves from this skill as input to execution cost models.
birdeye-api: Fetch detailed pool data including trade history and LP events.
dexscreener-api: Free pool discovery and basic liquidity metrics.
Example Workflow
# Full liquidity assessment for a token
import httpx
TOKEN = "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263" # BONK
# 1. Get pools
resp = httpx.get(f"https://api.dexscreener.com/tokens/v1/solana/{TOKEN}")
pools = [p for p in resp.json() if p.get("liquidity", {}).get("usd", 0) > 0]
# 2. Analyze
total_liq = sum(p["liquidity"]["usd"] for p in pools)
largest = max(p["liquidity"]["usd"] for p in pools)
largest_pct = largest / total_liq if total_liq > 0 else 1.0
# 3. Risk flags
flags = detect_risk_flags(pools)
# 4. Score
score = compute_liquidity_score(total_liq, len(pools), largest_pct, 1000, 50)
# 5. Position sizing
max_pos = max_position_from_liquidity(total_liq, max_slippage_pct=1.0, trade_type="swing")
print(f"Total Liquidity: ${total_liq:,.0f}")
print(f"Pools: {len(pools)}")
print(f"Score: {score}/100")
print(f"Max Position (swing, 1% slip): ${max_pos:,.0f}")
for f in flags:
print(f" WARNING: {f}")Files
| File | Description |
|---|---|
references/slippage_curves.md | Slippage math for constant-product and CLMM pools, empirical curve fitting |
references/pool_types.md | AMM designs on Solana: constant product, concentrated, dynamic |
references/data_sources.md | API endpoints and on-chain methods for fetching liquidity data |
scripts/analyze_liquidity.py | Full liquidity assessment with scoring and risk flags |
scripts/pool_comparison.py | Compare pools across DEXes for a token |
Data Sources — APIs and On-Chain Methods for Liquidity Data
DexScreener (Free, No Auth)
Best for: Quick pool discovery, basic liquidity metrics, multi-chain support.
Get Pairs by Token
- Endpoint:
GET https://api.dexscreener.com/tokens/v1/solana/{tokenAddress} - Rate Limit: 60 requests/minute
- Auth: None required
Response fields relevant to liquidity:
| Field | Type | Description |
|---|---|---|
pairAddress | string | On-chain pool address |
dexId | string | DEX name (raydium, orca, meteora) |
liquidity.usd | number | Total pool liquidity in USD |
liquidity.base | number | Base token reserve |
liquidity.quote | number | Quote token reserve |
volume.h24 | number | 24h trading volume in USD |
priceUsd | string | Current price in USD |
pairCreatedAt | number | Pool creation timestamp (ms) |
labels | array | Tags like "v2", "v3" |
fdv | number | Fully diluted valuation |
Example:
import httpx
def get_dexscreener_pools(mint: str) -> list[dict]:
"""Fetch all pools for a token from DexScreener."""
resp = httpx.get(
f"https://api.dexscreener.com/tokens/v1/solana/{mint}",
timeout=10,
)
resp.raise_for_status()
return resp.json()Search Pairs
- Endpoint:
GET https://api.dexscreener.com/latest/dex/search?q={query} - Use case: Find pools by token name/symbol when you do not have the mint address
Get Pair by Address
- Endpoint:
GET https://api.dexscreener.com/latest/dex/pairs/solana/{pairAddress} - Use case: Get details for a specific pool
Jupiter Quote API (Free, No Auth)
Best for: Empirical slippage measurement, aggregated across all pools.
Get Quote
- Endpoint:
GET https://api.jup.ag/quote/v1 - Rate Limit: Generous but undocumented; use 10 req/s as safe limit
Parameters:
| Parameter | Required | Description |
|---|---|---|
inputMint | Yes | Input token mint address |
outputMint | Yes | Output token mint address |
amount | Yes | Input amount in smallest unit (lamports for SOL) |
slippageBps | No | Max slippage tolerance in basis points (default 50) |
onlyDirectRoutes | No | If true, no intermediate tokens |
Response fields relevant to liquidity:
| Field | Type | Description |
|---|---|---|
outAmount | string | Expected output in smallest unit |
priceImpactPct | string | Price impact as decimal string |
routePlan | array | Routing details (which pools, percentages) |
routePlan[].percent | number | Percentage routed through this pool |
routePlan[].swapInfo.ammKey | string | Pool address used |
Example — Building a slippage curve:
import httpx
SOL_MINT = "So11111111111111111111111111111111111111112"
LAMPORTS = 1_000_000_000
def get_jupiter_quote(token_mint: str, sol_amount: float) -> dict | None:
"""Get Jupiter quote for a specific trade size."""
resp = httpx.get(
"https://api.jup.ag/quote/v1",
params={
"inputMint": SOL_MINT,
"outputMint": token_mint,
"amount": str(int(sol_amount * LAMPORTS)),
"slippageBps": 5000,
},
timeout=10,
)
if resp.status_code != 200:
return None
return resp.json()Extracting routing info:
def parse_route_pools(quote: dict) -> list[dict]:
"""Extract pool routing details from Jupiter quote."""
pools = []
for step in quote.get("routePlan", []):
info = step.get("swapInfo", {})
pools.append({
"amm": info.get("label", "unknown"),
"pool": info.get("ammKey", ""),
"percent": step.get("percent", 0),
"in_amount": int(info.get("inAmount", 0)),
"out_amount": int(info.get("outAmount", 0)),
})
return poolsBirdeye API (API Key Required)
Best for: Detailed pool data, historical volume, trade counts.
Token Trade Data
- Endpoint:
GET https://public-api.birdeye.so/defi/v3/token/trade-data/single - Auth: Header
X-API-KEY: {key}, orx-chain: solana - Rate Limit: 100/min (free), 1000/min (paid)
Parameters: address (token mint)
Response includes: price, volume (24h, buy/sell split), trade count, liquidity, unique wallets.
Token Markets (Pool Listings)
- Endpoint:
GET https://public-api.birdeye.so/defi/v2/markets - Parameters:
address(token mint),sort_by,sort_type - Returns: List of pools with liquidity, volume, source (DEX name)
Example:
import httpx
import os
def get_birdeye_pools(mint: str) -> list[dict]:
"""Fetch pool listings from Birdeye."""
api_key = os.getenv("BIRDEYE_API_KEY", "")
if not api_key:
raise ValueError("Set BIRDEYE_API_KEY environment variable")
resp = httpx.get(
"https://public-api.birdeye.so/defi/v2/markets",
params={"address": mint},
headers={"X-API-KEY": api_key, "x-chain": "solana"},
timeout=10,
)
resp.raise_for_status()
return resp.json().get("data", {}).get("items", [])On-Chain Pool Data (RPC Only)
Best for: LP lock status, exact reserves, pool account verification.
Raydium V4 Pool State
Read the pool state account to get exact reserves:
import httpx
import base64
import struct
def get_raydium_reserves(pool_address: str, rpc_url: str) -> dict:
"""Fetch Raydium V4 pool reserves from on-chain data.
Note: This is simplified. Full implementation requires
reading the associated token vault accounts.
"""
resp = httpx.post(
rpc_url,
json={
"jsonrpc": "2.0",
"id": 1,
"method": "getAccountInfo",
"params": [pool_address, {"encoding": "base64"}],
},
timeout=10,
)
data = resp.json()
# Pool state account contains vault addresses at known offsets
# Read vault token balances for actual reserves
return dataLP Token Analysis
Check LP token distribution to assess rug risk:
1. Get the LP mint address from the pool state 2. Query getTokenLargestAccounts for the LP mint 3. Check if the largest holder is a known burn address or lock program
BURN_ADDRESSES = {
"1111111111111111111111111111111111", # System program (burned)
}
LOCK_PROGRAMS = {
"2r5VekMNiWPzi1pWwvJczrdPaZnJG59u91unSrTunwJg", # Raydium LP locker
}Source Selection Guide
| Use Case | Primary Source | Fallback |
|---|---|---|
| Pool discovery | DexScreener | Birdeye |
| Quick liquidity check | DexScreener | Jupiter quote |
| Slippage estimation | Jupiter Quote API | Manual calculation |
| Historical volume | Birdeye | DexScreener (24h only) |
| LP lock status | On-chain RPC | N/A |
| Exact reserves | On-chain RPC | DexScreener liquidity.base/quote |
| Multi-pool routing | Jupiter Quote API | N/A |
| Cross-chain comparison | DexScreener | CoinGecko |
Rate Limit Management
import time
from collections import deque
class RateLimiter:
"""Simple sliding-window rate limiter."""
def __init__(self, max_requests: int, window_seconds: float):
self.max_requests = max_requests
self.window = window_seconds
self.timestamps: deque[float] = deque()
def wait_if_needed(self) -> None:
now = time.time()
while self.timestamps and now - self.timestamps[0] > self.window:
self.timestamps.popleft()
if len(self.timestamps) >= self.max_requests:
sleep_time = self.window - (now - self.timestamps[0])
if sleep_time > 0:
time.sleep(sleep_time)
self.timestamps.append(time.time())Pool Types — AMM Designs on Solana
Constant Product (xy = k)
Implementations
- Raydium V4 (AMM): The most common pool type for new Solana tokens. PumpFun tokens graduate to Raydium V4 pools. Program ID:
675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 - Orca Legacy: Older constant-product pools being phased out in favor of Whirlpools.
Mechanics
Both reserves (SOL and TOKEN) maintain the invariant x * y = k. Liquidity is distributed uniformly across the entire price range from 0 to infinity.
Price = x / y (SOL per TOKEN)
After trade: x_new * y_new = k
Slippage = Δx / (x + Δx)Characteristics
| Property | Value |
|---|---|
| Capital efficiency | Low — liquidity spread across all prices |
| Slippage predictability | High — deterministic from reserves |
| Range exhaustion risk | None — always has liquidity |
| Impermanent loss | Standard IL curve |
| LP complexity | Low — deposit and forget |
Identifying On-Chain
Raydium V4 pools have a fixed account layout. Key accounts:
- Pool state: contains reserves, fees, LP mint
- Token vaults: two SPL token accounts holding reserves
- LP mint: SPL token minted to liquidity providers
# Raydium V4 program
RAYDIUM_AMM = "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8"Concentrated Liquidity (CLMM / Whirlpool)
Implementations
- Orca Whirlpool: Primary CLMM on Solana. Program ID:
whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc - Raydium CLMM: Raydium's concentrated liquidity implementation. Program ID:
CAMMCzo5YL8w4VFF8KVHr7UfsuqSrsNyZ5bfWQ5KSBhH
Mechanics
LPs choose a price range [p_low, p_high] to concentrate their liquidity. Within this range, the pool behaves like a constant-product pool with much higher virtual reserves.
Concentration factor = sqrt(p_high / p_low) / (sqrt(p_high / p_low) - 1)For a +/-5% range: concentration factor is approximately 10x. The LP provides 10x the effective depth per dollar of capital.
Tick System
Prices are discretized into ticks. Each tick represents a 0.01% price change (1 basis point). LPs deposit liquidity between two ticks.
tick = floor(log(price) / log(1.0001))
price = 1.0001^tickTicks are grouped into tick arrays. Liquidity can vary between tick groups, creating an uneven depth profile.
Characteristics
| Property | Value |
|---|---|
| Capital efficiency | High — 10-100x within range |
| Slippage predictability | Low — depends on tick distribution |
| Range exhaustion risk | High — zero liquidity outside range |
| Impermanent loss | Higher within range (amplified) |
| LP complexity | High — must manage range |
Impact on Traders
- Small trades: Much less slippage than constant-product at same TVL
- Large trades: Risk of exhausting the active range, causing sudden slippage spike
- Volatility: If price moves outside most LP ranges, liquidity can vanish quickly
Dynamic AMM (Meteora DLMM)
Implementation
- Meteora DLMM: Dynamic Liquidity Market Maker. Program ID:
LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo
Mechanics
Meteora uses a bin-based system. Price is divided into discrete bins, each representing a fixed price point. LPs deposit into specific bins.
bin_id = floor(log(price) / log(1 + bin_step/10000))Bin step sizes vary by pool (e.g., 1 bps, 5 bps, 25 bps, 100 bps). Smaller bin steps = more granular pricing but more bins to manage.
Dynamic Fees
Meteora adjusts fees based on volatility. High volatility = higher fees, compensating LPs for impermanent loss risk. Fee formula:
total_fee = base_fee + variable_fee
variable_fee = volatility_accumulator * bin_step^2Characteristics
| Property | Value |
|---|---|
| Capital efficiency | High — bin-based concentration |
| Slippage predictability | Medium — depends on bin distribution |
| Range exhaustion risk | Medium — bins can be empty |
| Fee structure | Dynamic — adjusts to volatility |
| LP complexity | Medium — choose bins |
PumpFun Token Lifecycle
Tokens launched on PumpFun follow a standard liquidity path:
1. Bonding curve phase: Token trades on PumpFun's internal bonding curve. No DEX pool yet. Liquidity is synthetic. 2. Graduation: At ~$69K market cap, PumpFun deploys a Raydium V4 pool with approximately 79 SOL + the remaining token supply. 3. Post-graduation: Additional pools may appear on Orca, Meteora, or other DEXes as market makers and LPs enter.
Implications for Liquidity Analysis
- Immediately after graduation, there is exactly 1 pool with ~79 SOL of liquidity
- Pool TVL will be approximately $15-20K at typical SOL prices
- LP tokens from PumpFun graduation are typically burned (locked)
- Additional pools are a positive signal — means market makers see opportunity
Comparison Table
| Feature | Raydium V4 | Orca Whirlpool | Raydium CLMM | Meteora DLMM |
|---|---|---|---|---|
| Model | xy = k | Concentrated | Concentrated | Bin-based |
| Capital efficiency | 1x | 10-100x | 10-100x | 10-50x |
| Min TVL for trading | High | Low | Low | Low |
| Slippage predictability | High | Low | Low | Medium |
| Fee model | Fixed | Fixed per pool | Fixed per pool | Dynamic |
| LP token | Fungible SPL | NFT position | NFT position | Fungible per bin |
| Best for | New tokens | Major pairs | Major pairs | Volatile pairs |
| Worst for | Large caps | Microcaps | Microcaps | Stable pairs |
Identifying Pool Type from DexScreener Data
DexScreener's dexId and labels fields help identify pool type:
def classify_pool(pair: dict) -> str:
"""Classify a DexScreener pair by pool type."""
dex = pair.get("dexId", "").lower()
labels = [l.lower() for l in pair.get("labels", [])]
if dex == "raydium":
if "clmm" in labels or "concentrated" in labels:
return "raydium_clmm"
return "raydium_v4"
elif dex == "orca":
return "orca_whirlpool" # Almost all Orca pools are Whirlpools now
elif dex == "meteora":
if "dlmm" in labels:
return "meteora_dlmm"
return "meteora_amm"
else:
return dexSlippage Curves — Mathematical Models and Empirical Estimation
Constant-Product AMM Slippage
In a constant-product AMM (xy = k), the price impact of a trade is deterministic.
Buy Side (SOL to TOKEN)
Given a pool with x SOL and y TOKEN reserves:
k = x * y (invariant)
x_new = x + Δx (add SOL)
y_new = k / x_new (new TOKEN reserve)
tokens_out = y - y_new (tokens received)
tokens_out = y * Δx / (x + Δx)Price impact (slippage):
effective_price = Δx / tokens_out = (x + Δx) / y
spot_price = x / y
slippage = effective_price / spot_price - 1
slippage = Δx / (x + Δx)Key insight: Slippage depends only on the ratio of trade size to pool reserve, not on the token price.
Sell Side (TOKEN to SOL)
Symmetrically:
slippage = Δy / (y + Δy)Where Δy is the number of tokens being sold and y is the token reserve.
Worked Example
Pool: 50 SOL / 5,000,000 TOKEN (spot price = 0.00001 SOL/TOKEN)
Buying 1 SOL worth of TOKEN:
Δx = 1 SOL, x = 50 SOL
slippage = 1 / (50 + 1) = 1.96%
tokens_out = 5,000,000 * 1 / (50 + 1) = 98,039 TOKEN
effective_price = 1 / 98,039 = 0.00001020 SOL/TOKENBuying 5 SOL worth:
slippage = 5 / (50 + 5) = 9.09%
tokens_out = 5,000,000 * 5 / (50 + 5) = 454,545 TOKENBuying 0.1 SOL worth:
slippage = 0.1 / (50 + 0.1) = 0.20%Trade Size to Slippage Table (50 SOL reserve)
| Trade Size (SOL) | Slippage | Tokens Received |
|---|---|---|
| 0.1 | 0.20% | 9,980 |
| 0.5 | 0.99% | 49,505 |
| 1.0 | 1.96% | 98,039 |
| 2.0 | 3.85% | 192,308 |
| 5.0 | 9.09% | 454,545 |
| 10.0 | 16.67% | 833,333 |
| 25.0 | 33.33% | 1,666,667 |
Concentrated Liquidity (CLMM) Slippage
CLMM pools (Orca Whirlpool, Raydium CLMM, Meteora DLMM) concentrate liquidity in a price range [p_low, p_high].
Effective Depth
Within the active range, a CLMM pool with TVL L concentrated in range [p_low, p_high] provides the same depth as a constant-product pool with TVL:
L_effective = L * p_spot / (sqrt(p_high) - sqrt(p_low))For a $50K CLMM pool concentrated in a +/-5% range around $0.01:
p_low = 0.0095, p_high = 0.0105
L_effective ≈ $50K * sqrt(0.01) / (sqrt(0.0105) - sqrt(0.0095))
L_effective ≈ $500K equivalent constant-product depthRange Exhaustion
If a trade moves the price outside the active range, remaining liquidity drops to zero. The trade fails or routes through other pools. This makes large trades on CLMM pools riskier if ranges are narrow.
Tick Crossing
CLMM pools have discrete price ticks. Each tick may have different liquidity. Slippage calculation requires summing across ticks:
total_slippage = Σ (slippage_at_tick_i * amount_at_tick_i) / total_amountThis is complex to compute off-chain. The empirical approach (querying Jupiter) is more practical.
Empirical Slippage Estimation
Method
Query Jupiter Quote API at multiple trade sizes and measure actual output:
import httpx
SOL_MINT = "So11111111111111111111111111111111111111112"
LAMPORTS = 1_000_000_000
SIZES = [0.1, 0.5, 1.0, 5.0, 10.0, 25.0]
async def build_slippage_curve(token_mint: str) -> list[dict]:
"""Build empirical slippage curve using Jupiter quotes."""
results = []
base_rate = None
async with httpx.AsyncClient(timeout=15) as client:
for sol in SIZES:
resp = await client.get(
"https://api.jup.ag/quote/v1",
params={
"inputMint": SOL_MINT,
"outputMint": token_mint,
"amount": str(int(sol * LAMPORTS)),
"slippageBps": 5000,
},
)
if resp.status_code != 200:
continue
data = resp.json()
out = int(data["outAmount"])
rate = out / sol if sol > 0 else 0
if base_rate is None:
base_rate = rate
slippage_bps = int((1 - rate / base_rate) * 10000) if base_rate > 0 else 0
results.append({"sol": sol, "tokens": out, "slippage_bps": max(0, slippage_bps)})
return resultsAdvantages Over Analytical Models
1. Multi-pool routing: Jupiter splits trades across pools. Empirical measurement captures this. 2. CLMM complexity: No need to fetch tick data and compute across ranges. 3. Real fees: Jupiter includes swap fees in the quote. 4. Current state: Reflects actual on-chain liquidity at query time.
Limitations
1. Point-in-time: Liquidity changes constantly. Curves are valid for minutes, not hours. 2. Jupiter-specific: Other aggregators may route differently. 3. Rate limits: Querying 6 sizes per token adds up. Batch wisely.
Multi-Pool Routing Impact
Jupiter splits large trades across multiple pools to minimize slippage. A token with 3 pools of $20K each will have lower slippage than a token with 1 pool of $60K for large trades, because Jupiter can parallelize.
Empirical observation: Multi-pool routing typically reduces slippage by 30-60% compared to single-pool execution for trades above 1% of the largest pool.
Slippage Thresholds by Trade Type
| Trade Type | Target Holding | Max Entry Slippage | Max Exit Slippage | Notes |
|---|---|---|---|---|
| Scalp | Minutes-hours | 50 bps (0.5%) | 50 bps | Must exit quickly; both sides matter |
| Swing | Hours-days | 200 bps (2%) | 200 bps | More forgiving on entry |
| Position | Days-weeks | 500 bps (5%) | 300 bps | Large position; exit matters more |
| Snipe | Seconds | 1000+ bps | N/A | Speed over price; high risk |
Curve Fitting for Prediction
For positions between measured sizes, fit a power curve:
slippage_bps = a * trade_size^bWhere a and b are fitted from empirical data. For constant-product pools, b ≈ 1.0. For multi-pool routing, b < 1.0 (sub-linear due to splitting).
import numpy as np
def fit_slippage_curve(sizes: list[float], slippages_bps: list[int]) -> tuple[float, float]:
"""Fit power curve to empirical slippage data.
Returns (a, b) where slippage_bps = a * size^b.
"""
log_sizes = np.log(sizes)
log_slips = np.log([max(s, 1) for s in slippages_bps])
b, log_a = np.polyfit(log_sizes, log_slips, 1)
return float(np.exp(log_a)), float(b)Inverting the Curve
Given a maximum slippage budget, find the maximum trade size:
max_size = (max_slippage_bps / a) ^ (1/b)This is the primary input to position sizing from liquidity.
#!/usr/bin/env python3
"""Analyze DEX liquidity depth for a Solana token.
Fetches pool data from DexScreener and Jupiter quotes to build a complete
liquidity profile: pool inventory, slippage curve, composite score, and
risk flags.
Usage:
python scripts/analyze_liquidity.py # Demo mode (SOL/USDC)
python scripts/analyze_liquidity.py <token_mint> # Analyze specific token
TOKEN_MINT=<address> python scripts/analyze_liquidity.py
Dependencies:
uv pip install httpx
Environment Variables:
TOKEN_MINT: Token mint address (optional, overridden by CLI arg)
"""
import math
import os
import sys
import time
from typing import Optional
import httpx
# ── Configuration ───────────────────────────────────────────────────
SOL_MINT = "So11111111111111111111111111111111111111112"
LAMPORTS = 1_000_000_000
SLIPPAGE_TEST_SIZES = [0.1, 0.5, 1.0, 5.0, 10.0, 25.0]
DEXSCREENER_BASE = "https://api.dexscreener.com"
JUPITER_QUOTE_URL = "https://api.jup.ag/quote/v1"
# Demo data for --demo mode or when API calls fail
DEMO_MINT = SOL_MINT # SOL itself for demo
DEMO_POOLS = [
{
"pairAddress": "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2",
"dexId": "raydium",
"baseToken": {"symbol": "SOL"},
"quoteToken": {"symbol": "USDC"},
"priceUsd": "145.50",
"liquidity": {"usd": 12_500_000, "base": 43_000, "quote": 6_250_000},
"volume": {"h24": 85_000_000},
"pairCreatedAt": int((time.time() - 365 * 86400) * 1000),
"labels": [],
},
{
"pairAddress": "7qbRF6YsyGuLUVs6Y1q64bdVrfe4ZcUUz1JRdoVNUJnm",
"dexId": "orca",
"baseToken": {"symbol": "SOL"},
"quoteToken": {"symbol": "USDC"},
"priceUsd": "145.48",
"liquidity": {"usd": 8_200_000, "base": 28_200, "quote": 4_100_000},
"volume": {"h24": 42_000_000},
"pairCreatedAt": int((time.time() - 300 * 86400) * 1000),
"labels": ["concentrated"],
},
{
"pairAddress": "FpCMFDFGYotvufJ7HrFHsWEiiQCGbkLCtwHiDnh7o28Q",
"dexId": "meteora",
"baseToken": {"symbol": "SOL"},
"quoteToken": {"symbol": "USDC"},
"priceUsd": "145.52",
"liquidity": {"usd": 3_100_000, "base": 10_700, "quote": 1_550_000},
"volume": {"h24": 15_000_000},
"pairCreatedAt": int((time.time() - 120 * 86400) * 1000),
"labels": ["dlmm"],
},
]
# ── Data Fetching ──────────────────────────────────────────────────
def fetch_pools(mint: str) -> list[dict]:
"""Fetch all DEX pools for a token from DexScreener.
Args:
mint: Token mint address.
Returns:
List of pool/pair objects with liquidity data.
"""
try:
resp = httpx.get(
f"{DEXSCREENER_BASE}/tokens/v1/solana/{mint}",
timeout=15,
)
resp.raise_for_status()
pairs = resp.json()
if not isinstance(pairs, list):
pairs = pairs.get("pairs", []) if isinstance(pairs, dict) else []
return [p for p in pairs if p.get("liquidity", {}).get("usd", 0) > 0]
except (httpx.HTTPError, ValueError) as e:
print(f" [warn] DexScreener fetch failed: {e}")
return []
def fetch_jupiter_quote(
token_mint: str, sol_amount: float, client: httpx.Client
) -> Optional[dict]:
"""Fetch a Jupiter quote for a specific trade size.
Args:
token_mint: Output token mint address.
sol_amount: Amount of SOL to swap.
client: Reusable httpx client.
Returns:
Quote response dict, or None on failure.
"""
try:
resp = client.get(
JUPITER_QUOTE_URL,
params={
"inputMint": SOL_MINT,
"outputMint": token_mint,
"amount": str(int(sol_amount * LAMPORTS)),
"slippageBps": 5000,
},
timeout=15,
)
if resp.status_code == 200:
return resp.json()
except (httpx.HTTPError, ValueError):
pass
return None
def build_slippage_curve(
token_mint: str, sizes: Optional[list[float]] = None
) -> list[dict]:
"""Build empirical slippage curve via Jupiter quotes.
Args:
token_mint: Token to buy with SOL.
sizes: SOL amounts to test.
Returns:
List of dicts with sol, tokens_out, slippage_bps.
"""
if sizes is None:
sizes = SLIPPAGE_TEST_SIZES
results: list[dict] = []
base_rate: Optional[float] = None
with httpx.Client() as client:
for sol in sizes:
quote = fetch_jupiter_quote(token_mint, sol, client)
if quote is None:
continue
out_amount = int(quote.get("outAmount", 0))
if out_amount <= 0:
continue
rate = out_amount / sol
if base_rate is None:
base_rate = rate
slippage_bps = int((1 - rate / base_rate) * 10000) if base_rate else 0
results.append({
"sol": sol,
"tokens_out": out_amount,
"slippage_bps": max(0, slippage_bps),
"price_impact_pct": quote.get("priceImpactPct", "0"),
})
time.sleep(0.15) # Respect rate limits
return results
# ── Analysis Functions ─────────────────────────────────────────────
def pool_age_hours(pair_created_at_ms: int) -> float:
"""Calculate pool age in hours from creation timestamp.
Args:
pair_created_at_ms: Pool creation time in milliseconds.
Returns:
Age in hours, or 0 if timestamp is invalid.
"""
if pair_created_at_ms <= 0:
return 0.0
return max(0.0, (time.time() * 1000 - pair_created_at_ms) / 3_600_000)
def compute_liquidity_score(
total_liquidity_usd: float,
pool_count: int,
largest_pool_pct: float,
oldest_pool_hours: float,
max_slippage_bps_at_1sol: int,
) -> int:
"""Compute composite liquidity score from 0 (dangerous) to 100 (deep).
Components:
Depth (40%): log-scaled TVL from $1K (0) to $1M+ (40)
Diversity (15%): more pools = more resilient, capped at 5 pools
Concentration (15%): penalty if one pool dominates
Age (15%): older pools are more reliable, full marks at 7 days
Slippage (15%): lower slippage at 1 SOL = better
Args:
total_liquidity_usd: Total liquidity across all pools in USD.
pool_count: Number of active pools.
largest_pool_pct: Fraction of total liquidity in the largest pool (0-1).
oldest_pool_hours: Age of the oldest pool in hours.
max_slippage_bps_at_1sol: Slippage in bps for a 1 SOL trade.
Returns:
Score from 0 to 100.
"""
# Depth: 0-40 points (log scale: $1K=0, $1M=40)
if total_liquidity_usd <= 0:
depth = 0
else:
depth = min(40, int(40 * math.log10(max(total_liquidity_usd, 1)) / 6))
# Diversity: 0-15 points (3 pts per pool, max 5 pools)
diversity = min(15, pool_count * 3)
# Concentration: 0-15 points (penalize single-pool dominance)
concentration = int(15 * (1 - largest_pool_pct))
# Age: 0-15 points (7+ days = full marks)
age = min(15, int(15 * min(oldest_pool_hours, 168) / 168))
# Slippage: 0-15 points (0 bps = 15 pts, 150+ bps = 0 pts)
slippage = max(0, 15 - max_slippage_bps_at_1sol // 10)
return max(0, min(100, depth + diversity + concentration + age + slippage))
def detect_risk_flags(pools: list[dict]) -> list[str]:
"""Detect liquidity risk flags from pool data.
Args:
pools: List of DexScreener pool/pair objects.
Returns:
List of human-readable warning strings.
"""
flags: list[str] = []
if len(pools) == 0:
flags.append("NO_POOLS: No active pools found")
return flags
if len(pools) == 1:
flags.append("SINGLE_POOL: Only 1 pool exists — exit may be difficult")
total_liq = sum(p.get("liquidity", {}).get("usd", 0) for p in pools)
if total_liq < 10_000:
flags.append(f"THIN_LIQUIDITY: Total TVL ${total_liq:,.0f} < $10,000")
for p in pools:
age_h = pool_age_hours(p.get("pairCreatedAt", 0))
if 0 < age_h < 2:
dex = p.get("dexId", "unknown")
flags.append(f"NEW_POOL: {dex} pool is {age_h:.1f}h old")
for p in pools:
vol = p.get("volume", {}).get("h24", 0)
liq = p.get("liquidity", {}).get("usd", 0)
if liq > 0 and vol / liq > 10:
dex = p.get("dexId", "unknown")
flags.append(f"VOLUME_MISMATCH: {dex} 24h volume/TVL = {vol/liq:.1f}x (possible wash trading)")
prices = [float(p.get("priceUsd", 0)) for p in pools if float(p.get("priceUsd", 0)) > 0]
if len(prices) >= 2:
deviation = (max(prices) - min(prices)) / min(prices)
if deviation > 0.05:
flags.append(f"PRICE_DEVIATION: {deviation:.1%} price difference across pools")
return flags
def max_position_from_liquidity(
total_liquidity_usd: float,
max_slippage_pct: float = 1.0,
trade_type: str = "swing",
) -> float:
"""Estimate maximum position size in USD based on available liquidity.
Uses rule-of-thumb: for constant-product AMMs, trading X% of pool
reserves produces approximately X% slippage.
Args:
total_liquidity_usd: Total liquidity across all pools.
max_slippage_pct: Maximum acceptable slippage percentage.
trade_type: One of "scalp", "swing", "position".
Returns:
Maximum position size in USD.
"""
fractions = {"scalp": 0.01, "swing": 0.03, "position": 0.07}
base_fraction = fractions.get(trade_type, 0.03)
adjusted = base_fraction * (max_slippage_pct / 2.0)
return total_liquidity_usd * adjusted
# ── Reporting ──────────────────────────────────────────────────────
def format_report(
token_mint: str,
pools: list[dict],
slippage_curve: list[dict],
score: int,
flags: list[str],
max_pos: float,
) -> str:
"""Format a complete liquidity analysis report.
Args:
token_mint: Token mint address.
pools: Pool data from DexScreener.
slippage_curve: Empirical slippage data from Jupiter.
score: Composite liquidity score (0-100).
flags: Risk flag strings.
max_pos: Maximum recommended position size in USD.
Returns:
Formatted report string.
"""
lines: list[str] = []
lines.append("=" * 70)
lines.append("LIQUIDITY ANALYSIS REPORT")
lines.append("=" * 70)
lines.append(f"Token: {token_mint}")
lines.append(f"Pools: {len(pools)}")
total_liq = sum(p.get("liquidity", {}).get("usd", 0) for p in pools)
total_vol = sum(p.get("volume", {}).get("h24", 0) for p in pools)
lines.append(f"Total Liquidity: ${total_liq:,.0f}")
lines.append(f"Total 24h Volume: ${total_vol:,.0f}")
lines.append("")
# Pool breakdown
lines.append("── Pool Breakdown " + "─" * 52)
lines.append(f" {'DEX':<12} {'Liquidity':>14} {'Volume 24h':>14} {'Age':>10} {'Price':>12}")
lines.append(" " + "-" * 64)
for p in sorted(pools, key=lambda x: x.get("liquidity", {}).get("usd", 0), reverse=True):
dex = p.get("dexId", "?")[:11]
liq = p.get("liquidity", {}).get("usd", 0)
vol = p.get("volume", {}).get("h24", 0)
age_h = pool_age_hours(p.get("pairCreatedAt", 0))
price = p.get("priceUsd", "?")
if age_h > 24:
age_str = f"{age_h / 24:.0f}d"
else:
age_str = f"{age_h:.1f}h"
lines.append(f" {dex:<12} ${liq:>12,.0f} ${vol:>12,.0f} {age_str:>10} ${price:>10}")
lines.append("")
# Slippage curve
if slippage_curve:
lines.append("── Slippage Curve " + "─" * 52)
lines.append(f" {'SOL':>8} {'Tokens Out':>16} {'Slippage':>10} {'Impact':>10}")
lines.append(" " + "-" * 46)
for s in slippage_curve:
sol_str = f"{s['sol']:.1f}"
tokens = f"{s['tokens_out']:,}"
slip = f"{s['slippage_bps']} bps"
impact = f"{s.get('price_impact_pct', '?')}%"
lines.append(f" {sol_str:>8} {tokens:>16} {slip:>10} {impact:>10}")
lines.append("")
# Score and sizing
lines.append("── Assessment " + "─" * 56)
score_label = (
"CRITICAL" if score < 20 else
"POOR" if score < 40 else
"FAIR" if score < 60 else
"GOOD" if score < 80 else
"EXCELLENT"
)
lines.append(f" Liquidity Score: {score}/100 ({score_label})")
lines.append(f" Max Position (swing, 1% slip): ${max_pos:,.0f}")
lines.append(f" Max Position (scalp, 0.5% slip): ${max_position_from_liquidity(total_liq, 0.5, 'scalp'):,.0f}")
lines.append("")
# Risk flags
if flags:
lines.append("── Risk Flags " + "─" * 56)
for f in flags:
lines.append(f" !! {f}")
lines.append("")
else:
lines.append("── Risk Flags " + "─" * 56)
lines.append(" No risk flags detected.")
lines.append("")
lines.append("=" * 70)
lines.append("Note: This is informational analysis, not financial advice.")
lines.append("Liquidity conditions change rapidly. Re-check before trading.")
lines.append("=" * 70)
return "\n".join(lines)
# ── Main ────────────────────────────────────────────────────────────
def main() -> None:
"""Run liquidity analysis for a token."""
# Determine token mint
if len(sys.argv) > 1:
token_mint = sys.argv[1]
else:
token_mint = os.getenv("TOKEN_MINT", "")
use_demo = not token_mint or token_mint == "--demo"
if use_demo:
print("[demo mode] Using hardcoded SOL/USDC pool data")
print("Pass a token mint address as argument for live analysis.\n")
token_mint = DEMO_MINT
pools = DEMO_POOLS
slippage_curve: list[dict] = [
{"sol": 0.1, "tokens_out": 68_900, "slippage_bps": 0, "price_impact_pct": "0.00"},
{"sol": 0.5, "tokens_out": 344_400, "slippage_bps": 1, "price_impact_pct": "0.01"},
{"sol": 1.0, "tokens_out": 688_700, "slippage_bps": 2, "price_impact_pct": "0.02"},
{"sol": 5.0, "tokens_out": 3_442_000, "slippage_bps": 5, "price_impact_pct": "0.05"},
{"sol": 10.0, "tokens_out": 6_880_000, "slippage_bps": 8, "price_impact_pct": "0.08"},
{"sol": 25.0, "tokens_out": 17_190_000, "slippage_bps": 15, "price_impact_pct": "0.15"},
]
else:
print(f"Fetching pools for {token_mint}...")
pools = fetch_pools(token_mint)
if not pools:
print("No pools found. Token may not be listed or mint address may be incorrect.")
sys.exit(1)
print(f"Found {len(pools)} pool(s). Building slippage curve...")
slippage_curve = build_slippage_curve(token_mint)
# Compute metrics
total_liq = sum(p.get("liquidity", {}).get("usd", 0) for p in pools)
if total_liq > 0:
largest_pool_liq = max(p.get("liquidity", {}).get("usd", 0) for p in pools)
largest_pool_pct = largest_pool_liq / total_liq
else:
largest_pool_pct = 1.0
oldest_hours = max(
(pool_age_hours(p.get("pairCreatedAt", 0)) for p in pools),
default=0.0,
)
slippage_at_1sol = 0
for s in slippage_curve:
if s["sol"] == 1.0:
slippage_at_1sol = s["slippage_bps"]
break
score = compute_liquidity_score(
total_liq, len(pools), largest_pool_pct, oldest_hours, slippage_at_1sol
)
flags = detect_risk_flags(pools)
max_pos = max_position_from_liquidity(total_liq, max_slippage_pct=1.0, trade_type="swing")
report = format_report(token_mint, pools, slippage_curve, score, flags, max_pos)
print(report)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Compare DEX pools for a Solana token.
Fetches all pools from DexScreener, compares them by liquidity, volume,
age, and price, identifies the best pool for execution, and flags
suspicious pools.
Usage:
python scripts/pool_comparison.py <token_mint>
python scripts/pool_comparison.py # Demo mode with BONK
TOKEN_MINT=<address> python scripts/pool_comparison.py
Dependencies:
uv pip install httpx
Environment Variables:
TOKEN_MINT: Token mint address (optional, overridden by CLI arg)
"""
import os
import sys
import time
from typing import Optional
import httpx
# ── Configuration ───────────────────────────────────────────────────
DEXSCREENER_BASE = "https://api.dexscreener.com"
# BONK mint for demo mode
DEMO_MINT = "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263"
DEMO_POOLS = [
{
"pairAddress": "8sLbNZoA1cfnvMJLPfp98ZR1Gm14nAkoTn4YzMNqDZkN",
"dexId": "raydium",
"baseToken": {"symbol": "BONK", "address": DEMO_MINT},
"quoteToken": {"symbol": "SOL", "address": "So11111111111111111111111111111111111111112"},
"priceUsd": "0.00002341",
"liquidity": {"usd": 4_500_000, "base": 192_000_000_000, "quote": 15_500},
"volume": {"h24": 12_000_000, "h6": 3_200_000, "h1": 450_000},
"txns": {"h24": {"buys": 8500, "sells": 7200}},
"pairCreatedAt": int((time.time() - 400 * 86400) * 1000),
"labels": [],
},
{
"pairAddress": "9dMXBpMXA1zMcbdv4PgFhoBLGsXgUEi6YkBhsK6kxPgA",
"dexId": "orca",
"baseToken": {"symbol": "BONK", "address": DEMO_MINT},
"quoteToken": {"symbol": "SOL", "address": "So11111111111111111111111111111111111111112"},
"priceUsd": "0.00002339",
"liquidity": {"usd": 2_800_000, "base": 119_000_000_000, "quote": 9_650},
"volume": {"h24": 7_500_000, "h6": 1_800_000, "h1": 280_000},
"txns": {"h24": {"buys": 4200, "sells": 3900}},
"pairCreatedAt": int((time.time() - 350 * 86400) * 1000),
"labels": ["concentrated"],
},
{
"pairAddress": "2KgMEJkL4XKqbFMjT2mFNc3VGHh7HQXR3HjfNfTS7Nfu",
"dexId": "meteora",
"baseToken": {"symbol": "BONK", "address": DEMO_MINT},
"quoteToken": {"symbol": "SOL", "address": "So11111111111111111111111111111111111111112"},
"priceUsd": "0.00002345",
"liquidity": {"usd": 950_000, "base": 40_500_000_000, "quote": 3_270},
"volume": {"h24": 3_200_000, "h6": 900_000, "h1": 120_000},
"txns": {"h24": {"buys": 1800, "sells": 1500}},
"pairCreatedAt": int((time.time() - 90 * 86400) * 1000),
"labels": ["dlmm"],
},
{
"pairAddress": "FakePoolForDemoSuspiciousActivity1234567890ab",
"dexId": "raydium",
"baseToken": {"symbol": "BONK", "address": DEMO_MINT},
"quoteToken": {"symbol": "USDC", "address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"},
"priceUsd": "0.00002290",
"liquidity": {"usd": 5_000, "base": 214_000_000, "quote": 2_500},
"volume": {"h24": 180_000, "h6": 45_000, "h1": 8_000},
"txns": {"h24": {"buys": 120, "sells": 95}},
"pairCreatedAt": int((time.time() - 0.5 * 86400) * 1000),
"labels": [],
},
]
# ── Data Fetching ──────────────────────────────────────────────────
def fetch_pools(mint: str) -> list[dict]:
"""Fetch all DEX pools for a token from DexScreener.
Args:
mint: Token mint address.
Returns:
List of pool/pair objects with liquidity > 0.
"""
try:
resp = httpx.get(
f"{DEXSCREENER_BASE}/tokens/v1/solana/{mint}",
timeout=15,
)
resp.raise_for_status()
pairs = resp.json()
if not isinstance(pairs, list):
pairs = pairs.get("pairs", []) if isinstance(pairs, dict) else []
return [p for p in pairs if p.get("liquidity", {}).get("usd", 0) > 0]
except (httpx.HTTPError, ValueError) as e:
print(f" [warn] DexScreener fetch failed: {e}")
return []
# ── Analysis Functions ─────────────────────────────────────────────
def pool_age_hours(pair_created_at_ms: int) -> float:
"""Calculate pool age in hours from creation timestamp.
Args:
pair_created_at_ms: Creation time in milliseconds.
Returns:
Age in hours.
"""
if pair_created_at_ms <= 0:
return 0.0
return max(0.0, (time.time() * 1000 - pair_created_at_ms) / 3_600_000)
def format_age(hours: float) -> str:
"""Format age in hours to human-readable string.
Args:
hours: Age in hours.
Returns:
Formatted string like '3.5h', '14d', '6mo'.
"""
if hours < 1:
return f"{hours * 60:.0f}m"
elif hours < 48:
return f"{hours:.1f}h"
elif hours < 24 * 90:
return f"{hours / 24:.0f}d"
else:
return f"{hours / 24 / 30:.0f}mo"
def analyze_pool(pool: dict, all_pools: list[dict]) -> dict:
"""Analyze a single pool and compute metrics.
Args:
pool: DexScreener pair object.
all_pools: All pools for context (price comparison).
Returns:
Dict with analysis metrics.
"""
liq = pool.get("liquidity", {}).get("usd", 0)
vol_24h = pool.get("volume", {}).get("h24", 0)
age_h = pool_age_hours(pool.get("pairCreatedAt", 0))
price = float(pool.get("priceUsd", 0))
txns = pool.get("txns", {}).get("h24", {})
buys = txns.get("buys", 0)
sells = txns.get("sells", 0)
total_txns = buys + sells
# Volume/liquidity ratio (healthy: 1-5x, suspicious: >10x)
vol_liq_ratio = vol_24h / liq if liq > 0 else 0
# Buy/sell ratio (healthy: 0.4-0.6 buys, suspicious if very skewed)
buy_ratio = buys / total_txns if total_txns > 0 else 0.5
# Price deviation from median across all pools
all_prices = [float(p.get("priceUsd", 0)) for p in all_pools if float(p.get("priceUsd", 0)) > 0]
median_price = sorted(all_prices)[len(all_prices) // 2] if all_prices else price
price_deviation_pct = abs(price - median_price) / median_price * 100 if median_price > 0 else 0
return {
"dex": pool.get("dexId", "unknown"),
"pair_address": pool.get("pairAddress", ""),
"base": pool.get("baseToken", {}).get("symbol", "?"),
"quote": pool.get("quoteToken", {}).get("symbol", "?"),
"price_usd": price,
"liquidity_usd": liq,
"volume_24h": vol_24h,
"age_hours": age_h,
"total_txns_24h": total_txns,
"buy_ratio": buy_ratio,
"vol_liq_ratio": vol_liq_ratio,
"price_deviation_pct": price_deviation_pct,
"labels": pool.get("labels", []),
}
def flag_suspicious(analysis: dict) -> list[str]:
"""Flag suspicious characteristics for a pool.
Args:
analysis: Pool analysis dict from analyze_pool().
Returns:
List of warning strings.
"""
flags: list[str] = []
if analysis["age_hours"] < 2:
flags.append("VERY_NEW: Pool less than 2 hours old")
elif analysis["age_hours"] < 24:
flags.append("NEW: Pool less than 24 hours old")
if analysis["liquidity_usd"] < 5_000:
flags.append(f"MICRO_LIQUIDITY: Only ${analysis['liquidity_usd']:,.0f}")
elif analysis["liquidity_usd"] < 10_000:
flags.append(f"LOW_LIQUIDITY: ${analysis['liquidity_usd']:,.0f}")
if analysis["vol_liq_ratio"] > 10:
flags.append(f"HIGH_VOL_RATIO: {analysis['vol_liq_ratio']:.1f}x volume/TVL")
if analysis["price_deviation_pct"] > 5:
flags.append(f"PRICE_OFF: {analysis['price_deviation_pct']:.1f}% from median")
if analysis["buy_ratio"] > 0.85:
flags.append(f"BUY_SKEWED: {analysis['buy_ratio']:.0%} buys (possible bot activity)")
elif analysis["buy_ratio"] < 0.15:
flags.append(f"SELL_SKEWED: {1 - analysis['buy_ratio']:.0%} sells (possible dump)")
if analysis["total_txns_24h"] < 20:
flags.append(f"LOW_ACTIVITY: Only {analysis['total_txns_24h']} txns in 24h")
return flags
def rank_pools(analyses: list[dict]) -> list[dict]:
"""Rank pools by execution quality.
Scoring: 50% liquidity rank + 30% volume rank + 20% age rank.
Penalties for suspicious flags.
Args:
analyses: List of pool analysis dicts.
Returns:
Sorted list with added 'rank_score' field.
"""
if not analyses:
return []
n = len(analyses)
# Rank by each metric (higher = better)
by_liq = sorted(range(n), key=lambda i: analyses[i]["liquidity_usd"])
by_vol = sorted(range(n), key=lambda i: analyses[i]["volume_24h"])
by_age = sorted(range(n), key=lambda i: analyses[i]["age_hours"])
liq_rank = {idx: pos for pos, idx in enumerate(by_liq)}
vol_rank = {idx: pos for pos, idx in enumerate(by_vol)}
age_rank = {idx: pos for pos, idx in enumerate(by_age)}
for i, a in enumerate(analyses):
base_score = (
0.50 * liq_rank[i] / max(n - 1, 1)
+ 0.30 * vol_rank[i] / max(n - 1, 1)
+ 0.20 * age_rank[i] / max(n - 1, 1)
)
# Penalty for suspicious flags
flags = flag_suspicious(a)
penalty = len(flags) * 0.1
a["rank_score"] = max(0, base_score - penalty)
a["flags"] = flags
return sorted(analyses, key=lambda x: x["rank_score"], reverse=True)
# ── Reporting ──────────────────────────────────────────────────────
def format_comparison(token_mint: str, ranked: list[dict]) -> str:
"""Format pool comparison report.
Args:
token_mint: Token mint address.
ranked: Ranked pool analyses.
Returns:
Formatted report string.
"""
lines: list[str] = []
lines.append("=" * 78)
lines.append("POOL COMPARISON REPORT")
lines.append("=" * 78)
lines.append(f"Token: {token_mint}")
lines.append(f"Pools Found: {len(ranked)}")
total_liq = sum(a["liquidity_usd"] for a in ranked)
total_vol = sum(a["volume_24h"] for a in ranked)
lines.append(f"Total Liquidity: ${total_liq:,.0f}")
lines.append(f"Total 24h Volume: ${total_vol:,.0f}")
lines.append("")
# Comparison table
lines.append("── Pool Comparison " + "─" * 58)
header = (
f" {'#':>2} {'DEX':<10} {'Pair':<10} {'Liquidity':>12} "
f"{'Volume 24h':>12} {'Age':>6} {'Txns':>6} {'Score':>6}"
)
lines.append(header)
lines.append(" " + "-" * 74)
for i, a in enumerate(ranked, 1):
pair = f"{a['base']}/{a['quote']}"[:9]
age_str = format_age(a["age_hours"])
lines.append(
f" {i:>2} {a['dex']:<10} {pair:<10} "
f"${a['liquidity_usd']:>10,.0f} "
f"${a['volume_24h']:>10,.0f} "
f"{age_str:>6} "
f"{a['total_txns_24h']:>5} "
f"{a['rank_score']:>5.2f}"
)
lines.append("")
# Best pool recommendation
if ranked:
best = ranked[0]
lines.append("── Best Pool for Execution " + "─" * 50)
lines.append(f" DEX: {best['dex']}")
lines.append(f" Pair: {best['base']}/{best['quote']}")
lines.append(f" Address: {best['pair_address']}")
lines.append(f" Liquidity: ${best['liquidity_usd']:,.0f}")
lines.append(f" 24h Volume: ${best['volume_24h']:,.0f}")
lines.append(f" Age: {format_age(best['age_hours'])}")
pct = best["liquidity_usd"] / total_liq * 100 if total_liq > 0 else 0
lines.append(f" Share of Total Liquidity: {pct:.1f}%")
lines.append("")
# Suspicious pools
suspicious = [a for a in ranked if a.get("flags")]
if suspicious:
lines.append("── Suspicious Pool Flags " + "─" * 52)
for a in suspicious:
lines.append(f" {a['dex']} ({a['base']}/{a['quote']}):")
for f in a["flags"]:
lines.append(f" !! {f}")
lines.append("")
# Price comparison
prices = [a for a in ranked if a["price_usd"] > 0]
if len(prices) >= 2:
lines.append("── Price Comparison " + "─" * 57)
for a in prices:
lines.append(f" {a['dex']:<10} {a['base']}/{a['quote']:<8} ${a['price_usd']:.10f}")
min_p = min(a["price_usd"] for a in prices)
max_p = max(a["price_usd"] for a in prices)
spread = (max_p - min_p) / min_p * 100 if min_p > 0 else 0
lines.append(f" Price spread: {spread:.2f}%")
if spread > 2:
lines.append(" !! Significant price deviation — possible arbitrage or stale pool")
lines.append("")
lines.append("=" * 78)
lines.append("Note: This is informational analysis, not financial advice.")
lines.append("Pool conditions change rapidly. Re-check before trading.")
lines.append("=" * 78)
return "\n".join(lines)
# ── Main ────────────────────────────────────────────────────────────
def main() -> None:
"""Run pool comparison for a token."""
if len(sys.argv) > 1:
token_mint = sys.argv[1]
else:
token_mint = os.getenv("TOKEN_MINT", "")
use_demo = not token_mint or token_mint == "--demo"
if use_demo:
print("[demo mode] Using hardcoded BONK pool data")
print("Pass a token mint address as argument for live analysis.\n")
token_mint = DEMO_MINT
pools = DEMO_POOLS
else:
print(f"Fetching pools for {token_mint}...")
pools = fetch_pools(token_mint)
if not pools:
print("No pools found. Token may not be listed or mint address may be incorrect.")
sys.exit(1)
print(f"Found {len(pools)} pool(s).\n")
# Analyze each pool
analyses = [analyze_pool(p, pools) for p in pools]
# Rank and report
ranked = rank_pools(analyses)
report = format_comparison(token_mint, ranked)
print(report)
if __name__ == "__main__":
main()