
Birdeye Api
- 200 installs
- 257 repo stars
- Updated June 24, 2026
- agiprolabs/claude-trading-skills
birdeye-api is a Claude Code skill for fetching Solana token market data (prices, OHLCV, trades, security) from the Birdeye API.
About
This skill documents how to fetch Solana token market data through the Birdeye API, including prices, OHLCV candles, trade history, token metadata, security checks and trader activity. It shows the endpoints, required headers, compute-unit costs and pagination for long history. A developer uses it for Solana token research, historical analysis and sourcing backtesting data.
- Fetches Solana token market data via the Birdeye API
- Covers prices, OHLCV candles, trades, token metadata and security checks
- Documents endpoints, compute-unit costs and pagination
Birdeye Api by the numbers
- 200 all-time installs (skills.sh)
- Ranked #469 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
birdeye-api capabilities & compatibility
Free tier of 30K compute units/month at 1 req/sec; requires a Birdeye API key.
- Capabilities
- market data fetch · token research · ohlcv data · token security check
- Use cases
- trading · data analysis · research
- Pricing
- Bring your own API key
- Requires keys
- BIRDEYE_API_KEY
What birdeye-api says it does
Birdeye aggregates market data across all Solana DEXes (and 13 other chains). Prices, OHLCV candles, trade history, token metadata, security info, and wallet analytics.
Sign up at [birdeye.so](https://birdeye.so) — free tier: 30K compute units/month, 1 req/sec.
npx skills add https://github.com/agiprolabs/claude-trading-skills --skill birdeye-apiAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 200 |
|---|---|
| repo stars | ★ 257 |
| Last updated | June 24, 2026 |
| Repository | agiprolabs/claude-trading-skills ↗ |
What it does
Fetch Solana token prices, OHLCV candles and security checks from the Birdeye API for research and backtesting data.
Who is it for?
Developers pulling historical Solana token data for research, analysis or backtesting.
Skip if: Real-time trading systems, which the skill directs to Yellowstone gRPC instead.
When should I use this skill?
You need Solana token prices, OHLCV candles, metadata or a security check via the Birdeye API.
What you get
Working Birdeye API calls returning Solana prices, candles, metadata and pre-trade security info.
- Birdeye API request code for Solana market data
By the numbers
- Free tier: 30K compute units/month, 1 req/sec
- OHLCV max 1000 candles per request
Files
Birdeye API — Solana Market Data
Birdeye aggregates market data across all Solana DEXes (and 13 other chains). Prices, OHLCV candles, trade history, token metadata, security info, and wallet analytics. Primary data source for Solana token research and historical analysis.
Note: For real-time trading systems, use Yellowstone gRPC (see yellowstone-grpc skill). Birdeye is best for historical data, research, and analysis workflows.
Quick Start
1. Get an API Key
Sign up at birdeye.so — free tier: 30K compute units/month, 1 req/sec.
export BIRDEYE_API_KEY="your-api-key"2. Install Dependencies
uv pip install httpx pandas python-dotenv3. First Request
import httpx, os
API_KEY = os.environ["BIRDEYE_API_KEY"]
headers = {"X-API-KEY": API_KEY, "x-chain": "solana", "accept": "application/json"}
# Get SOL price
resp = httpx.get(
"https://public-api.birdeye.so/defi/price",
headers=headers,
params={"address": "So11111111111111111111111111111111111111112"},
)
price = resp.json()["data"]["value"]
print(f"SOL: ${price}")Core Endpoints
All endpoints use base URL https://public-api.birdeye.so with X-API-KEY and x-chain headers.
Token Price
# Single token price (10 CU)
GET /defi/price?address=TOKEN_MINT
# Multiple token prices (variable CU)
POST /defi/multi_price
Body: {"addresses": ["MINT1", "MINT2", "MINT3"]}
# Price at specific timestamp (10 CU)
GET /defi/historical_price_unix?address=TOKEN_MINT&unixtime=1700000000
# Historical price series (60 CU)
GET /defi/history_price?address=TOKEN_MINT&address_type=token&type=15m&time_from=T1&time_to=T2OHLCV Candles
The primary endpoint for backtesting data.
# Token OHLCV (40 CU, max 1000 candles per request)
GET /defi/ohlcv?address=TOKEN_MINT&type=15m&time_from=T1&time_to=T2Timeframes: 1m, 3m, 5m, 15m, 30m, 1H, 2H, 4H, 6H, 8H, 12H, 1D, 3D, 1W, 1M
Response:
{
"data": {
"items": [
{"o": 23.5, "h": 24.1, "l": 23.2, "c": 23.8, "v": 1234567.89, "unixTime": 1692175200, "type": "15m"}
]
},
"success": true
}Pagination for long history: Max 1000 candles per request. At 15m intervals, 1000 candles ≈ 10.4 days. Slide time_from/time_to windows for longer history.
# Pair OHLCV (40 CU)
GET /defi/ohlcv/pair?address=PAIR_ADDRESS&type=1H&time_from=T1&time_to=T2Token Overview
Comprehensive metadata + market data in one call.
# Token overview (30 CU)
GET /defi/token_overview?address=TOKEN_MINTReturns: name, symbol, decimals, logoURI, liquidity, price, mc (market cap), supply, price changes (30m/1h/2h/4h/6h/8h/12h/24h), trade counts, buy/sell volumes, unique wallets, and social links.
See references/birdeye_endpoints.md for full field listing.
Token Security
Pre-trade safety check — essential before entering any new token.
# Token security (50 CU)
GET /defi/token_security?address=TOKEN_MINTReturns:
creatorAddress,ownerAddress— who controls the tokentop10HolderPercent— concentration riskmutableMetadata— can metadata be changed?freezeable,freezeAuthority— can tokens be frozen?transferFeeEnable— Token-2022 transfer fees?isToken2022— which token program?lockInfo— LP lock details
Quick safety checks:
- Mintable =
ownerAddress != null(supply can increase) - Renounced =
ownerAddress == null(no mint authority) - Mutable =
mutableMetadata == true - Freezeable =
freezeable == true
Trades
# Recent trades for a token (10 CU)
GET /defi/txs/token?address=TOKEN_MINT&limit=50&tx_type=swap
# Trades by time range (15 CU)
GET /defi/txs/token/seek_by_time?address=TOKEN_MINT&after_time=UNIX_TS&limit=50
# WARNING: do NOT pass both before_time and after_time — returns 422Trade response fields: txHash, source (DEX), blockUnixTime, owner (trader), from (token sent), to (token received) with amounts and prices.
Top Traders
# Top traders for a token (30 CU)
GET /defi/v2/tokens/top_traders?address=TOKEN_MINT&time_frame=24h&sort_by=volume&limit=10Returns: owner, volume, volumeBuy, volumeSell, trade, tradeBuy, tradeSell, tags (e.g., "arbitrage-bot", "sniper-bot").
New Listings
# New token listings (80 CU)
GET /defi/v2/tokens/new_listing?time_to=UNIX_TS&limit=10&meme_platform_enabled=trueReturns tokens listed within ~3 days with liquidity ≥ $10. Set meme_platform_enabled=true to include PumpFun tokens.
Trending Tokens
# Trending tokens (50 CU)
GET /defi/token_trending?sort_by=rank&sort_type=asc&limit=20Token Holders
# Token holders (50 CU)
GET /defi/v3/token/holder?address=TOKEN_MINTSearch
# Search tokens/markets (50 CU)
GET /defi/v3/search?keyword=bonk&chain=solanaWallet Endpoints
Rate limited: 30 rpm across all wallet endpoints regardless of tier.
# Wallet portfolio (100 CU)
GET /v1/wallet/token_list?wallet=WALLET_ADDRESS
# Single token balance (5 CU)
GET /v1/wallet/token_balance?wallet=WALLET&token_address=TOKEN_MINT
# Wallet transaction history (150 CU)
GET /v1/wallet/tx_list?wallet=WALLET_ADDRESS
# Wallet net worth (100 CU)
GET /wallet/v2/current-net-worth?wallet=WALLET_ADDRESS
# Wallet PnL (variable CU)
GET /wallet/v2/pnl?wallet=WALLET_ADDRESSWebSocket API
Real-time streaming for price, trades, and new listings. Requires Premium Plus ($250/mo) or higher.
# Connection URL
wss://public-api.birdeye.so/socket/solana?x-api-key=YOUR_KEY
# Required headers
Origin: ws://public-api.birdeye.so
Sec-WebSocket-Protocol: echo-protocol
# Subscribe to price updates
{"type": "SUBSCRIBE_PRICE", "data": {"chartType": "1m", "address": "TOKEN", "currency": "usd"}}
# Subscribe to trades
{"type": "SUBSCRIBE_TXS", "data": {"address": "TOKEN"}}
# Subscribe to new listings
{"type": "SUBSCRIBE_TOKEN_NEW_LISTING", "data": {}}Max 100 tokens per connection. Channels: SUBSCRIBE_PRICE, SUBSCRIBE_TXS, SUBSCRIBE_TOKEN_NEW_LISTING, SUBSCRIBE_NEW_PAIR, SUBSCRIBE_LARGE_TRADE_TXS, SUBSCRIBE_WALLET_TXS, SUBSCRIBE_TOKEN_STATS.
Pricing & CU Budget
| Tier | Price/mo | CU | Rate Limit |
|---|---|---|---|
| Free | $0 | 30K | 1 rps |
| Lite | $39 | 1.5M | 15 rps |
| Starter | $99 | 5M | 15 rps |
| Premium | $199 | 15M | 50 rps |
| Premium Plus | $250 | 20M | 50 rps + WebSocket |
| Business | $499-$2,300 | 50M-500M | 100-150 rps |
CU planning: Free tier (30K CU) = ~750 price calls OR ~375 OHLCV calls/month. Budget accordingly.
Files
References
references/birdeye_endpoints.md— Complete endpoint listing with parameters, CU costs, and response fieldsreferences/error_handling.md— Error codes, rate limits, retry strategies, CU optimizationreferences/token_overview_fields.md— Full field reference for token_overview response
Scripts
scripts/fetch_ohlcv.py— Fetch OHLCV candle data with pagination for backtestingscripts/token_screener.py— Screen tokens using overview, security, and trader data
Birdeye API — Endpoint Reference
Base URL: https://public-api.birdeye.so
Headers required on every request:
X-API-KEY: your-keyx-chain: solana(or ethereum, base, arbitrum, etc.)accept: application/json
Price Endpoints
GET /defi/price — Single Token Price
- Params:
address(required) - CU: 10
- Response:
{ data: { value: 23.44, updateUnixTime: 1692175119, updateHumanTime: "..." }, success: true }
POST /defi/multi_price — Batch Token Prices
- Body:
{ "addresses": ["MINT1", "MINT2"] } - CU: Variable
- More efficient than calling
/defi/pricein a loop
GET /defi/historical_price_unix — Price at Timestamp
- Params:
address,unixtime - CU: 10
GET /defi/history_price — Historical Price Series
- Params:
address,address_type(token),type(timeframe),time_from,time_to - CU: 60
GET /defi/v3/price/stats/single — Price Statistics
- CU: 20
OHLCV Endpoints
GET /defi/ohlcv — Token OHLCV
- Params:
address,type(1m-1M),time_from,time_to - CU: 40
- Max: 1000 candles per request
- Timeframes: 1m, 3m, 5m, 15m, 30m, 1H, 2H, 4H, 6H, 8H, 12H, 1D, 3D, 1W, 1M
- Response item:
{ o, h, l, c, v, unixTime, type, address }
GET /defi/ohlcv/pair — Pair OHLCV
- Params:
address(pair address),type,time_from,time_to - CU: 40
GET /defi/ohlcv/base_quote — Base/Quote OHLCV
- CU: 40
Token Data Endpoints
GET /defi/token_overview — Token Overview
- Params:
address - CU: 30
- Key fields: See
token_overview_fields.mdfor complete list
GET /defi/token_security — Security Info
- Params:
address - CU: 50
- Key fields:
creatorAddress,ownerAddress,top10HolderPercent,mutableMetadata,freezeable,freezeAuthority,transferFeeEnable,isToken2022,lockInfo,totalSupply,creationTx,creationTime
GET /defi/token_creation_info — Creation Details
- Params:
address - CU: 80
GET /defi/token_trending — Trending Tokens
- Params:
sort_by(rank/liquidity/volume24hUSD),sort_type(asc/desc),offset,limit - CU: 50
- Response item:
{ address, decimals, liquidity, logoURI, name, symbol, volume24hUSD, rank }
GET /defi/v2/tokens/new_listing — New Listings
- Params:
time_to(required),limit(1-10),meme_platform_enabled(boolean, Solana only) - CU: 80
- Response item:
{ address, symbol, name, decimals, liquidityAddedAt, liquidity }
GET /defi/v3/token/holder — Token Holders
- Params:
address - CU: 50
GET /defi/v3/token/exit-liquidity — Exit Liquidity
- Params:
address - CU: 30
GET /defi/v3/token/meta-data/single — Token Metadata
- CU: 5
GET /defi/v3/token/market-data — Market Data
- CU: 15
GET /defi/v3/search — Search
- Params:
keyword,chain - CU: 50
Trade Endpoints
GET /defi/txs/token — Recent Token Trades
- Params:
address,offset(0-1000),limit(1-50),tx_type(swap/add/remove/all) - CU: 10
- Response item:
{ txHash, source, blockUnixTime, address, owner, from: {symbol, decimals, amount, uiAmount, price, nearestPrice}, to: {...} }
GET /defi/txs/token/seek_by_time — Token Trades by Time
- Params:
address,before_timeORafter_time(NOT both),offset,limit,tx_type - CU: 15
- Pagination: Use
data.hasNextboolean
GET /defi/txs/pair — Pair Trades
- Params: Same as token trades but with pair address
- CU: 10
GET /defi/txs/pair/seek_by_time — Pair Trades by Time
- CU: 15
GET /defi/v3/token/txs — Token Trades (v3)
- CU: 20
Trader Endpoints
GET /defi/v2/tokens/top_traders — Top Traders
- Params:
address,sort_by(volume/trade),sort_type,time_frame(30m-24h),offset,limit(1-10) - CU: 30
- Response item:
{ owner, volume, volumeBuy, volumeSell, trade, tradeBuy, tradeSell, tags } - Tags:
"arbitrage-bot","sniper-bot", etc.
GET /trader/gainers-losers — Top Gainers/Losers
- CU: 30
GET /trader/txs/seek_by_time — Trader Trades by Time
- CU: 15
Wallet Endpoints (30 rpm limit)
GET /v1/wallet/token_list — Portfolio
- Params:
wallet - CU: 100
- Response:
{ wallet, totalUsd, items: [{ address, symbol, name, balance, uiAmount, priceUsd, valueUsd }] }
GET /v1/wallet/token_balance — Token Balance
- Params:
wallet,token_address - CU: 5
GET /v1/wallet/tx_list — Transaction History
- Params:
wallet - CU: 150
GET /wallet/v2/current-net-worth — Net Worth
- CU: 100
GET /wallet/v2/pnl — PnL
- CU: Variable
Pair Endpoints
GET /defi/v3/pair/overview/single — Pair Overview
- CU: 20
GET /defi/v2/markets — All Markets for Token
- CU: 50
Birdeye API — Error Handling & Rate Limits
Error Responses
All errors return: { "success": false, "message": "..." }
| Code | Meaning | Action |
|---|---|---|
| 400 | Bad Request | Check parameter format |
| 401 | Unauthorized | Invalid or missing API key |
| 403 | Forbidden | Tier lacks access to endpoint |
| 422 | Unprocessable Entity | Invalid param combination |
| 429 | Too Many Requests | Rate limit — back off |
| 500 | Internal Server Error | Retry with backoff |
Common 422 Causes
- Passing both
before_timeandafter_timeto seek_by_time endpoints - Invalid token address format
- Timeframe not supported for chain
Rate Limits
Limits are per account (not per key or endpoint).
| Tier | Requests/sec | Notes |
|---|---|---|
| Free | 1 | Very restrictive |
| Lite/Starter | 15 | Adequate for research |
| Premium | 50 (1000 rpm) | Most use cases |
| Business | 100-150 | Production workloads |
Wallet endpoints: Fixed at 30 rpm regardless of tier, across all 7 wallet endpoints.
No rate limit headers are returned in responses — you must track your own usage.
Retry Strategy
import httpx
import time
import random
def birdeye_request(
endpoint: str,
params: dict,
api_key: str,
chain: str = "solana",
max_retries: int = 3,
) -> dict:
"""Make a Birdeye API request with retry logic.
Args:
endpoint: API path (e.g., '/defi/price').
params: Query parameters.
api_key: Birdeye API key.
chain: Chain identifier.
max_retries: Max retry attempts.
Returns:
Parsed response data (the 'data' field).
Raises:
RuntimeError: On persistent failure.
"""
headers = {
"X-API-KEY": api_key,
"x-chain": chain,
"accept": "application/json",
}
url = f"https://public-api.birdeye.so{endpoint}"
for attempt in range(max_retries + 1):
try:
resp = httpx.get(url, headers=headers, params=params, timeout=30.0)
if resp.status_code == 429:
delay = (2 ** attempt) + random.uniform(0, 1)
print(f"Rate limited. Retrying in {delay:.1f}s...")
time.sleep(delay)
continue
if resp.status_code == 403:
raise RuntimeError(
f"Forbidden: tier lacks access to {endpoint}. "
"Upgrade at birdeye.so"
)
resp.raise_for_status()
data = resp.json()
if not data.get("success"):
raise RuntimeError(f"Birdeye error: {data.get('message', 'unknown')}")
return data["data"]
except httpx.TimeoutException:
if attempt < max_retries:
time.sleep(2 ** attempt)
continue
raise
raise RuntimeError(f"Max retries exceeded for {endpoint}")CU Optimization Tips
Use Multi-Price Instead of Loops
# Bad: 10 CU per call × 20 tokens = 200 CU
for token in tokens:
price = get_price(token)
# Good: Variable CU for batch
prices = get_multi_price(tokens) # single callCache Token Overview
token_overview (30 CU) returns price, volume, trades, wallets — often enough without needing separate calls.
Use Appropriate Timeframes
For backtesting, choose the coarsest timeframe that meets your needs:
- 1D candles: 1000 candles = 2.7 years → 1 call (40 CU)
- 1H candles: 1000 candles = 41.6 days → ~9 calls for 1 year (360 CU)
- 15m candles: 1000 candles = 10.4 days → ~35 calls for 1 year (1400 CU)
Free Tier Budget (30K CU/month)
| Use Case | Calls | CU Used |
|---|---|---|
| 750 price checks | 750 | 7,500 |
| 100 OHLCV fetches | 100 | 4,000 |
| 50 token overviews | 50 | 1,500 |
| 20 security checks | 20 | 1,000 |
| Total | 920 | 14,000 |
Starter Tier Budget (5M CU/month)
Enough for ~125K price calls or ~12.5K OHLCV fetches. Comfortable for daily research workflows.
Pagination Patterns
OHLCV Pagination (Time-Window Sliding)
import time
def fetch_all_ohlcv(
address: str,
timeframe: str,
start_ts: int,
end_ts: int,
api_key: str,
) -> list[dict]:
"""Fetch complete OHLCV history by paginating time windows."""
all_candles = []
current_start = start_ts
# Seconds per candle (approximate)
tf_seconds = {
"1m": 60, "5m": 300, "15m": 900, "30m": 1800,
"1H": 3600, "4H": 14400, "1D": 86400, "1W": 604800,
}
window = tf_seconds.get(timeframe, 3600) * 1000 # 1000 candles
while current_start < end_ts:
current_end = min(current_start + window, end_ts)
data = birdeye_request(
"/defi/ohlcv",
{"address": address, "type": timeframe,
"time_from": current_start, "time_to": current_end},
api_key,
)
items = data.get("items", [])
all_candles.extend(items)
if not items:
break
current_start = items[-1]["unixTime"] + 1
time.sleep(0.1) # respect rate limits
return all_candlesTrade Pagination (Time-Based)
def fetch_trades_after(
address: str, after_time: int, api_key: str, max_pages: int = 10
) -> list[dict]:
"""Fetch trades after a timestamp, paginating forward."""
all_trades = []
current_time = after_time
for _ in range(max_pages):
data = birdeye_request(
"/defi/txs/token/seek_by_time",
{"address": address, "after_time": current_time,
"limit": 50, "tx_type": "swap"},
api_key,
)
items = data.get("items", [])
all_trades.extend(items)
if not data.get("hasNext") or not items:
break
current_time = items[-1]["blockUnixTime"]
time.sleep(0.1)
return all_tradesBirdeye Token Overview — Field Reference
Response from GET /defi/token_overview?address=TOKEN_MINT (30 CU).
Core Fields
| Field | Type | Description |
|---|---|---|
address | string | Token mint address |
decimals | int | Token decimals |
symbol | string | Token symbol |
name | string | Token name |
logoURI | string | Token logo URL |
liquidity | float | Total liquidity across all pools (USD) |
price | float | Current price (USD) |
supply | float | Circulating supply |
mc | float | Market cap (USD) |
numberMarkets | int | Number of DEX markets |
lastTradeUnixTime | int | Last trade timestamp |
lastTradeHumanTime | string | Last trade human-readable |
Social / Extension Fields
| Field | Type | Description |
|---|---|---|
coingeckoId | string | CoinGecko ID |
website | string | Project website |
twitter | string | Twitter/X URL |
discord | string | Discord URL |
telegram | string | Telegram URL |
medium | string | Medium URL |
description | string | Token description |
Price Change Fields (per interval)
For each interval (30m, 1h, 2h, 4h, 6h, 8h, 12h, 24h):
| Field Pattern | Example (1h) | Description |
|---|---|---|
history{X}Price | history1hPrice | Price X ago |
priceChange{X}Percent | priceChange1hPercent | % change over X |
Trading Metrics (per interval)
For each interval (30m, 1h, 2h, 4h, 6h, 8h, 12h, 24h):
| Field Pattern | Example (24h) | Description |
|---|---|---|
trade{X} | trade24h | Total trades in period |
tradeHistory{X} | tradeHistory24h | Trades in previous period |
trade{X}ChangePercent | trade24hChangePercent | Trade count % change |
buy{X} | buy24h | Buy trades |
sell{X} | sell24h | Sell trades |
v{X} | v24h | Volume (native) |
v{X}USD | v24hUSD | Volume (USD) |
vBuy{X} | vBuy24h | Buy volume (native) |
vSell{X} | vSell24h | Sell volume (native) |
vBuy{X}USD | vBuy24hUSD | Buy volume (USD) |
vSell{X}USD | vSell24hUSD | Sell volume (USD) |
Unique Wallet Metrics (per interval)
| Field Pattern | Example (24h) | Description |
|---|---|---|
uniqueWallet{X} | uniqueWallet24h | Unique wallets in period |
uniqueWalletHistory{X} | uniqueWalletHistory24h | Previous period |
uniqueWallet{X}ChangePercent | uniqueWallet24hChangePercent | % change |
Using Token Overview for Analysis
Quick Health Check
overview = fetch_token_overview(mint)
# Liquidity check
if overview["liquidity"] < 10_000:
print("LOW LIQUIDITY — dangerous")
# Volume check
buy_sell_ratio = overview.get("vBuy24hUSD", 0) / max(overview.get("vSell24hUSD", 1), 1)
# Wallet diversity
unique_wallets = overview.get("uniqueWallet24h", 0)
# Momentum
price_change_1h = overview.get("priceChange1hPercent", 0)
price_change_24h = overview.get("priceChange24hPercent", 0)
# Trade activity
trades_24h = overview.get("trade24h", 0)Compare Current vs Historical Activity
# Is activity increasing or decreasing?
trade_change = overview.get("trade24hChangePercent", 0)
wallet_change = overview.get("uniqueWallet24hChangePercent", 0)
volume_change_pct = (
(overview.get("v24hUSD", 0) - overview.get("vHistory24hUSD", 0))
/ max(overview.get("vHistory24hUSD", 1), 1)
* 100
)
if trade_change > 50 and wallet_change > 50:
print("Increasing interest — new wallet inflow")
elif trade_change < -50:
print("Declining activity — interest fading")Multi-Timeframe Analysis
# Compare short-term vs long-term price action
pct_30m = overview.get("priceChange30mPercent", 0)
pct_1h = overview.get("priceChange1hPercent", 0)
pct_4h = overview.get("priceChange4hPercent", 0)
pct_24h = overview.get("priceChange24hPercent", 0)
# Accelerating momentum
if pct_30m > pct_1h > 0:
print("Accelerating upward momentum")
# Reverting from pump
if pct_30m < 0 and pct_4h > 50:
print("Reverting after pump — caution")#!/usr/bin/env python3
"""Fetch OHLCV candle data from Birdeye with automatic pagination.
Retrieves historical price candles for any Solana token and outputs as
a pandas DataFrame suitable for backtesting. Handles the 1000-candle
per-request limit by sliding the time window automatically.
Usage:
python scripts/fetch_ohlcv.py
TOKEN_ADDRESS="EPjFWdd5..." TIMEFRAME="1H" python scripts/fetch_ohlcv.py
Dependencies:
uv pip install httpx pandas python-dotenv
Environment Variables:
BIRDEYE_API_KEY: Your Birdeye API key
TOKEN_ADDRESS: Token mint address (default: SOL)
TIMEFRAME: Candle interval (default: 1H)
DAYS_BACK: How many days of history to fetch (default: 30)
"""
import os
import sys
import time
from datetime import datetime, timezone, timedelta
from typing import Optional
import httpx
import pandas as pd
# ── Configuration ───────────────────────────────────────────────────
API_KEY = os.getenv("BIRDEYE_API_KEY", "")
if not API_KEY:
print("Set BIRDEYE_API_KEY environment variable")
print(" Get a key at https://birdeye.so")
sys.exit(1)
TOKEN_ADDRESS = os.getenv(
"TOKEN_ADDRESS", "So11111111111111111111111111111111111111112"
)
TIMEFRAME = os.getenv("TIMEFRAME", "1H")
DAYS_BACK = int(os.getenv("DAYS_BACK", "30"))
BASE_URL = "https://public-api.birdeye.so"
HEADERS = {
"X-API-KEY": API_KEY,
"x-chain": "solana",
"accept": "application/json",
}
# Seconds per candle for each timeframe
TIMEFRAME_SECONDS = {
"1m": 60, "3m": 180, "5m": 300, "15m": 900, "30m": 1800,
"1H": 3600, "2H": 7200, "4H": 14400, "6H": 21600,
"8H": 28800, "12H": 43200, "1D": 86400, "3D": 259200,
"1W": 604800, "1M": 2592000,
}
# Rate limit: pause between requests
RATE_LIMIT_DELAY = 0.2 # 5 rps (conservative for free tier)
# ── API Functions ───────────────────────────────────────────────────
def fetch_ohlcv_page(
address: str,
timeframe: str,
time_from: int,
time_to: int,
) -> list[dict]:
"""Fetch a single page of OHLCV data (max 1000 candles).
Args:
address: Token mint address.
timeframe: Candle interval (e.g., '1H', '15m', '1D').
time_from: Start unix timestamp.
time_to: End unix timestamp.
Returns:
List of candle dicts with o, h, l, c, v, unixTime fields.
Raises:
httpx.HTTPStatusError: On API error.
RuntimeError: On Birdeye-level error.
"""
resp = httpx.get(
f"{BASE_URL}/defi/ohlcv",
headers=HEADERS,
params={
"address": address,
"type": timeframe,
"time_from": time_from,
"time_to": time_to,
},
timeout=30.0,
)
if resp.status_code == 429:
print(" Rate limited — waiting 5s...")
time.sleep(5.0)
return fetch_ohlcv_page(address, timeframe, time_from, time_to)
resp.raise_for_status()
data = resp.json()
if not data.get("success"):
raise RuntimeError(f"Birdeye error: {data.get('message', 'unknown')}")
return data.get("data", {}).get("items", [])
def fetch_full_ohlcv(
address: str,
timeframe: str,
start_time: int,
end_time: int,
) -> pd.DataFrame:
"""Fetch complete OHLCV history with automatic pagination.
Handles the 1000-candle limit by sliding the time window forward.
Args:
address: Token mint address.
timeframe: Candle interval.
start_time: Start unix timestamp.
end_time: End unix timestamp.
Returns:
DataFrame with columns: timestamp, open, high, low, close, volume.
"""
tf_secs = TIMEFRAME_SECONDS.get(timeframe)
if tf_secs is None:
raise ValueError(
f"Unknown timeframe '{timeframe}'. "
f"Valid: {', '.join(TIMEFRAME_SECONDS.keys())}"
)
window_secs = tf_secs * 999 # slightly under 1000 to avoid edge cases
all_candles: list[dict] = []
current_start = start_time
page = 0
while current_start < end_time:
current_end = min(current_start + window_secs, end_time)
page += 1
print(
f" Page {page}: "
f"{datetime.fromtimestamp(current_start, tz=timezone.utc).strftime('%Y-%m-%d %H:%M')} → "
f"{datetime.fromtimestamp(current_end, tz=timezone.utc).strftime('%Y-%m-%d %H:%M')}",
end="",
)
candles = fetch_ohlcv_page(address, timeframe, current_start, current_end)
print(f" — {len(candles)} candles")
if not candles:
# No data in this window, skip forward
current_start = current_end + 1
time.sleep(RATE_LIMIT_DELAY)
continue
all_candles.extend(candles)
# Move start to after the last candle we received
last_time = max(c["unixTime"] for c in candles)
current_start = last_time + 1
time.sleep(RATE_LIMIT_DELAY)
if not all_candles:
print("No candle data returned.")
return pd.DataFrame()
# Build DataFrame
df = pd.DataFrame(all_candles)
df = df.rename(columns={
"o": "open", "h": "high", "l": "low", "c": "close", "v": "volume",
})
df["timestamp"] = pd.to_datetime(df["unixTime"], unit="s", utc=True)
df = df[["timestamp", "open", "high", "low", "close", "volume"]]
df = df.drop_duplicates(subset=["timestamp"])
df = df.sort_values("timestamp").reset_index(drop=True)
return df
# ── Analysis ────────────────────────────────────────────────────────
def print_summary(df: pd.DataFrame, symbol: str) -> None:
"""Print a summary of the fetched OHLCV data.
Args:
df: OHLCV DataFrame.
symbol: Token symbol for display.
"""
if df.empty:
print("No data to summarize.")
return
print(f"\n{'='*60}")
print(f"OHLCV Summary: {symbol}")
print(f"{'='*60}")
print(f" Period: {df['timestamp'].iloc[0]} → {df['timestamp'].iloc[-1]}")
print(f" Candles: {len(df)}")
print(f" Timeframe: {TIMEFRAME}")
print(f" Open: ${df['open'].iloc[0]:.4f}")
print(f" Close: ${df['close'].iloc[-1]:.4f}")
print(f" High: ${df['high'].max():.4f}")
print(f" Low: ${df['low'].min():.4f}")
change = (df["close"].iloc[-1] / df["open"].iloc[0] - 1) * 100
print(f" Change: {change:+.2f}%")
print(f" Avg Volume: ${df['volume'].mean():,.0f}")
print(f" Total Vol: ${df['volume'].sum():,.0f}")
# Check for gaps
if len(df) > 1:
expected_diff = pd.Timedelta(seconds=TIMEFRAME_SECONDS[TIMEFRAME])
actual_diffs = df["timestamp"].diff().dropna()
gaps = actual_diffs[actual_diffs > expected_diff * 1.5]
if len(gaps) > 0:
print(f" Gaps: {len(gaps)} (max: {gaps.max()})")
else:
print(f" Gaps: None")
# ── Main ────────────────────────────────────────────────────────────
def main() -> None:
"""Fetch OHLCV data and print summary."""
now = datetime.now(tz=timezone.utc)
end_time = int(now.timestamp())
start_time = int((now - timedelta(days=DAYS_BACK)).timestamp())
print(f"Token: {TOKEN_ADDRESS}")
print(f"Timeframe: {TIMEFRAME}")
print(f"Period: {DAYS_BACK} days")
print(f"Fetching OHLCV data...\n")
df = fetch_full_ohlcv(TOKEN_ADDRESS, TIMEFRAME, start_time, end_time)
if df.empty:
print("No data returned. Check the token address and try again.")
sys.exit(1)
# Get token symbol for display
try:
resp = httpx.get(
f"{BASE_URL}/defi/token_overview",
headers=HEADERS,
params={"address": TOKEN_ADDRESS},
timeout=10.0,
)
symbol = resp.json().get("data", {}).get("symbol", TOKEN_ADDRESS[:8])
except Exception:
symbol = TOKEN_ADDRESS[:8]
print_summary(df, symbol)
# Save to CSV
csv_path = f"ohlcv_{symbol}_{TIMEFRAME}_{DAYS_BACK}d.csv"
df.to_csv(csv_path, index=False)
print(f"\nSaved to {csv_path}")
print(f"Load with: df = pd.read_csv('{csv_path}', parse_dates=['timestamp'])")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Screen Solana tokens using Birdeye overview, security, and trader data.
Fetches token overview, security info, and top trader data to generate a
screening report. Flags potential risks (mintable, freezeable, high
concentration) and highlights trading activity metrics.
Usage:
python scripts/token_screener.py
TOKEN_ADDRESS="TokenMint..." python scripts/token_screener.py
Dependencies:
uv pip install httpx python-dotenv
Environment Variables:
BIRDEYE_API_KEY: Your Birdeye API key
TOKEN_ADDRESS: Token mint to screen (default: SOL)
"""
import os
import sys
import time
from typing import Optional
import httpx
# ── Configuration ───────────────────────────────────────────────────
API_KEY = os.getenv("BIRDEYE_API_KEY", "")
if not API_KEY:
print("Set BIRDEYE_API_KEY environment variable")
sys.exit(1)
TOKEN_ADDRESS = os.getenv(
"TOKEN_ADDRESS", "So11111111111111111111111111111111111111112"
)
BASE_URL = "https://public-api.birdeye.so"
HEADERS = {
"X-API-KEY": API_KEY,
"x-chain": "solana",
"accept": "application/json",
}
RATE_LIMIT_DELAY = 1.0 # conservative for free tier
# ── API Helper ──────────────────────────────────────────────────────
def birdeye_get(endpoint: str, params: Optional[dict] = None) -> dict:
"""Make a GET request to Birdeye API with retry.
Args:
endpoint: API path (e.g., '/defi/token_overview').
params: Query parameters.
Returns:
The 'data' field from the response.
Raises:
RuntimeError: On API error.
"""
for attempt in range(3):
try:
resp = httpx.get(
f"{BASE_URL}{endpoint}",
headers=HEADERS,
params=params or {},
timeout=30.0,
)
if resp.status_code == 429:
time.sleep(3.0 * (attempt + 1))
continue
if resp.status_code == 403:
return {} # tier doesn't have access, skip gracefully
resp.raise_for_status()
data = resp.json()
if not data.get("success"):
return {}
return data.get("data", {})
except httpx.TimeoutException:
if attempt < 2:
time.sleep(2.0)
continue
raise
return {}
# ── Data Fetching ───────────────────────────────────────────────────
def fetch_overview(address: str) -> dict:
"""Fetch token overview (30 CU)."""
return birdeye_get("/defi/token_overview", {"address": address})
def fetch_security(address: str) -> dict:
"""Fetch token security info (50 CU)."""
return birdeye_get("/defi/token_security", {"address": address})
def fetch_top_traders(address: str, timeframe: str = "24h") -> list[dict]:
"""Fetch top traders for a token (30 CU)."""
data = birdeye_get("/defi/v2/tokens/top_traders", {
"address": address,
"time_frame": timeframe,
"sort_by": "volume",
"sort_type": "desc",
"limit": 10,
})
return data.get("items", []) if isinstance(data, dict) else []
# ── Analysis ────────────────────────────────────────────────────────
def analyze_security(security: dict) -> list[str]:
"""Analyze security info and return risk flags.
Args:
security: Token security response data.
Returns:
List of risk flag strings.
"""
flags = []
if not security:
flags.append("[?] Security data unavailable")
return flags
# Mint authority
owner = security.get("ownerAddress")
if owner:
flags.append(f"[!] MINTABLE — owner: {owner[:16]}...")
else:
flags.append("[ok] Not mintable (owner renounced)")
# Freeze authority
if security.get("freezeable"):
freeze_auth = security.get("freezeAuthority", "unknown")
flags.append(f"[!] FREEZEABLE — authority: {freeze_auth[:16]}...")
else:
flags.append("[ok] Not freezeable")
# Metadata mutability
if security.get("mutableMetadata"):
flags.append("[!] Metadata is mutable")
else:
flags.append("[ok] Metadata immutable")
# Holder concentration
top10 = security.get("top10HolderPercent", 0)
if top10 > 80:
flags.append(f"[!!] EXTREME CONCENTRATION — top 10 hold {top10:.1f}%")
elif top10 > 50:
flags.append(f"[!] High concentration — top 10 hold {top10:.1f}%")
elif top10 > 0:
flags.append(f"[ok] Top 10 hold {top10:.1f}%")
# Creator balance
creator_pct = security.get("creatorBalance", 0)
if creator_pct and float(str(creator_pct)) > 10:
flags.append(f"[!] Creator holds significant balance")
# Token-2022
if security.get("isToken2022"):
flags.append("[i] Token-2022 program")
if security.get("transferFeeEnable"):
flags.append("[!] Transfer fees enabled")
if security.get("nonTransferable"):
flags.append("[!!] Non-transferable token")
return flags
def analyze_traders(traders: list[dict]) -> dict:
"""Analyze top trader data.
Args:
traders: List of top trader dicts.
Returns:
Analysis summary dict.
"""
if not traders:
return {"total_traders": 0}
total_volume = sum(t.get("volume", 0) for t in traders)
bot_count = sum(1 for t in traders if t.get("tags"))
bot_volume = sum(t.get("volume", 0) for t in traders if t.get("tags"))
return {
"total_traders": len(traders),
"total_volume_top10": total_volume,
"bot_count": bot_count,
"bot_volume_pct": round(bot_volume / total_volume * 100, 1) if total_volume > 0 else 0,
"top_trader_volume": traders[0].get("volume", 0) if traders else 0,
"top_trader_address": traders[0].get("owner", "?")[:16] + "..." if traders else "N/A",
"bot_tags": list(set(
tag for t in traders for tag in (t.get("tags") or [])
)),
}
# ── Display ─────────────────────────────────────────────────────────
def format_usd(value: float) -> str:
"""Format a USD value for display."""
if value >= 1_000_000_000:
return f"${value / 1e9:.2f}B"
elif value >= 1_000_000:
return f"${value / 1e6:.2f}M"
elif value >= 1_000:
return f"${value / 1e3:.2f}K"
return f"${value:.2f}"
def print_report(
address: str,
overview: dict,
security_flags: list[str],
trader_analysis: dict,
) -> None:
"""Print formatted screening report."""
name = overview.get("name", "Unknown")
symbol = overview.get("symbol", "?")
print(f"\n{'='*60}")
print(f"TOKEN SCREENING: {symbol} ({name})")
print(f"{'='*60}")
print(f" Mint: {address}")
# Market data
print(f"\n--- Market Data ---")
price = overview.get("price", 0)
print(f" Price: ${price:.6f}" if price < 1 else f" Price: ${price:.2f}")
print(f" Market Cap: {format_usd(overview.get('mc', 0))}")
print(f" Liquidity: {format_usd(overview.get('liquidity', 0))}")
print(f" Markets: {overview.get('numberMarkets', 0)}")
# Price changes
print(f"\n--- Price Changes ---")
for tf in ["30m", "1h", "4h", "24h"]:
pct = overview.get(f"priceChange{tf.replace('m', 'm').replace('h', 'h')}Percent", 0)
key = f"priceChange{tf}Percent"
# Try various key formats
for k in [f"priceChange{tf}Percent", f"priceChange{tf.upper()}Percent"]:
if k in overview:
pct = overview[k]
break
print(f" {tf:>4}: {pct:+.2f}%")
# Volume & activity
print(f"\n--- Activity (24h) ---")
print(f" Volume: {format_usd(overview.get('v24hUSD', 0))}")
print(f" Trades: {overview.get('trade24h', 0):,}")
print(f" Buys: {overview.get('buy24h', 0):,}")
print(f" Sells: {overview.get('sell24h', 0):,}")
buy_vol = overview.get("vBuy24hUSD", 0)
sell_vol = overview.get("vSell24hUSD", 0)
if buy_vol and sell_vol:
ratio = buy_vol / sell_vol if sell_vol > 0 else float("inf")
print(f" Buy/Sell Vol: {ratio:.2f}x")
print(f" Unique Wallets: {overview.get('uniqueWallet24h', 0):,}")
# Security
print(f"\n--- Security Check ---")
for flag in security_flags:
print(f" {flag}")
# Top traders
if trader_analysis.get("total_traders", 0) > 0:
print(f"\n--- Top Traders (24h) ---")
print(f" Top 10 volume: {format_usd(trader_analysis['total_volume_top10'])}")
print(f" #1 trader: {trader_analysis['top_trader_address']}")
print(f" Bots detected: {trader_analysis['bot_count']}/{trader_analysis['total_traders']}")
print(f" Bot volume: {trader_analysis['bot_volume_pct']}% of top 10")
if trader_analysis["bot_tags"]:
print(f" Bot types: {', '.join(trader_analysis['bot_tags'])}")
# Overall assessment
print(f"\n--- Assessment ---")
risks = sum(1 for f in security_flags if f.startswith("[!]") or f.startswith("[!!]"))
liq = overview.get("liquidity", 0)
if any(f.startswith("[!!]") for f in security_flags):
print(" RISK: HIGH — critical security flags detected")
elif risks >= 3:
print(" RISK: ELEVATED — multiple risk factors")
elif risks >= 1:
print(" RISK: MODERATE — some risk factors present")
else:
print(" RISK: LOW — no major flags")
if liq < 10_000:
print(" LIQUIDITY: DANGEROUSLY LOW (<$10K)")
elif liq < 50_000:
print(" LIQUIDITY: LOW (<$50K)")
elif liq < 250_000:
print(" LIQUIDITY: MODERATE")
else:
print(" LIQUIDITY: ADEQUATE")
# ── Main ────────────────────────────────────────────────────────────
def main() -> None:
"""Run token screening report."""
print(f"Screening token: {TOKEN_ADDRESS}")
# Fetch data (total: ~110 CU)
print("Fetching overview...")
overview = fetch_overview(TOKEN_ADDRESS)
time.sleep(RATE_LIMIT_DELAY)
print("Fetching security...")
security = fetch_security(TOKEN_ADDRESS)
time.sleep(RATE_LIMIT_DELAY)
print("Fetching top traders...")
traders = fetch_top_traders(TOKEN_ADDRESS)
if not overview:
print("Could not fetch token data. Check the address and API key.")
sys.exit(1)
# Analyze
security_flags = analyze_security(security)
trader_analysis = analyze_traders(traders)
# Report
print_report(TOKEN_ADDRESS, overview, security_flags, trader_analysis)
if __name__ == "__main__":
main()
Related skills
FAQ
What data can it fetch?
Prices, OHLCV candles, trade history, token metadata, security info and wallet/trader analytics across Solana DEXes.
Is it good for real-time trading?
No - the skill recommends Yellowstone gRPC for real-time systems and positions Birdeye for historical data, research and analysis.