> For the complete documentation index, see [llms.txt](https://docs.sai.fun/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.sai.fun/for-devs/use-cases/tg-guide.md).

# 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](https://www.python.org/downloads/)
  * Check your version: Open terminal and type `python3 --version`
* **A code editor** - Any of these work:
  * [VS Code](https://code.visualstudio.com/) (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

1. Open the Telegram app (phone, desktop, or [web](https://web.telegram.org/))
2. Search for `@BotFather` in the search bar
3. Click on it and start a conversation

You should see BotFather respond with a welcome message and commands.

### Step 2: Create a New Bot

1. Send this command: `/newbot`
2. 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)
3. 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 with `bot`)
   * Example: `sai_price_bot`, `trading_bot_sai`, etc.

### Step 3: Copy Your Token

BotFather will send you a message like:

```txt
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:

```bash
mkdir sai-telegram-bot
cd sai-telegram-bot
```

**What 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

A virtual environment is like a sandbox for your project. It keeps your bot's dependencies separate from other Python projects.

**On Mac/Linux:**

```bash
python3 -m venv venv
source venv/bin/activate
```

**On Windows:**

```bash
python -m venv venv
venv\Scripts\activate
```

**What 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

Let's create the folders and files you'll need:

```bash
mkdir bot
touch bot/__init__.py
touch bot/main.py
touch bot/graphql.py
touch requirements.txt
touch .env
touch env.example
```

**What this creates:**

```txt
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 .env
```

***

## 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`

Open your code editor and create a file called `requirements.txt` with this content:

```txt
python-telegram-bot==21.4
requests==2.32.3
python-dotenv==1.0.1
pytz==2024.1
```

**What 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

In your terminal (make sure `(venv)` is showing), type:

```bash
pip install -r requirements.txt
```

This will download and install all the packages. You should see lots of text scrolling by. Wait for it to finish.

**Success looks like:**

```cmd
Successfully installed python-telegram-bot-21.4 requests-2.32.3 python-dotenv-1.0.1 pytz-2024.1
```

***

## 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`

This file shows what secrets are needed (without actual values):

```ini
# 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.nibiru.fi/query
```

### Step 2: Create `.env`

Copy the example file:

```bash
cp env.example .env
```

Now open `.env` in your editor and replace `your_bot_token_here` with your actual token from BotFather:

```ini
TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyz
SAI_GRAPHQL_ENDPOINT=https://sai-keeper.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`

This file handles all communication with Sai's API. Here's the code:

```python
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.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 None
```

**Key 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 API
* **`fetch_trades():`** - Gets trade data for a wallet
* **`fetch_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`)

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):

```python
# This file makes 'bot' a Python package
```

Create `bot/main.py` - Start with imports and setup:

```python
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`:

```python
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 info
* `format_trade_closed()` - Displays a closed trade with entry/exit prices
* The division by `10^6` converts from micro-units (how the API stores numbers) to regular dollars

***

### Section 3: Price Formatting Functions

Add this to `bot/main.py`:

```python
# 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 pagination
* `format_price_single()` - Formats a single price nicely
* Popular tokens (BTC, ETH, etc.) appear first

***

### Section 4: Command Handlers

Add this to `bot/main.py`:

```python
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:**

* `/start` command shows welcome message
* `/help` shows the same message

***

### Section 5: The Trades Command

Add this to `bot/main.py`:

```python
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`:

```python
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`:

```python
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`:

```python
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:

```bash
# You should see (venv) at the start of your terminal prompt
# If not, activate it:

# Mac/Linux:
source venv/bin/activate

# Windows:
venv\Scripts\activate
```

### Step 2: Start the Bot

```bash
python -m bot.main
```

You should see:

```txt
INFO:__main__:🚀 Bot is starting...
```

**This means your bot is running!** Leave this terminal open.

### Step 3: Test on Telegram

1. Open Telegram
2. Search for your bot (the username you created)
3. Send `/start`
4. 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

When you're done testing, press `Ctrl+C` in the terminal to stop the bot.

***

## Troubleshooting

### Problem: "TELEGRAM\_BOT\_TOKEN not found"

**Solution:**

1. Check that `.env` file exists in your project root
2. Open it and verify the token is there
3. Make sure there are no spaces: `TELEGRAM_BOT_TOKEN=123456789:ABC...`

### Problem: Bot doesn't respond to commands

**Solution:**

1. Check that the bot is still running (you should see `🚀 Bot is starting...` in terminal)
2. Try the `/start` command first
3. Check for errors in the terminal

### Problem: "ModuleNotFoundError: No module named 'bot'"

**Solution:**

```bash
# 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.main
```

### Problem: GraphQL errors or "No prices found"

**Solution:**

1. Check your internet connection
2. Verify the endpoint is correct in `.env`:

   ```txt
   SAI_GRAPHQL_ENDPOINT=https://sai-keeper.nibiru.fi/query
   ```
3. Test the endpoint manually in your browser (visit the URL)

***

## How It All Works Together

Here's the flow when someone uses your bot:

```txt
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 prices
```

***

## Next 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 `systemd` or Docker to keep it running

### Learn More

* [python-telegram-bot docs](https://python-telegram-bot.org/)
* [GraphQL basics](https://graphql.org/learn/)
* [Sai Docs](https://docs.sai.fi)

***

## 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! 🚀


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.sai.fun/for-devs/use-cases/tg-guide.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
