samayly.com is live. Hosted plans open soon. Join the waitlist β†’
Phone optionreference19 min read

Option 2: Telegram Bot Integration

πŸ’‘ Prefer no cloud? Option 6 runs a Telegram bot on your own Mac using your existing ts03 commands: no Cloud Run, no extra API key.

Build a Telegram bot that lets you (and friends) update your Google Sheet using natural language, from any device - iPhone, Android, Mac, Watch, or browser.

⏱️ Setup time: 45 min πŸ’° Cost: ~$1-3/month (Claude API only, Cloud Run stays free-tier) πŸ‘₯ Users: You + whitelisted friends


What you're building

1. Send message

2. Webhook POST

3. Parse with Claude

4. Return structured action

5. Execute on sheet

6. Result

7. Reply message

8. Notification to user

πŸ“± You / Friends
(any device with Telegram)

πŸ€– Telegram Bot

☁️ Cloud Run
(Python webhook)

🧠 Claude API

πŸ“Š Google Sheets API

πŸ“— Your Sheet

Chat looks like this:

code
You: What's due this week?
Bot: πŸ“Š 3 items due this week:
     β€’ 30 Sep - Ethics Assignment (4 days)
     β€’ 4 Oct - GPU Quiz (8 days)
     β€’ 5 Oct - Innovation Assignment (9 days)

You: Add GPU quiz Oct 15 6pm
Bot: πŸ“‹ Proposed row:
     Subject: GPU Programming
     Type: Quiz
     Date: 15-Oct-2026
     Time: 18:00 IST
     Reply YES to confirm.

You: yes
Bot: βœ… Added row #8 to TS-03 Quiz & Assignment

Setup Overview

Step 1
Create Telegram Bot

Step 2
Get Claude API Key

Step 3
Create GCP Service Account

Step 4
Share Sheet with Service Account

Step 5
Write Python Bot Code

Step 6
Deploy to Cloud Run

Step 7
Register Webhook

Step 8
Add Friends (whitelist)

Step 9
Test end-to-end


1Create Telegram Bot

Telegram bots are created via a bot called BotFather (meta, yes).

  1. Open Telegram β†’ search for @BotFather β†’ start chat
  2. Send: /newbot
  3. Answer prompts: - Name: Tracker Bot (display name) - Username: my_tracker_bot (must end in _bot, unique)
  4. BotFather replies with: Use this token to access the HTTP API: 7898765432:AAG_xxxxxxxxxxxxxxxxxxxxxx
  5. Save this token securely - treat it like a password

Optional customization: - /setdescription - describe what your bot does - /setuserpic - upload avatar - /setcommands - define slash commands like /week, /today, /add


2Get Claude API Key

  1. Go to console.anthropic.com
  2. Sign in β†’ API Keys β†’ Create Key
  3. Name it: telegram-tracker-bot
  4. Copy the key (starts with sk-ant-...) - save it, you can't see it again

Add ~$5 credit to your Anthropic account for testing (Settings β†’ Billing).


3Create GCP Service Account (for sheet access)

Unlike the MCP setup which uses OAuth (you-as-user), the bot uses a service account (a bot identity) that doesn't need browser flows.

  1. Go to console.cloud.google.com β†’ select your sheets-mcp project (or create new)
  2. Left sidebar β†’ IAM & Admin β†’ Service Accounts
  3. Click + Create Service Account
  4. Fill in: - Name: telegram-bot-sheets - ID: auto-fills - Description: Service account for TS-03 Telegram bot
  5. Click Create and Continue
  6. Skip the role/permission steps (we grant per-sheet access instead)
  7. Click Done
  8. Click on the created service account β†’ Keys tab β†’ Add Key β†’ Create new key β†’ JSON
  9. A JSON file downloads - save it as service-account.json

4Share your sheet with the service account

