Build Your First Sai Telegram Bot - Beginner's Guide
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
Section titled “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
Section titled “Prerequisites Checklist”Before starting, make sure you have:
Software You’ll Need
Section titled “Software You’ll Need”- Python 3.10+ - Download here
- Check your version: Open terminal and type
python3 --version
- Check your version: Open terminal and type
- 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
Section titled “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
Section titled “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)
Section titled “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
Section titled “Step 1: Open BotFather on Telegram”- Open the Telegram app (phone, desktop, or web)
- Search for
@BotFatherin the search bar - Click on it and start a conversation
You should see BotFather respond with a welcome message and commands.
Step 2: Create a New Bot
Section titled “Step 2: Create a New Bot”- Send this command:
/newbot - BotFather 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)
- Type something like:
- 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.
- Type something like:
Step 3: Copy Your Token
Section titled “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:ABCdefGHIjklMNOpqrsTUVwxyzImportant: This token is like your bot’s password. Never share it publicly or commit it to GitHub!
Part 2: Prepare Your Computer (10 minutes)
Section titled “Part 2: Prepare Your Computer (10 minutes)”Step 1: Create a Project Folder
Section titled “Step 1: Create a Project Folder”Open your terminal/command prompt and create a folder for your project:
mkdir sai-telegram-botcd sai-telegram-botWhat this does:
- Creates a new folder called
sai-telegram-bot - Moves you into that folder (you’ll work from here)
Step 2: Create a Virtual Environment
Section titled “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 venvsource venv/bin/activateOn Windows:
python -m venv venvvenv\Scripts\activateWhat should happen:
- Your terminal prompt should now show
(venv)at the beginning - Example:
(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
Section titled “Step 3: Create Your Project Structure”Let’s create the folders and files you’ll need:
mkdir bottouch bot/__init__.pytouch bot/main.pytouch bot/graphql.pytouch requirements.txttouch .envtouch 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)
Section titled “Part 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
Section titled “Step 1: Create requirements.txt”Open your code editor and create a file called requirements.txt with this content:
python-telegram-bot==21.4requests==2.32.3python-dotenv==1.0.1pytz==2024.1What each package does:
| Package | Purpose |
|---|---|
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
Section titled “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)
Section titled “Part 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
Section titled “Step 1: Create env.example”This file shows what secrets are needed (without actual values):
# Telegram Bot Configuration# Get your bot token from @BotFather on TelegramTELEGRAM_BOT_TOKEN=your_bot_token_here
# Sai GraphQL Endpoint (the server we get data from)SAI_GRAPHQL_ENDPOINT=https://sai-keeper.nibiru.fi/queryStep 2: Create .env
Section titled “Step 2: Create .env”Copy 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:ABCdefGHIjklMNOpqrsTUVwxyzSAI_GRAPHQL_ENDPOINT=https://sai-keeper.nibiru.fi/querySecurity 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)
Section titled “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?
Section titled “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
Section titled “Create bot/graphql.py”This file handles all communication with Sai’s API. Here’s the code:
import osfrom typing import Any, Dict, List, Optionalimport requests
# Get the GraphQL endpoint from .env file, or use defaultSAI_GRAPHQL_ENDPOINT = os.environ.get( "SAI_GRAPHQL_ENDPOINT", "https://sai-keeper.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: oracle_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 prices- String variables like
$trader- These are placeholders that get filled in with real values
Part 6: Create the Bot’s Brain (bot/main.py)
Section titled “Part 6: Create the Bot’s Brain (bot/main.py)”This is where your bot comes alive! We’ll build this in sections.
Section 1: Imports and Setup
Section titled “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 osimport loggingfrom typing import Optional
from dotenv import load_dotenvfrom telegram import Update, InlineKeyboardButton, InlineKeyboardMarkupfrom 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
Section titled “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 prices- The division by
10^6converts from micro-units (how the API stores numbers) to regular dollars
Section 3: Price Formatting Functions
Section titled “Section 3: Price Formatting Functions”Add this to bot/main.py:
# Popular tokens to show firstPOPULAR_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 nicely- Popular tokens (BTC, ETH, etc.) appear first
Section 4: Command Handlers
Section titled “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
Section titled “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
Section titled “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)
Section titled “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
Section titled “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)
Section titled “Part 7: Test Your Bot! (5 minutes)”Step 1: Activate Virtual Environment
Section titled “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
Section titled “Step 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
Section titled “Step 3: Test on Telegram”- Open Telegram
- Search for your bot (the username you created)
- Send
/start - You should see the welcome message!
Try these commands:
| Command | What it does |
|---|---|
/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
Section titled “Step 4: Stop the Bot”When you’re done testing, press Ctrl+C in the terminal to stop the bot.
Troubleshooting
Section titled “Troubleshooting”Problem: “TELEGRAM_BOT_TOKEN not found”
Section titled “Problem: “TELEGRAM_BOT_TOKEN not found””Solution:
- Check that
.envfile exists in your project root - Open 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
Section titled “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 first - Check for errors in the terminal
Problem: “ModuleNotFoundError: No module named ‘bot’”
Section titled “Problem: “ModuleNotFoundError: No module named ‘bot’””Solution:
# Make sure you're in the project root directorycd /path/to/sai-telegram-bot
# Make sure you activated the virtual environmentsource venv/bin/activate # Mac/Linux# orvenv\Scripts\activate # Windows
# Run with -m flagpython -m bot.mainProblem: GraphQL errors or “No prices found”
Section titled “Problem: GraphQL errors or “No prices found””Solution:
-
Check your internet connection
-
Verify the endpoint is correct in
.env:SAI_GRAPHQL_ENDPOINT=https://sai-keeper.nibiru.fi/query -
Test the endpoint manually in your browser (visit the URL)
How It All Works Together
Section titled “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
Section titled “Next Steps”Your bot is working! Now you can:
Level Up Your Bot
Section titled “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
Section titled “Deploy to Production”- Run your bot 24/7 on a server (AWS, DigitalOcean, etc.)
- Use
systemdor Docker to keep it running
Learn More
Section titled “Learn More”Congratulations! 🎉
Section titled “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
Section titled “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! 🚀