Home › Guides › Telegram /price command
Guide · Telegram · Python · CoinGeckoTelegram /price command with CoinGecko in Python
A /price btc command is one of the most requested features in crypto community groups. This guide builds a robust version with python-telegram-bot v22 and the public CoinGecko API: it understands tickers and names, shows the 24h change, caches results to stay inside rate limits, and answers with a clear message instead of crashing when something goes wrong.
1. The two CoinGecko endpoints you need
CoinGecko identifies coins by an id such as bitcoin or solana, not by ticker – many tickers are used by several tokens. So the command needs two steps:
| Endpoint | Purpose | Example |
|---|---|---|
/api/v3/search?query=… | Find the id for what the user typed | query=sol → solana (plus other matches) |
/api/v3/simple/price | Price and 24h change for an id | ids=bitcoin&vs_currencies=usd&include_24hr_change=true |
Try it in a terminal first:
curl "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd&include_24hr_change=true"
# {"bitcoin":{"usd":77340,"usd_24h_change":-0.44}} (example response, 3 Nov 2026)
The public API works without a key but has a low, shared rate limit. CoinGecko offers a free “Demo” API key (sent as header x-cg-demo-api-key) with its own limits; check CoinGecko’s current plan page and terms before using it in a busy group.
2. Bot token and packages
Create a bot with @BotFather (/newbot) and copy the token. Then:
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install "python-telegram-bot==22.8" "httpx==0.28.1"
httpx is already a dependency of python-telegram-bot; pinning it just makes the version explicit. For commands, the bot can stay in privacy mode – bots always receive commands addressed to them.
3. The complete code
Save as price_bot.py:
"""Telegram /price command with the public CoinGecko API (python-telegram-bot v22)."""
import logging
import math
import os
import time
import httpx
from telegram import Update
from telegram.ext import Application, CommandHandler, ContextTypes
API = "https://api.coingecko.com/api/v3"
API_KEY = os.environ.get("COINGECKO_API_KEY", "") # optional free "Demo" key
CACHE_SECONDS = 60
ALIASES = {"btc": "bitcoin", "eth": "ethereum", "sol": "solana", "usdt": "tether",
"bnb": "binancecoin", "xrp": "ripple", "doge": "dogecoin", "ada": "cardano"}
logging.basicConfig(format="%(asctime)s %(levelname)s %(message)s", level=logging.INFO)
_cache: dict[str, tuple[float, dict]] = {}
async def resolve_coin(client: httpx.AsyncClient, query: str) -> str | None:
"""Turn 'btc', 'Bitcoin' or 'bitcoin' into a CoinGecko id like 'bitcoin'."""
query = query.lower().strip()
if query in ALIASES:
return ALIASES[query]
resp = await client.get("/search", params={"query": query})
resp.raise_for_status()
coins = resp.json().get("coins", [])
for coin in coins: # prefer an exact symbol or id match
if query in (coin["symbol"].lower(), coin["id"]):
return coin["id"]
return coins[0]["id"] if coins else None
async def get_price(client: httpx.AsyncClient, coin_id: str) -> dict | None:
cached = _cache.get(coin_id)
if cached and time.monotonic() - cached[0] < CACHE_SECONDS:
return cached[1]
resp = await client.get("/simple/price", params={
"ids": coin_id, "vs_currencies": "usd", "include_24hr_change": "true"})
resp.raise_for_status()
data = resp.json().get(coin_id)
if data:
_cache[coin_id] = (time.monotonic(), data)
return data
def format_usd(value: float) -> str:
if value >= 1 or value <= 0:
return f"${value:,.2f}"
decimals = 3 - math.floor(math.log10(value)) # keep 4 significant digits
return f"${value:.{decimals}f}".rstrip("0")
async def price(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
if not context.args:
await update.effective_message.reply_text("Usage: /price <coin>, e.g. /price btc")
return
client: httpx.AsyncClient = context.bot_data["http"]
query = " ".join(context.args)
try:
coin_id = await resolve_coin(client, query)
data = await get_price(client, coin_id) if coin_id else None
except httpx.HTTPStatusError as exc:
if exc.response.status_code == 429:
text = "Rate limit reached, please try again in a minute."
else:
text = "The price service returned an error, please try again later."
await update.effective_message.reply_text(text)
return
except httpx.HTTPError:
await update.effective_message.reply_text("Could not reach the price service.")
return
if not data or "usd" not in data:
await update.effective_message.reply_text(f"No price found for '{query}'.")
return
change = data.get("usd_24h_change")
change_text = f" ({change:+.2f}% 24h)" if change is not None else ""
await update.effective_message.reply_text(
f"{coin_id}: {format_usd(data['usd'])}{change_text}\nData: CoinGecko"
)
async def post_init(app: Application) -> None:
headers = {"accept": "application/json"}
if API_KEY:
headers["x-cg-demo-api-key"] = API_KEY
app.bot_data["http"] = httpx.AsyncClient(base_url=API, headers=headers, timeout=10)
async def post_shutdown(app: Application) -> None:
await app.bot_data["http"].aclose()
def main() -> None:
app = (Application.builder().token(os.environ["BOT_TOKEN"])
.post_init(post_init).post_shutdown(post_shutdown).build())
app.add_handler(CommandHandler("price", price))
app.run_polling(allowed_updates=Update.ALL_TYPES)
if __name__ == "__main__":
main()
4. Design details
- Aliases first. Common tickers map straight to ids without an API call, and they avoid surprises: a search for “sol” returns several tokens, and you want Solana.
- Exact match before “first result”. The search result is ranked by CoinGecko; preferring an exact symbol or id match makes
/price pepereturn the token whose symbol is PEPE. - 60-second cache per coin. If ten people type
/price btcin a minute, CoinGecko is asked once. Prices on the free API are not tick-by-tick anyway. - One shared HTTP client with a 10-second timeout, created in
post_initand closed inpost_shutdown. Creating a new client per command is slower and leaks connections. - Error messages instead of stack traces: HTTP 429 (rate limit), other HTTP errors, network errors and unknown coins each get a short reply.
- Small prices stay readable: values below $1 keep four significant digits (e.g.
$0.00000487) instead of rounding to$0.00. - The reply names CoinGecko as the source; CoinGecko’s terms ask for attribution when you display their data.
5. Run and test
export BOT_TOKEN="123456789:AA...your-token..."
export COINGECKO_API_KEY="" # optional Demo key
python price_bot.py
In Telegram, send /price btc, /price Solana or /price pepe to the bot or in a group where it is a member. Example replies from our live test on 3 November 2026 (prices change constantly):
bitcoin: $77,301.00 (-0.50% 24h)
Data: CoinGecko
pepe: $0.00000487 (+1.14% 24h)
Data: CoinGecko
How we tested this code: python-telegram-bot 22.8, httpx 0.28.1, Python 3.13; ruff without findings; the handler was called with a mocked Telegram update against the live CoinGecko API for btc, Solana, pepe and a non-existent coin, plus offline tests for the usage message and the rate-limit reply. We did not run it in a live Telegram chat for this guide.
6. Limits
- Free API data can be delayed by a minute or more and is not suitable for trading decisions.
- Very new or illiquid tokens may be missing or have unreliable prices; a ticker can belong to several tokens – add your community’s token to
ALIASESwith its exact CoinGecko id. - The cache is in memory and per process. That is fine for one bot; it is reset on restart.
- For many groups or heavy use, the shared public limit will not be enough; use an API key and respect CoinGecko’s terms.
Need a full group bot?
The Telegram Group Bot Starter Kit ($19, source code) contains a more complete /price command (configurable currency, Demo or Pro key, own aliases) together with welcome messages, a captcha, anti-spam and admin commands, a Docker setup and 67 offline tests. The code on this page works on its own; you do not need the kit for it.