The service account's email looks like: [email protected]

  1. Open your Google Sheet in browser
  2. Click Share (top right)
  3. Paste the service account email
  4. Set permission: Editor
  5. Uncheck "Notify people" (it's a bot, no email needed)
  6. Click Share

5Write the Python Bot

Create a project folder:

bash
mkdir ~/Desktop/tracker-telegram-bot
cd ~/Desktop/tracker-telegram-bot

5.1 - main.py

python
import os
import json
from flask import Flask, request
import anthropic
from google.oauth2 import service_account
from googleapiclient.discovery import build
import telegram

app = Flask(__name__)

# --- Config from environment ---
TELEGRAM_TOKEN = os.environ['TELEGRAM_TOKEN']
ANTHROPIC_KEY = os.environ['ANTHROPIC_KEY']
SHEET_ID = os.environ['SHEET_ID']
SHEET_TAB = os.environ.get('SHEET_TAB', 'TS-03 Quiz & Assignment')

# Whitelist: Telegram user IDs (get yours by messaging @userinfobot)
ALLOWED_USER_IDS = [int(x) for x in os.environ['ALLOWED_USERS'].split(',')]

# --- Clients ---
bot = telegram.Bot(token=TELEGRAM_TOKEN)
claude = anthropic.Anthropic(api_key=ANTHROPIC_KEY)

# Service account credentials from Secret Manager (mounted as file)
creds = service_account.Credentials.from_service_account_file(
    '/secrets/service-account.json',
    scopes=['https://www.googleapis.com/auth/spreadsheets']
)
sheets_api = build('sheets', 'v4', credentials=creds)

# --- Session state (in-memory; consider Redis for production) ---
pending_confirmations = {}  # user_id -> proposed row

# --- System prompt for Claude ---
SYSTEM_PROMPT = f"""You are a tracker bot assistant. Parse the user's message into
one of these actions on their Google Sheet:

VALID SUBJECTS ONLY:
- FoVL-Language
- GPU Programming
- Ethics and Professional Life
- Innovation and IP Management
- Advanced AI

Sheet columns: A=#, B=Subject, C=Type, D=Title, E=Due Date (DD-Mmm-YYYY),
F=Time (HH:MM IST), G=Mode (default: Google Classroom), H=Source (default: GCR),
I=Status (default: Upcoming), J=Days Left (formula, skip), K=Notes

Return ONLY valid JSON, no other text:

For READ: {{"action": "read", "query": "user's original request"}}
For ADD: {{"action": "add", "row": {{"subject": ..., "type": ..., "title": ..., "due_date": ..., "time": ..., "notes": ...}}}}
For CONFIRM: {{"action": "confirm"}}
For UNKNOWN: {{"action": "unknown", "message": "explanation"}}

Always convert SGT times to IST (subtract 2h 30m).
If subject invalid, action=unknown with clarification message.
"""


def read_sheet(query):
    """Read sheet and use Claude to answer the query."""
    result = sheets_api.spreadsheets().values().get(
        spreadsheetId=SHEET_ID,
        range=f"'{SHEET_TAB}'!A4:K100"
    ).execute()
    rows = result.get('values', [])

    # Ask Claude to answer the query using the sheet data
    response = claude.messages.create(
        model="claude-sonnet-4-5",
        max_tokens=1024,
        messages=[{
            "role": "user",
            "content": f"Sheet data:\n{json.dumps(rows)}\n\nQuery: {query}\n\nAnswer concisely for Telegram."
        }]
    )
    return response.content[0].text


def add_row(row_data):
    """Append a row to the sheet."""
    # Find next # value
    existing = sheets_api.spreadsheets().values().get(
        spreadsheetId=SHEET_ID,
        range=f"'{SHEET_TAB}'!A:A"
    ).execute().get('values', [])
    next_num = len([r for r in existing if r and r[0].isdigit()]) + 1

    values = [[
        next_num,
        row_data['subject'],
        row_data['type'],
        row_data['title'],
        row_data['due_date'],
        row_data['time'],
        'Google Classroom',
        'GCR',
        'Upcoming',
        '',  # Days Left auto-calculated
        row_data.get('notes', '')
    ]]

    sheets_api.spreadsheets().values().append(
        spreadsheetId=SHEET_ID,
        range=f"'{SHEET_TAB}'!A5",
        valueInputOption='USER_ENTERED',
        body={'values': values}
    ).execute()
    return f"βœ… Added row #{next_num}: {row_data['subject']} - {row_data['title']}"


@app.route('/webhook', methods=['POST'])
def webhook():
    update = telegram.Update.de_json(request.get_json(), bot)
    if not update.message:
        return 'ok'

    user_id = update.message.from_user.id
    text = update.message.text or ''

    # --- Authorization check ---
    if user_id not in ALLOWED_USER_IDS:
        bot.send_message(
            chat_id=user_id,
            text=f"β›” Unauthorized. Your ID: {user_id}. Ask the admin to whitelist you."
        )
        return 'ok'

    try:
        # --- Handle confirmation flow ---
        if text.lower() in ['yes', 'y', 'confirm']:
            if user_id in pending_confirmations:
                result = add_row(pending_confirmations[user_id])
                del pending_confirmations[user_id]
                bot.send_message(chat_id=user_id, text=result)
            else:
                bot.send_message(chat_id=user_id, text="Nothing pending to confirm.")
            return 'ok'

        if text.lower() in ['no', 'cancel']:
            pending_confirmations.pop(user_id, None)
            bot.send_message(chat_id=user_id, text="Cancelled.")
            return 'ok'

        # --- Parse with Claude ---
        parse_response = claude.messages.create(
            model="claude-sonnet-4-5",
            max_tokens=1024,
            system=SYSTEM_PROMPT,
            messages=[{"role": "user", "content": text}]
        )
        parsed = json.loads(parse_response.content[0].text)

        # --- Execute action ---
        if parsed['action'] == 'read':
            answer = read_sheet(parsed['query'])
            bot.send_message(chat_id=user_id, text=answer)

        elif parsed['action'] == 'add':
            row = parsed['row']
            pending_confirmations[user_id] = row
            preview = (
                f"πŸ“‹ *Proposed row:*\n"
                f"Subject: {row['subject']}\n"
                f"Type: {row['type']}\n"
                f"Title: {row['title']}\n"
                f"Date: {row['due_date']}\n"
                f"Time: {row['time']}\n"
                f"Notes: {row.get('notes', '-')}\n\n"
                f"Reply *YES* to confirm, *NO* to cancel."
            )
            bot.send_message(chat_id=user_id, text=preview, parse_mode='Markdown')

        elif parsed['action'] == 'unknown':
            bot.send_message(chat_id=user_id, text=parsed['message'])

    except Exception as e:
        bot.send_message(chat_id=user_id, text=f"❌ Error: {str(e)}")

    return 'ok'


@app.route('/', methods=['GET'])
def health():
    return 'OK'


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=int(os.environ.get('PORT', 8080)))

