Telegram Bot Integration
A step-by-step walkthrough for creating a Telegram bot that queries real data from the SAI protocol. No prior bot experience needed!
What You're About to Build
By the end of this guide, you'll have a working Telegram bot that:
📊 Shows perpetual trades for any wallet address
💰 Displays oracle prices for all tokens
🔍 Lets users search for specific token prices
🔗 Filters trades by asset (BTC only, ETH only, etc.)
📄 Displays results with pagination (Next buttons)
Best part: No database needed! Everything pulls live data from SAI's GraphQL API.
Prerequisites Checklist
Before starting, make sure you have:
✅ Software You'll Need
Python 3.10+ - Download here
Check your version: Open terminal and type
python3 --version
A code editor - Any of these work:
VS Code (recommended for beginners)
PyCharm
Sublime Text
Even Notepad will work!
Terminal/Command Line - You'll need to type a few commands
✅ Accounts You'll Need
Telegram account - To test your bot
A Telegram bot token - We'll get this from BotFather (the official Telegram bot creator tool)
✅ Knowledge Requirements
Basic Python understanding (if-statements, functions, dictionaries)
How to use terminal/command line (navigating folders, running commands)
No GraphQL knowledge needed - We'll explain everything!
💡 Don't have Python installed? This is normal! Follow the download link above and choose your operating system. Python installation is straightforward.
Part 1: Get Your Telegram Bot Token (5 minutes)
Your bot needs a unique token to identify itself to Telegram. Here's how to get one:
Step 1: Open BotFather on Telegram
Open the Telegram app (phone, desktop, or web)
Search for
@BotFatherin the search barClick on it and start a conversation
You should see BotFather respond with a welcome message and commands.
Step 2: Create a New Bot
Send this command:
/newbotBotFather will ask: "Alright! New bot. How are we going to call it? Please choose a name for your bot."
Type something like:
My SAI Trading Bot(this is what appears in Telegram)
BotFather will ask: "Good. Now let's choose a username for your bot. It must end in bot."
Type something like:
my_sai_trading_bot(must end withbot)Example:
sai_price_bot,trading_bot_sai, etc.
Step 3: Copy Your Token
BotFather will send you a message like:
Done! Congratulations on your new bot. You'll find it at t.me/my_sai_trading_bot.
You can now add a description, about section and profile picture for your bot, see /help for a list of commands. By the way, when you've finished creating your bot and it's ready to go public, ping our Bot Support if you'd like a chance to be featured on the @BotStoreBot and the Bot Store within Telegram.
Here's your token:
123456789:ABCdefGHIjklMNOpqrsTUVwxyz⚠️ Important: This token is like your bot's password. Never share it publicly or commit it to GitHub!
Part 2: Prepare Your Computer (10 minutes)
Step 1: Create a Project Folder
Open your terminal/command prompt and create a folder for your project:
mkdir sai-telegram-bot
cd sai-telegram-botWhat this does:
Creates a new folder called
sai-telegram-botMoves you into that folder (you'll work from here)
Step 2: Create a Virtual Environment
A virtual environment is like a sandbox for your project. It keeps your bot's dependencies separate from other Python projects.
On Mac/Linux:
python3 -m venv venv
source venv/bin/activateOn Windows:
python -m venv venv
venv\Scripts\activateWhat should happen:
Your terminal prompt should now show
(venv)at the beginningExample:
(venv) user@computer sai-telegram-bot %
If you see (venv) ✅ - Perfect! You're in the virtual environment.
💡 What's a virtual environment? Think of it like a separate Python installation just for this project. If you install packages here, they won't affect your other projects.
Step 3: Create Your Project Structure
Let's create the folders and files you'll need:
mkdir bot
touch bot/__init__.py
touch bot/main.py
touch bot/graphql.py
touch requirements.txt
touch .env
touch env.exampleWhat this creates:
sai-telegram-bot/
├── venv/ # Virtual environment (created automatically)
├── bot/
│ ├── __init__.py # Makes 'bot' a Python package
│ ├── main.py # Your bot's main code
│ └── graphql.py # Code to talk to SAI's API
├── requirements.txt # List of packages to install
├── .env # Your secret config (token goes here)
└── env.example # Template for .envPart 3: Install Required Packages (2 minutes)
Packages are like plugins that give Python extra abilities. We need several for this bot.
Step 1: Create requirements.txt
requirements.txtOpen your code editor and create a file called requirements.txt with this content:
python-telegram-bot==21.4
requests==2.32.3
python-dotenv==1.0.1
pytz==2024.1What each package does:
python-telegram-bot
Library for creating Telegram bots
requests
Makes HTTP requests to APIs
python-dotenv
Loads secret config from .env file
pytz
Handles timezones
Step 2: Install the Packages
In your terminal (make sure (venv) is showing), type:
pip install -r requirements.txtThis will download and install all the packages. You should see lots of text scrolling by. Wait for it to finish.
Success looks like:
Successfully installed python-telegram-bot-21.4 requests-2.32.3 python-dotenv-1.0.1 pytz-2024.1Part 4: Set Up Your Secret Token (5 minutes)
We'll store your bot token in a special file so it's not exposed in your code.
Step 1: Create env.example
env.exampleThis file shows what secrets are needed (without actual values):
# Telegram Bot Configuration
# Get your bot token from @BotFather on Telegram
TELEGRAM_BOT_TOKEN=your_bot_token_here
# SAI GraphQL Endpoint (the server we get data from)
SAI_GRAPHQL_ENDPOINT=https://sai-keeper.testnet-2.nibiru.fi/queryStep 2: Create .env
.envCopy the example file:
cp env.example .envNow open .env in your editor and replace your_bot_token_here with your actual token from BotFather:
TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyz
SAI_GRAPHQL_ENDPOINT=https://sai-keeper.testnet-2.nibiru.fi/query⚠️ Security note: The .env file should NEVER be shared or committed to GitHub. If you use Git, add .env to your .gitignore file.
Part 5: Build the GraphQL Client (Intermediate)
The "GraphQL client" is code that talks to SAI's data API. Don't worry - we'll explain everything!
What is GraphQL?
GraphQL is a way to ask a server for specific data. Think of it like a restaurant menu:
❌ REST API (old way): "Give me all the data about trades" (you get everything)
✅ GraphQL (new way): "Give me only the trade ID, amount, and price" (you get exactly what you want)
Create bot/graphql.py
bot/graphql.pyThis file handles all communication with SAI's API. Here's the code:
import os
from typing import Any, Dict, List, Optional
import requests
# Get the GraphQL endpoint from .env file, or use default
SAI_GRAPHQL_ENDPOINT = os.environ.get(
"SAI_GRAPHQL_ENDPOINT",
"https://sai-keeper.testnet-2.nibiru.fi/query"
)
class SaiGQLClient:
"""
This class talks to the SAI GraphQL API.
It's like a translator between your bot and SAI's data server.
"""
def __init__(self, endpoint: Optional[str] = None) -> None:
"""
Initialize the client.
Args:
endpoint: URL of the GraphQL server (or None to use default)
"""
self.endpoint = endpoint or SAI_GRAPHQL_ENDPOINT
def query(self, query: str, variables: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""
Send a GraphQL query to the server.
Args:
query: The GraphQL query string (ask what data you want)
variables: Dynamic values to plug into the query
Returns:
Dictionary with the response data
Raises:
RuntimeError if the query fails
"""
# Send POST request (like sending a letter) to GraphQL server
resp = requests.post(
self.endpoint,
json={"query": query, "variables": variables or {}},
timeout=20, # Wait max 20 seconds for response
)
# Check if the HTTP request itself failed
resp.raise_for_status()
# Convert response from JSON format to Python dictionary
try:
payload = resp.json()
except ValueError as e:
# If response wasn't JSON, something went wrong
raise RuntimeError(
f"Invalid JSON response from {self.endpoint}: {resp.text[:200]}"
)
# Check if GraphQL returned an error
if "errors" in payload:
raise RuntimeError(str(payload["errors"]))
# Return just the "data" part of the response
return payload.get("data", {})
def fetch_trades(
self,
trader: str,
is_open: Optional[bool] = None,
limit: int = 100,
base_symbol: Optional[str] = None
) -> List[Dict[str, Any]]:
"""
Get trades for a specific wallet address.
Args:
trader: Wallet address (e.g., "nibiru1abc123...")
is_open:
- True: only open trades
- False: only closed trades
- None: all trades
limit: Max number of trades to return
base_symbol: Filter by token (e.g., "BTC" for Bitcoin only)
Returns:
List of trade dictionaries
"""
# This is a GraphQL query (in plain English: "give me trades")
query = """
query Trades($trader: String!, $isOpen: Boolean, $limit: Int!) {
perp {
trades(
where: { trader: $trader, isOpen: $isOpen }
limit: $limit
order_by: sequence
order_desc: true
) {
id
trader
isOpen
isLong
leverage
openPrice
closePrice
openCollateralAmount
collateralAmount
openBlock { block block_ts }
closeBlock { block block_ts }
state {
positionValue
liquidationPrice
pnlCollateral
pnlPct
}
perpBorrowing {
marketId
baseToken { id name symbol }
quoteToken { id name symbol }
}
}
}
}
"""
# Send the query with the variables
data = self.query(query, {
"trader": trader,
"isOpen": is_open,
"limit": limit
})
# Extract trades from the response
trades = data.get("perp", {}).get("trades", [])
# Filter by symbol if user asked for it (e.g., BTC only)
if base_symbol:
base_symbol_upper = base_symbol.upper()
trades = [
t for t in trades
if (t.get("perpBorrowing", {}).get("baseToken", {}).get("symbol", "") or "").upper() == base_symbol_upper
]
return trades
def fetch_prices(self, limit: int = 200) -> List[Dict[str, Any]]:
"""
Get current prices for all tokens from the oracle.
Args:
limit: Max number of prices to return
Returns:
List of price dictionaries
"""
query = """
query Prices($limit: Int!) {
oracle {
tokenPricesUsd(limit: $limit, order_by: token_id) {
priceUsd
token { id name symbol }
lastUpdatedBlock { block block_ts }
}
}
}
"""
data = self.query(query, {"limit": limit})
return data.get("oracle", {}).get("tokenPricesUsd", [])
def fetch_price_by_symbol(self, symbol: str) -> Optional[Dict[str, Any]]:
"""
Get price for one specific token.
Args:
symbol: Token symbol (e.g., "BTC", "ETH")
Returns:
Price dictionary if found, None if not
"""
prices = self.fetch_prices(limit=200)
symbol_upper = symbol.upper()
for price in prices:
token = price.get("token") or {}
if (token.get("symbol") or "").upper() == symbol_upper:
return price
return NoneKey concepts explained:
class SaiGQLClient:- A class is like a blueprint. It groups related functions together.def query():- The main function that sends queries to the APIfetch_trades():- Gets trade data for a walletfetch_prices():- Gets token pricesString variables like
$trader- These are placeholders that get filled in with real values
Part 6: Create the Bot's Brain (bot/main.py)
bot/main.py)This is where your bot comes alive! We'll build this in sections.
Section 1: Imports and Setup
Create bot/__init__.py (can be empty):
# This file makes 'bot' a Python packageCreate bot/main.py - Start with imports and setup:
import os
import logging
from typing import Optional
from dotenv import load_dotenv
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import (
Application,
CommandHandler,
CallbackQueryHandler,
ContextTypes,
)
from .graphql import SaiGQLClient
# Set up logging (so you can see what your bot is doing)
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)What this does:
Imports libraries we installed earlier
Sets up logging so we can debug issues
Imports our GraphQL client
Section 2: Trade Formatting Functions
Add this to bot/main.py:
def format_trade_open(trade: dict) -> str:
"""
Format an open trade for display in Telegram.
Shows all the important info: position value, profit/loss, etc.
"""
trade_id = trade.get('id', '?')
is_long = trade.get('isLong', False)
side = "🟢 LONG" if is_long else "🔴 SHORT"
# Extract market info (what's being traded)
market = trade.get("perpBorrowing", {})
base_token = market.get("baseToken") or {}
quote_token = market.get("quoteToken") or {}
base = base_token.get("symbol") or base_token.get("name") or "?"
quote = quote_token.get("symbol") or quote_token.get("name") or "?"
# Extract trade details
leverage = trade.get('leverage')
entry_price = trade.get('openPrice')
open_collateral = trade.get('openCollateralAmount')
# Extract state info (for open trades)
state = trade.get('state') or {}
position_value = state.get('positionValue') # How much the position is worth
liquidation_price = state.get('liquidationPrice') # When you get liquidated
pnl = state.get('pnlCollateral') # Profit/Loss in dollars
pnl_pct = state.get('pnlPct') # Profit/Loss in percentage
open_ts = trade.get("openBlock", {}).get("block_ts")
# Build the message
lines = [
f"━━━━━━━━━━━━━━━━",
f"Trade #{trade_id} ✅ OPEN",
f"{base}/{quote} | {side} | {leverage}x leverage",
f"Entry Price: {entry_price}",
]
if liquidation_price:
lines.append(f"Liquidation Price: {liquidation_price}")
# Convert from micro units to dollars
# (API stores values * 1,000,000, we need to divide back)
if position_value is not None:
position_value_usd = position_value / (10 ** 6)
lines.append(f"Position Value: ${position_value_usd:,.2f}")
if pnl is not None:
pnl_usd = pnl / (10 ** 6)
pnl_sign = "+" if pnl_usd >= 0 else ""
lines.append(f"PnL: {pnl_sign}${pnl_usd:,.2f}")
if pnl_pct is not None:
pnl_sign = "+" if pnl_pct >= 0 else ""
lines.append(f"PnL %: {pnl_sign}{pnl_pct:.2f}%")
if open_collateral:
collateral_usd = open_collateral / (10 ** 6)
lines.append(f"Collateral: ${collateral_usd:,.2f}")
if open_ts:
lines.append(f"Opened: {open_ts}")
return "\n".join(lines)
def format_trade_closed(trade: dict) -> str:
"""
Format a closed trade for display.
Shows entry/exit prices and when it was opened/closed.
"""
trade_id = trade.get('id', '?')
is_long = trade.get('isLong', False)
side = "🟢 LONG" if is_long else "🔴 SHORT"
market = trade.get("perpBorrowing", {})
base_token = market.get("baseToken") or {}
quote_token = market.get("quoteToken") or {}
base = base_token.get("symbol") or base_token.get("name") or "?"
quote = quote_token.get("symbol") or quote_token.get("name") or "?"
leverage = trade.get('leverage')
entry_price = trade.get('openPrice')
exit_price = trade.get('closePrice')
open_ts = trade.get("openBlock", {}).get("block_ts")
close_ts = trade.get("closeBlock", {}).get("block_ts")
lines = [
f"━━━━━━━━━━━━━━━━",
f"Trade #{trade_id} ❌ CLOSED",
f"{base}/{quote} | {side} | {leverage}x leverage",
f"Entry Price: {entry_price}",
]
if exit_price:
lines.append(f"Exit Price: {exit_price}")
if open_ts:
lines.append(f"Opened: {open_ts}")
if close_ts:
lines.append(f"Closed: {close_ts}")
return "\n".join(lines)What this does:
format_trade_open()- Displays an open trade with profit/loss infoformat_trade_closed()- Displays a closed trade with entry/exit pricesThe division by
10^6converts from micro-units (how the API stores numbers) to regular dollars
Section 3: Price Formatting Functions
Add this to bot/main.py:
# Popular tokens to show first
POPULAR_TOKENS = ["BTC", "ETH", "USDT", "USDC", "NIBI", "ATOM", "SOL"]
def format_prices(prices: list, start_idx: int = 0, page_size: int = 10) -> tuple:
"""
Format prices for display with pagination (Next buttons).
Args:
prices: List of all prices
start_idx: Which index to start at
page_size: How many prices per page
Returns:
(formatted_text, has_more_pages)
"""
if not prices:
return "No prices found.", False
# Sort prices: popular tokens first, then by ID
def sort_key(p):
token = p.get("token") or {}
symbol = (token.get("symbol") or "").upper()
if symbol in POPULAR_TOKENS:
return (0, POPULAR_TOKENS.index(symbol))
return (1, token.get("id", 9999))
sorted_prices = sorted(prices, key=sort_key)
# Get just this page of prices
end_idx = min(start_idx + page_size, len(sorted_prices))
page_prices = sorted_prices[start_idx:end_idx]
has_more = end_idx < len(sorted_prices)
# Format as text
msg_lines = [f"💰 Oracle Prices ({end_idx} of {len(sorted_prices)})\n"]
for p in page_prices:
token = p.get("token") or {}
symbol = token.get("symbol") or token.get("name") or "Unknown"
price = p.get("priceUsd")
if price is not None:
price_str = f"${price:,.2f}" if price >= 1 else f"${price:.8f}"
msg_lines.append(f"• {symbol}: {price_str}")
return "\n".join(msg_lines), has_more
def format_price_single(price_data: dict) -> str:
"""Format a single price for display."""
token = price_data.get("token") or {}
symbol = token.get("symbol") or token.get("name") or "?"
price = price_data.get("priceUsd")
if price is None:
return f"Price not available for {symbol}"
price_str = f"${price:,.2f}" if price >= 1 else f"${price:.8f}"
return f"💰 {symbol}: {price_str}"What this does:
format_prices()- Formats multiple prices with paginationformat_price_single()- Formats a single price nicelyPopular tokens (BTC, ETH, etc.) appear first
Section 4: Command Handlers
Add this to bot/main.py:
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Handle /start command - shows welcome message."""
text = (
"👋 Welcome to SAI Bot!\n\n"
"Available commands:\n"
"/trades <address> - View trades for a wallet\n"
"/prices - Show all token prices\n"
"/price <symbol> - Get price for one token\n"
"/help - Show this message\n\n"
"Examples:\n"
"/trades nibiru1abc123def456\n"
"/price BTC\n"
"/prices"
)
await update.effective_message.reply_text(text)
async def help_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Handle /help command."""
await start(update, context)What this does:
/startcommand shows welcome message/helpshows the same message
Section 5: The Trades Command
Add this to bot/main.py:
async def trades_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""
Handle /trades command.
Usage:
/trades <address>
/trades <address> open
/trades <address> closed
/trades <address> btc
"""
if not context.args:
await update.effective_message.reply_text(
"Usage: /trades <wallet_address> [open|closed] [symbol]\n\n"
"Examples:\n"
"/trades nibiru1abc123...\n"
"/trades nibiru1abc123... open\n"
"/trades nibiru1abc123... btc"
)
return
address = context.args[0].strip()
is_open = None # None = all trades
symbol = None
# Parse additional arguments
for arg in context.args[1:]:
arg_lower = arg.strip().lower()
if arg_lower == "open":
is_open = True
elif arg_lower == "closed":
is_open = False
else:
symbol = arg.strip().upper()
try:
# Get trades from SAI
gql = SaiGQLClient()
trades = gql.fetch_trades(
trader=address,
is_open=is_open,
limit=100,
base_symbol=symbol
)
if not trades:
await update.effective_message.reply_text(
f"❌ No trades found for {address}"
)
return
# Separate open and closed trades
open_trades = [t for t in trades if t.get('isOpen')]
closed_trades = [t for t in trades if not t.get('isOpen')]
# Show open trades first
if open_trades:
for i, trade in enumerate(open_trades[:5]): # Show first 5
await update.effective_message.reply_text(
format_trade_open(trade)
)
# Show closed trades
if closed_trades:
for i, trade in enumerate(closed_trades[:5]): # Show first 5
await update.effective_message.reply_text(
format_trade_closed(trade)
)
except Exception as e:
logger.error(f"Error in trades_cmd: {e}")
await update.effective_message.reply_text(
f"❌ Error: {str(e)}"
)Section 6: The Prices Commands
Add this to bot/main.py:
async def prices_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Handle /prices command - show all prices."""
try:
gql = SaiGQLClient()
prices = gql.fetch_prices(limit=200)
if not prices:
await update.effective_message.reply_text("No prices found.")
return
# Get first page
text, has_more = format_prices(prices, start_idx=0, page_size=10)
# Add keyboard with "Next" button if there are more prices
reply_markup = None
if has_more:
keyboard = [[InlineKeyboardButton("Next →", callback_data="prices_next")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.effective_message.reply_text(text, reply_markup=reply_markup)
except Exception as e:
logger.error(f"Error in prices_cmd: {e}")
await update.effective_message.reply_text(f"❌ Error: {str(e)}")
async def price_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Handle /price <symbol> command - show single price."""
if not context.args:
await update.effective_message.reply_text(
"Usage: /price <symbol>\n\n"
"Examples: /price BTC, /price ETH"
)
return
symbol = context.args[0].strip()
try:
gql = SaiGQLClient()
price_data = gql.fetch_price_by_symbol(symbol)
if not price_data:
await update.effective_message.reply_text(
f"❌ Token '{symbol}' not found"
)
return
text = format_price_single(price_data)
await update.effective_message.reply_text(text)
except Exception as e:
logger.error(f"Error in price_cmd: {e}")
await update.effective_message.reply_text(f"❌ Error: {str(e)}")Section 7: Callback Handler (for buttons)
Add this to bot/main.py:
async def callback_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Handle button clicks (pagination buttons)."""
query = update.callback_query
if not query:
return
# Acknowledge the button click
await query.answer()
# Handle "Next" button for prices
if query.data == "prices_next":
page = context.user_data.get("prices_page", 0) + 1
context.user_data["prices_page"] = page
try:
gql = SaiGQLClient()
prices = gql.fetch_prices(limit=200)
text, has_more = format_prices(prices, start_idx=page * 10, page_size=10)
reply_markup = None
if has_more:
keyboard = [[InlineKeyboardButton("Next →", callback_data="prices_next")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(text, reply_markup=reply_markup)
except Exception as e:
await query.edit_message_text(f"❌ Error: {str(e)}")Section 8: Start the Bot
Add this to the end of bot/main.py:
def main() -> None:
"""Main function - creates and starts the bot."""
# Load environment variables from .env file
load_dotenv()
# Get bot token
token = os.environ.get("TELEGRAM_BOT_TOKEN")
if not token:
raise RuntimeError("❌ TELEGRAM_BOT_TOKEN not found in .env!")
# Create the bot application
app = Application.builder().token(token).build()
# Register command handlers
app.add_handler(CommandHandler("start", start))
app.add_handler(CommandHandler("help", help_cmd))
app.add_handler(CommandHandler("trades", trades_cmd))
app.add_handler(CommandHandler("prices", prices_cmd))
app.add_handler(CommandHandler("price", price_cmd))
# Register button click handler
app.add_handler(CallbackQueryHandler(callback_handler))
# Start the bot
logger.info("🚀 Bot is starting...")
app.run_polling()
if __name__ == "__main__":
main()Part 7: Test Your Bot! (5 minutes)
Step 1: Activate Virtual Environment
Make sure you're in the project folder and the virtual environment is active:
# You should see (venv) at the start of your terminal prompt
# If not, activate it:
# Mac/Linux:
source venv/bin/activate
# Windows:
venv\Scripts\activateStep 2: Start the Bot
python -m bot.mainYou should see:
INFO:__main__:🚀 Bot is starting...This means your bot is running! Leave this terminal open.
Step 3: Test on Telegram
Open Telegram
Search for your bot (the username you created)
Send
/startYou should see the welcome message!
Try these commands:
/start
Shows welcome message
/help
Shows help message
/prices
Shows all token prices
/price BTC
Shows BTC price
/trades nibiru1abc123...
Shows trades (need real wallet address)
Step 4: Stop the Bot
When you're done testing, press Ctrl+C in the terminal to stop the bot.
Troubleshooting
Problem: "TELEGRAM_BOT_TOKEN not found"
Solution:
Check that
.envfile exists in your project rootOpen it and verify the token is there
Make sure there are no spaces:
TELEGRAM_BOT_TOKEN=123456789:ABC...
Problem: Bot doesn't respond to commands
Solution:
Check that the bot is still running (you should see
🚀 Bot is starting...in terminal)Try the
/startcommand firstCheck for errors in the terminal
Problem: "ModuleNotFoundError: No module named 'bot'"
Solution:
# Make sure you're in the project root directory
cd /path/to/sai-telegram-bot
# Make sure you activated the virtual environment
source venv/bin/activate # Mac/Linux
# or
venv\Scripts\activate # Windows
# Run with -m flag
python -m bot.mainProblem: GraphQL errors or "No prices found"
Solution:
Check your internet connection
Verify the endpoint is correct in
.env:SAI_GRAPHQL_ENDPOINT=https://sai-keeper.testnet-2.nibiru.fi/queryTest the endpoint manually in your browser (visit the URL)
How It All Works Together
Here's the flow when someone uses your bot:
1. User sends: /prices
↓
2. Telegram receives it and forwards to your bot
↓
3. prices_cmd() function is called
↓
4. SaiGQLClient queries the SAI GraphQL API
↓
5. API returns price data
↓
6. format_prices() formats the data beautifully
↓
7. Bot sends the message back to the user
↓
8. User sees prices with a "Next" button
↓
9. User clicks "Next"
↓
10. callback_handler() fetches the next page
↓
11. User sees next page of pricesNext Steps
Your bot is working! Now you can:
Level Up Your Bot
Add price alerts ("Notify me when BTC hits $100,000")
Add wallet monitoring ("Show me when this address opens a trade")
Add more markets or data sources
Deploy to Production
Run your bot 24/7 on a server (AWS, DigitalOcean, etc.)
Use
systemdor Docker to keep it running
Learn More
Congratulations! 🎉
You've built your first Telegram bot that:
✅ Connects to the SAI protocol
✅ Queries real live data
✅ Displays prices and trades
✅ Handles pagination
✅ Filters by asset
You're now a bot developer!
Common Questions
Q: Can I share my bot with others? A: Yes! They can find it by searching your bot's username on Telegram and click "Start".
Q: Will my bot keep running if I close my computer? A: No. You'll need to deploy it to a server to run 24/7. See the "Deploy to Production" section.
Q: How do I update my bot with new features? A: Edit the Python files, then restart the bot (press Ctrl+C and run python -m bot.main again).
Q: Is it safe to share my bot token? A: No! Treat it like a password. Keep it in .env and never commit to GitHub.
Happy coding! 🚀
Last updated