Learn how to build a Kamino Finance yield vault tracker using CoinMarketCap API on Solana.
Introduction
Kamino Finance dominates Solana lending.
With over $3.5 billion in TVL and a modular two-layer architecture — a permissionless Market Layer and a curator-managed Vault Layer — Kamino has become the foundational yield infrastructure on Solana. Expert curators like Gauntlet, Steakhouse Financial, and Allez Labs manage vaults across risk tiers: Conservative, Balanced, and Aggressive. Users deposit once and let the protocol rebalance automatically.
The challenge for developers is not execution. It is knowing which assets and markets are worth tracking before allocating capital.
CoinMarketCap API solves that. It gives you a structured data layer to discover vault-relevant assets, monitor TVL and price momentum, validate DEX pool liquidity, and apply macro regime filters — all before you interact with any Kamino vault on-chain.
- CoinMarketCap API powers the signal engine
- Kamino SDK and Solana RPC handle on-chain vault state validation
Why Use CoinMarketCap API for a Kamino Yield Vault Tracker?
Kamino gives you yield automation. CoinMarketCap gives you market context.
Instead of blindly depositing into vaults, you can use CoinMarketCap to:
- discover KMNO and vault-relevant assets with their Solana contract addresses
- monitor TVL and price momentum across Kamino ecosystem tokens
- validate DEX pool liquidity for assets in target vaults
- detect whale transaction flow on Raydium and Orca pools
- apply macro regime filters before deploying capital
- identify trending DeFi assets generating yield rotation on Solana
This turns your tracker from a deposit dashboard into a decision-making system.
System Architecture
CoinMarketCap API (Signal Layer)
├─ Asset Discovery (KMNO, vault assets, SPL addresses)
├─ TVL + Price Context (quotes, momentum, tvl_ratio)
├─ DEX Pool Validation (liquidity, LP burn/lock)
├─ Transaction Flow (whale detection)
├─ Sub-minute Candles (momentum)
└─ Macro Regime (fear/greed, altcoin season)
↓
Yield Signal Engine
↓
Kamino SDK / Solana RPC (Validation Layer)
↓
Vault APY + On-chain State Verification
Architecture Clarification
The CoinMarketCap API acts strictly as an off-chain Signal Layer for Kamino vault asset discovery, yield context monitoring, and lending market regime detection. It is not an on-chain yield oracle or vault state engine.
Real vault APYs, utilization rates, borrow/supply rates, liquidation thresholds, and kToken NAV must be validated directly on-chain via Kamino's SDK or Solana RPC. CMC data reflects market conditions with cache delays and does not track individual vault allocations, curator rebalancing events, or Kamino's internal risk parameters.
Project Setup
Python Dependencies
import os
import time
import requests
import pandas as pd
import numpy as np
Environment Variables
CMC_API_KEY = os.getenv("CMC_API_KEY")
CMC_BASE_URL = "https://pro-api.coinmarketcap.com"
Headers
HEADERS = {
"Accept": "application/json",
"Content-Type": "application/json",
"X-CMC_PRO_API_KEY": CMC_API_KEY,
}
Target Assets
# Core assets to track across Kamino vaults
KAMINO_ASSETS = ["KMNO", "USDC", "SOL", "USDT", "JitoSOL", "mSOL"]
# DEX slugs for Kamino-relevant pools on Solana
# Note: "meteora" returns 400 — the correct slug is "meteora-dlmm"
SOLANA_DEX_SLUGS = ["raydium", "orca", "meteora-dlmm"]
Step 1: Map Assets to CoinMarketCap IDs
Resolve all tracked assets to their CoinMarketCap IDs. Use IDs — not symbols — for all subsequent Core API calls. Symbols are ambiguous.
Endpoint
GET /v1/cryptocurrency/map
def map_assets(symbols="KMNO,USDC,SOL,USDT"):
url = f"{CMC_BASE_URL}/v1/cryptocurrency/map"
params = {"symbol": symbols}
r = requests.get(url, headers=HEADERS, params=params)
r.raise_for_status()
return r.json()["data"]
From each asset object, extract:
- id — use for all Core API calls
- platform.token_address — SPL mint address on Solana
- symbol
Note: /v1/cryptocurrency/map does not support filtering by chain natively. Filter locally by platform.name == "Solana" after fetching results.
Step 2: Fetch Quotes and TVL Context
Pull market data and TVL for all tracked assets. This is the base layer for yield rotation signals.
Endpoint
GET /v3/cryptocurrency/quotes/latest
def fetch_quotes(ids):
url = f"{CMC_BASE_URL}/v3/cryptocurrency/quotes/latest"
params = {"id": ",".join(str(i) for i in ids)}
r = requests.get(url, headers=HEADERS, params=params)
r.raise_for_status()
return r.json()["data"]
def parse_quote(asset):
# quote is a LIST in v3 — use next() to find USD entry by symbol
usd = next(
(q for q in asset.get("quote", []) if q.get("symbol") == "USD"),
{}
)
return {
"id": asset.get("id"),
"symbol": asset.get("symbol"),
"price": usd.get("price"),
"volume_24h": usd.get("volume_24h"),
"market_cap": usd.get("market_cap"),
"fdv": usd.get("fully_diluted_market_cap"),
"pct_change_1h": usd.get("percent_change_1h"),
"pct_change_24h": usd.get("percent_change_24h"),
"pct_change_7d": usd.get("percent_change_7d"),
"tvl": usd.get("tvl"), # lives inside quote -> USD
"tvl_ratio": asset.get("tvl_ratio"), # lives at asset root
}
tvl lives inside quote -> USD. tvl_ratio lives at the asset root. Both may return null — always parse defensively. For KMNO and major Kamino vault assets, TVL is generally populated given Kamino's scale, but treat nulls as expected behavior under network load.
The data field is a list. Build the lookup dict by iterating:
# raw_quotes is a list — build dict keyed by string ID
quotes = {str(a["id"]): parse_quote(a) for a in raw_quotes}
Step 3: Score Assets for Yield Rotation
Rank assets by yield rotation signals before checking vault availability.
def compute_yield_score(quote):
score = 0
# TVL signal — high TVL indicates protocol confidence
tvl = quote.get("tvl") or 0
if tvl > 500_000_000: score += 30
elif tvl > 50_000_000: score += 15
# Momentum — rising price suggests active capital rotation
pct_24h = quote.get("pct_change_24h") or 0
if pct_24h > 5: score += 25
elif pct_24h > 2: score += 15
elif pct_24h < -10: score -= 20
# Volume — high volume signals active market
vol = quote.get("volume_24h") or 0
if vol > 50_000_000: score += 25
elif vol > 10_000_000: score += 10
# TVL ratio — price relative to TVL
tvl_ratio = quote.get("tvl_ratio") or 0
if 0 < tvl_ratio < 1: score += 20 # undervalued relative to TVL
return score
Step 4: Validate DEX Pool Liquidity
Confirm that sufficient on-chain liquidity exists before treating a signal as actionable.
Endpoint
GET /v1/dex/token/pools
def fetch_token_pools(token_address):
url = f"{CMC_BASE_URL}/v1/dex/token/pools"
params = {
"address": token_address,
"platform": "solana"
}
r = requests.get(url, headers=HEADERS, params=params)
r.raise_for_status()
return r.json()["data"]
Key fields per pool:
- exn — DEX name (Raydium, Orca, Meteora)
- liqUsd - liquidity in USD
- v24 - 24h volume
- addr - pool address
- pubAt - pool creation timestamp
- lr - locked rate, percentage of LP locked
- br - burned rate, percentage of LP burned
Note: lr and br are documented as available for Solana pools but are frequently absent in live data. Do not rely on them as a signal. Use liqUsd and v24 as the primary pool quality indicators. For LP lock validation, query the pool contract directly on-chain.
def get_best_pool(pools, min_liquidity=100_000):
valid = [
p for p in pools
if (p.get("liqUsd") or 0) >= min_liquidity
]
return max(valid, key=lambda p: p.get("liqUsd", 0)) if valid else None
Step 5: Get Pool-Level Price and Reserves
Fetch precise pool-level pricing for execution context.
Endpoint
GET /v4/dex/pairs/quotes/latest
def fetch_pool_quote(pool_address, network_slug="solana"):
url = f"{CMC_BASE_URL}/v4/dex/pairs/quotes/latest"
params = {
"network_slug": network_slug, # required — disambiguates contracts across chains
"contract_address": pool_address,
"aux": "pool_base_asset,pool_quote_asset,buy_tax,sell_tax"
}
r = requests.get(url, headers=HEADERS, params=params)
r.raise_for_status()
return r.json()["data"]
network_slug is required alongside contract_address. Omitting it returns a 400 error.
Step 6: Discover Active Pairs on Solana DEXs
Find active trading pairs for vault-relevant assets across Raydium, Orca, and Meteora.
Endpoint
GET /v4/dex/spot-pairs/latest
dex_slug is required alongside network_slug. Passing only network_slug returns a 400 error. Make one request per DEX:
def fetch_pairs_for_dex(dex_slug, network_slug="solana"):
url = f"{CMC_BASE_URL}/v4/dex/spot-pairs/latest"
params = {
"network_slug": network_slug,
"dex_slug": dex_slug,
"sort": "liquidity",
"sort_dir": "desc"
}
r = requests.get(url, headers=HEADERS, params=params)
r.raise_for_status()
return r.json()["data"]
def fetch_all_solana_pairs(asset_symbol):
results = {}
for dex in SOLANA_DEX_SLUGS:
try:
pairs = fetch_pairs_for_dex(dex)
matched = [
p for p in pairs
if asset_symbol.upper() in (
p.get("base_asset_symbol", "").upper(),
p.get("quote_asset_symbol", "").upper()
)
]
if matched:
results[dex] = matched[0]
except Exception:
continue
return results
Price, liquidity, and volume live inside the quote array. Filter by convert_id == "2781" for USD values:
def parse_pair_quote(pair):
quotes = pair.get("quote", [])
usd = next(
(q for q in quotes if str(q.get("convert_id")) == "2781"), {}
)
return {
"dex": pair.get("dex_slug"),
"price": usd.get("price"),
"liquidity": usd.get("liquidity"),
"volume_24h": usd.get("volume_24h"),
}
Step 7: Monitor On-Chain Transaction Flow
Track whale-level activity to detect institutional rotation into or out of vault assets.
Endpoint
GET /v1/dex/tokens/transactions
def fetch_transactions(token_address, min_volume=50_000):
url = f"{CMC_BASE_URL}/v1/dex/tokens/transactions"
params = {
"address": token_address,
"platform": "solana", # "solana" only — "sol" returns 400
"minVolume": min_volume
}
r = requests.get(url, headers=HEADERS, params=params)
r.raise_for_status()
return r.json()["data"]
Returns raw swap records. Key fields per record: tx (hash), v (volume USD), t0a (base asset), t1a (quote asset). Large inflows into USDC or SOL pools often precede vault deposit surges on Kamino.
Step 8: Sub-Minute Momentum Detection
Track fast price movements on vault assets to detect entry timing signals.
Endpoint
GET /v1/k-line/candles
def fetch_candles(token_address, interval="1min"):
url = f"{CMC_BASE_URL}/v1/k-line/candles"
params = {
"platform": "solana",
"address": token_address,
"interval": interval # 1s, 5s, 30s, 1min, 3min
}
r = requests.get(url, headers=HEADERS, params=params)
r.raise_for_status()
return r.json()["data"]
Each candle is a positional array of 7 elements — not a dict:
Index
Field
[0]
open
[1]
high
[2]
low
[3]
close
[4]
volume
[5]
timestamp, UNIX seconds
[6]
traders, unique trader count
Do not call .get() on a candle. Use parse_candle() to convert:
def parse_candle(c):
return {
"open": c[0],
"high": c[1],
"low": c[2],
"close": c[3],
"volume": c[4],
"timestamp": c[5],
"traders": c[6],
}
Note: /v4/dex/pairs/ohlcv/historical returns 500 in production. Use /v1/k-line/candles for all DEX candle and historical data on Solana.
Step 9: Apply Macro Regime Filters
Yield rotation is more reliable in risk-on macro conditions. Filter out adverse regimes before flagging vault opportunities.
Endpoints
GET /v3/fear-and-greed/latest
GET /v1/altcoin-season-index/latest
def fetch_macro_regime():
fg_url = f"{CMC_BASE_URL}/v3/fear-and-greed/latest"
as_url = f"{CMC_BASE_URL}/v1/altcoin-season-index/latest"
fg = requests.get(fg_url, headers=HEADERS).json()["data"]
as_idx = requests.get(as_url, headers=HEADERS).json()["data"]
return {
"fear_greed_value": fg.get("value"),
"fear_greed_classification": fg.get("value_classification"),
"altcoin_index": as_idx.get("altcoin_index"),
}
def is_regime_favorable(regime):
fg = regime.get("fear_greed_value") or 0
as_i = regime.get("altcoin_index") or 0
# Avoid extreme fear; prefer Solana DeFi conditions
return fg > 35 and as_i >= 40
Fear & Greed updates every 15 minutes. Altcoin Season Index: ≥75 signals Altcoin Season, <25 signals Bitcoin Season. Both endpoints are available on the Basic plan.
Step 10: Discover Trending DeFi Assets
Identify which Solana DeFi and lending assets are attracting the most volume.
Trending Endpoints (Paid Only)
GET /v1/dex/tokens/trending/list (POST)
Paid Endpoint Warning
Error Code: 1006 [API_KEY_PLAN_NOT_AUTHORIZED]
Message: "Your API Key subscription plan doesn't support this endpoint."
Returns HTTP 403 on the Basic plan.
Basic Plan Fallback
def fetch_trending_defi():
url = f"{CMC_BASE_URL}/v3/cryptocurrency/listings/latest"
params = {
"sort": "volume_24h",
"sort_dir": "desc",
"limit": 200,
"tag": "defi", # accepted value on Basic
"percent_change_24h_min": 2,
"volume_24h_min": 5_000_000,
}
r = requests.get(url, headers=HEADERS, params=params)
r.raise_for_status()
return r.json()["data"]
# Filter locally for Solana / Kamino ecosystem assets
KAMINO_TAGS = {"solana-ecosystem", "lending", "defi", "yield-farming"}
def filter_kamino_relevant(assets):
results = []
for asset in assets:
tags = set(asset.get("tags") or [])
if tags & KAMINO_TAGS or asset.get("symbol") in KAMINO_ASSETS:
results.append(asset)
return results
The tag query parameter accepts "defi" on the Basic plan. Filter for Solana-ecosystem and lending tags locally — do not pass "solana-ecosystem" or "lending" directly as query params.
Step 11: Liquidity Quality — CEX Volume Proxy
Paid Endpoint Warning
/v2/cryptocurrency/market-pairs/latest returns HTTP 403 on the Basic plan.
Error Code: 1006 [API_KEY_PLAN_NOT_AUTHORIZED]
Despite documentation listing Basic as supported, live testing confirms 403.
Basic Plan Fallback
def estimate_liquidity(quote):
volume = quote.get("volume_24h") or 0
mkt_cap = quote.get("market_cap") or 0
return {
"volume_24h": volume,
"market_cap": mkt_cap,
"liquidity_signal": "high" if volume > 20_000_000
else "medium" if volume > 5_000_000
else "low",
}
If you have a paid plan, use /v2/cryptocurrency/market-pairs/latest with aux="effective_liquidity,market_score,market_reputation". Depth fields at quote -> USD -> depth_negative_two are frequently null for DeFi tokens.
Step 12: Historical Backtesting
Paid Endpoint Warning
/v3/cryptocurrency/quotes/historical returns HTTP 403 on the Basic plan.
Despite documentation listing Basic as supported, any time-series request returns 403.
Backtesting requires a paid CMC plan.
Cache: 5 minutes. Cost: 1 credit per 100 datapoints.
def fetch_historical_quotes(asset_id, time_start, time_end, interval="1h"):
url = f"{CMC_BASE_URL}/v3/cryptocurrency/quotes/historical"
params = {
"id": asset_id,
"time_start": time_start,
"time_end": time_end,
"interval": interval, # "1h", "4h", "daily"
}
r = requests.get(url, headers=HEADERS, params=params)
r.raise_for_status()
return r.json()["data"]
Use /v1/k-line/candles as the free alternative for DEX-level candle history on Solana.
Step 13: Minimal End-to-End Flow
def run_kamino_vault_tracker(asset_ids, asset_map):
# 1. Macro regime — poll every 15 min
regime = fetch_macro_regime()
# 2. Quotes — raw_quotes is a list, build dict by id
raw_quotes = fetch_quotes(list(asset_ids.values()))
quotes = {str(a["id"]): parse_quote(a) for a in raw_quotes}
results = []
for symbol, asset_id in asset_ids.items():
quote = quotes.get(str(asset_id), {})
# 3. Score the asset
yield_score = compute_yield_score(quote)
if yield_score < 30:
continue
# 4. Pool validation
token_addr = asset_map.get(symbol, {}).get("token_address")
if not token_addr:
continue
try:
pools = fetch_token_pools(token_addr)
best_pool = get_best_pool(pools)
except Exception:
continue
if not best_pool:
continue
# 5. Momentum check
try:
candles = fetch_candles(token_addr, interval="5min")
latest = candles[-1] if candles else None
traders = latest[6] if latest and len(latest) > 6 else 0
except Exception:
traders = 0
# 6. Regime filter
if not is_regime_favorable(regime):
yield_score -= 20
results.append({
"symbol": symbol,
"yield_score": yield_score,
"price": quote.get("price"),
"tvl": quote.get("tvl"),
"pct_24h": quote.get("pct_change_24h"),
"volume_24h": quote.get("volume_24h"),
"pool_liq": best_pool.get("liqUsd"),
"pool_dex": best_pool.get("exn"),
"traders_5min": traders,
})
return sorted(results, key=lambda x: -x["yield_score"])
Pass high-scoring assets to your Kamino SDK layer for on-chain vault APY validation and deposit execution.
Rate Limits and Polling
CoinMarketCap API is REST-only. There is no WebSocket streaming.
Cache Intervals
Endpoint Group
Cache Interval
Quotes, /v3/cryptocurrency/quotes/latest
60 seconds
DEX pairs, /v4/dex/spot-pairs/latest
60 seconds
Pool data, /v1/dex/token/pools
60 seconds
Candles, /v1/k-line/candles
60 seconds
Fear & Greed, Altcoin Season
15 minutes
Historical quotes
5 minutes, paid plan only
Best Practices
- poll every 60 seconds for all price and pool endpoints
- poll macro endpoints every 15 minutes
- cache responses locally between polls
- use exponential backoff for HTTP 429 errors, rate reset at 60 seconds
def request_with_backoff(fn, retries=3, base_delay=2):
for attempt in range(retries):
try:
return fn()
except requests.exceptions.HTTPError as e:
if e.response.status_code == 429:
time.sleep(base_delay ** attempt)
else:
raise
raise Exception("Max retries exceeded")
Common Mistakes
Parsing quote as a dict in v3
In /v3/cryptocurrency/quotes/latest, quote is a list. Using asset["quote"]["USD"] raises an AttributeError. The correct pattern is:
next((q for q in asset.get("quote", []) if q.get("symbol") == "USD"), {})
Iterating raw_quotes as a dict
The data field from /v3/cryptocurrency/quotes/latest is a list. Build the lookup dict by iterating:
{str(a["id"]): parse_quote(a) for a in raw_quotes}
Passing only network_slug to /v4/dex/spot-pairs/latest
dex_slug is required. Omitting it returns a 400 error. Make one request per DEX slug.
Omitting network_slug from pool quotes
/v4/dex/pairs/quotes/latest requires network_slug alongside contract_address. Omitting it returns a 400 error.
Using "sol" as the platform value
/v1/dex/tokens/transactions only accepts "solana". The abbreviated "sol" returns 400.
Tracking kTokens via the DEX API
Kamino kTokens are protocol-internal vault shares. They are not indexed in the CMC DEX API unless they have active secondary pools. Track the underlying assets (SOL, USDC, etc.) for market signals.
Assuming /v2/cryptocurrency/market-pairs/latest works on Basic
Returns 403 in practice. Use volume_24h from quotes as a liquidity proxy.
Assuming /v3/cryptocurrency/quotes/historical works on Basic
Returns 403 for any time-series request. Backtesting requires a paid plan. Use /v1/k-line/candles for DEX-level candle history.
Using /v4/dex/pairs/ohlcv/historical
Returns 500 in production. Use /v1/k-line/candles for all DEX candle data.
Calling .get() on candle arrays
Candles from /v1/k-line/candles are positional arrays. Use candle[6] for traders — not candle.get("traders").
Treating CMC as a vault state engine
CMC does not track vault APYs, utilization rates, curator rebalancing, or kToken NAV. Always validate vault state on-chain via Kamino SDK or Solana RPC before any deposit.
Final Thoughts
Kamino has made yield on Solana institutional-grade. Curated vaults, risk-tiered strategies, and automated rebalancing remove the manual overhead of managing DeFi positions.
CoinMarketCap API gives you the structured signal layer to decide which assets and market conditions are worth acting on before you interact with any vault on-chain.
The key separation:
- CoinMarketCap identifies which assets and regimes are worth tracking
- Kamino SDK and Solana RPC validate vault state and execute deposits
Next Steps
- add TVL growth rate tracking to detect vaults gaining inflows
- set alerts for yield_score threshold breaches per asset
- integrate Kamino SDK to pull live vault APYs and utilization rates
- cross-reference CMC momentum signals with on-chain borrow rate changes
- track curator-managed vault rebalancing events via Solana RPC
- store snapshots locally to build rolling yield signal history over time