5.2 - requirements.txt

code
Flask==3.0.0
anthropic==0.34.0
python-telegram-bot==13.15
google-api-python-client==2.100.0
google-auth==2.23.0
gunicorn==21.2.0

5.3 - Dockerfile

dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY main.py .
CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 60 main:app

6Deploy to Google Cloud Run

Cloud Run charges only when requests come in - free tier easily covers personal bot usage.

6.1 - Install gcloud CLI

bash
brew install --cask google-cloud-sdk
gcloud auth login
gcloud config set project YOUR_GCP_PROJECT_ID

6.2 - Store secrets in Secret Manager

bash
# Enable Secret Manager API
gcloud services enable secretmanager.googleapis.com

# Store service account JSON
gcloud secrets create sheets-service-account \
  --data-file=service-account.json

# Grant Cloud Run access to the secret
gcloud secrets add-iam-policy-binding sheets-service-account \
  --member="serviceAccount:$(gcloud projects describe YOUR_PROJECT --format='value(projectNumber)')[email protected]" \
  --role="roles/secretmanager.secretAccessor"

6.3 - Get your Telegram user ID

Message @userinfobot on Telegram β†’ replies with your ID (e.g., 123456789).

6.4 - Deploy

bash
gcloud run deploy tracker-telegram-bot \
  --source . \
  --region asia-southeast1 \
  --platform managed \
  --allow-unauthenticated \
  --set-env-vars TELEGRAM_TOKEN=YOUR_TOKEN,ANTHROPIC_KEY=sk-ant-xxx,SHEET_ID=your_sheet_id,ALLOWED_USERS=123456789 \
  --set-secrets /secrets/service-account.json=sheets-service-account:latest

Wait ~2 min for build + deploy. Output shows your service URL like:

code
https://tracker-telegram-bot-xxxxx-as.a.run.app

Save this URL.


7Register webhook with Telegram

Tell Telegram where to send incoming messages:

bash
export BOT_TOKEN="YOUR_TELEGRAM_TOKEN"
export SERVICE_URL="https://tracker-telegram-bot-xxxxx-as.a.run.app"

curl "https://api.telegram.org/bot${BOT_TOKEN}/setWebhook?url=${SERVICE_URL}/webhook"

Expected response:

json
{"ok":true,"result":true,"description":"Webhook was set"}

Verify:

bash
curl "https://api.telegram.org/bot${BOT_TOKEN}/getWebhookInfo"

Should show your Cloud Run URL as the current webhook.


8Add friends to the whitelist

For each friend who wants access:

  1. Have them message @userinfobot β†’ get their Telegram ID
  2. They send you their ID
  3. Update the ALLOWED_USERS env var on Cloud Run:
bash
gcloud run services update tracker-telegram-bot \
  --region asia-southeast1 \
  --update-env-vars ALLOWED_USERS=123456789,987654321,555555555
  1. They now have access - they can search for your bot on Telegram (@my_tracker_bot) and start chatting.

9Test end-to-end

Open Telegram β†’ search for your bot β†’ tap Start β†’ try:

Read queries:

code
What's due this week?
Show all GPU Programming items
When's the next assignment?

Add queries:

code
Add a GPU quiz on Oct 15 at 6 PM IST
Add Ethics quiz next Friday

Confirmation flow:

code
You: Add GPU quiz Oct 15
Bot: πŸ“‹ Proposed row: ... Reply YES to confirm.
You: yes
Bot: βœ… Added row #8

Cost breakdown (real numbers)

Typical personal usage (~50 messages/day, both you + 5 friends):

Service Free tier Your usage Monthly cost
Cloud Run 2M requests/month free ~1500 req/mo $0
Cloud Build 120 build-min/day free ~10 min/mo $0
Secret Manager 6 secrets, 10K access/mo free 1 secret, ~1500 access $0
Claude API (Sonnet 4.5) Pay per token ~500K tokens/mo ~$1.50
Telegram Fully free unlimited $0
Total ~$1.50/month

At 500 msg/day the Claude cost scales to ~$15/mo. Still cheap.


Security best practices

βœ… Do

  • User whitelist enforced server-side - never trust client-side checks
  • Secrets in Secret Manager - never in code or env vars visible in Cloud Run UI
  • Rate limiting - add a simple counter to prevent runaway API costs
  • Log all writes - audit trail for who added/modified what
  • Read/write distinction - writes require confirmation, reads don't

❌ Don't

  • Expose the webhook URL publicly (Telegram has your bot token, only they should call it)
  • Hard-code your Telegram token in the repo (use env vars/secrets)
  • Trust user input in SQL/formulas (Google Sheets is safer than SQL, but still validate)
  • Let the bot delete rows without extra confirmation

Advanced features (optional)

Inline buttons for quick actions

Replace text confirmation with tap buttons:

python
from telegram import InlineKeyboardButton, InlineKeyboardMarkup

keyboard = [[
    InlineKeyboardButton("βœ… Confirm", callback_data='confirm'),
    InlineKeyboardButton("❌ Cancel", callback_data='cancel'),
]]
bot.send_message(
    chat_id=user_id,
    text="Add this row?",
    reply_markup=InlineKeyboardMarkup(keyboard)
)

Voice notes β†’ text via Whisper

python
if update.message.voice:
    file = bot.get_file(update.message.voice.file_id)
    file.download('/tmp/voice.ogg')
    # Send to Whisper API for transcription
    text = whisper_transcribe('/tmp/voice.ogg')

Now users can hold-to-record voice messages instead of typing.

Slash commands

Register with BotFather (/setcommands):

code
week - What's due this week
today - What's due today
upcoming - All upcoming items
add - Add a new item
help - Show all commands

Then handle in code:

python
if text.startswith('/week'):
    answer = read_sheet("What's due in the next 7 days?")

Scheduled daily digest

Add Cloud Scheduler β†’ cron: 0 8 * * * (8 AM daily) β†’ hits a /daily-digest endpoint that reads sheet and sends to all whitelisted users.


Troubleshooting

Bot doesn't reply

Check webhook:

bash
curl "https://api.telegram.org/bot${BOT_TOKEN}/getWebhookInfo"

Look for last_error_message - often reveals the issue.

Check Cloud Run logs:

bash
gcloud run logs read tracker-telegram-bot --region asia-southeast1 --limit 50

"Permission denied" reading sheet

Service account email must be shared as Editor on the sheet. Verify: - Open sheet β†’ Share β†’ confirm bot's service account email listed

Claude returns invalid JSON

Sometimes the model wraps JSON in markdown fences. Strip them:

python
raw = parse_response.content[0].text
raw = raw.replace('```json', '').replace('```', '').strip()
parsed = json.loads(raw)

Cloud Run cold starts (~2 sec delay on first message)

Set minimum instances = 1 (small cost, ~$5/mo) to eliminate cold starts:

bash
gcloud run services update tracker-telegram-bot \
  --min-instances 1 --region asia-southeast1

Summary

You now have a Telegram bot anyone in your whitelist can chat with to update your Google Sheet using natural language. Works from iPhone, iPad, Android, Mac, browser - anywhere Telegram runs.

Next enhancement paths: - Option 4 (WhatsApp) for batch adoption in India where WhatsApp dominates - Option 5 (Siri) to add voice-first personal usage on top

Questions or corrections: [email protected]