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
Chat looks like this:
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
1Create Telegram Bot
Telegram bots are created via a bot called BotFather (meta, yes).
- Open Telegram β search for @BotFather β start chat
- Send:
/newbot - Answer prompts:
- Name:
Tracker Bot(display name) - Username:my_tracker_bot(must end in_bot, unique) - BotFather replies with:
Use this token to access the HTTP API: 7898765432:AAG_xxxxxxxxxxxxxxxxxxxxxx - 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
- Go to console.anthropic.com
- Sign in β API Keys β Create Key
- Name it:
telegram-tracker-bot - 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.
- Go to console.cloud.google.com β select your
sheets-mcpproject (or create new) - Left sidebar β IAM & Admin β Service Accounts
- Click + Create Service Account
- Fill in:
- Name:
telegram-bot-sheets- ID: auto-fills - Description:Service account for TS-03 Telegram bot - Click Create and Continue
- Skip the role/permission steps (we grant per-sheet access instead)
- Click Done
- Click on the created service account β Keys tab β Add Key β Create new key β JSON
- 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]
- Open your Google Sheet in browser
- Click Share (top right)
- Paste the service account email
- Set permission: Editor
- Uncheck "Notify people" (it's a bot, no email needed)
- Click Share
5Write the Python Bot
Create a project folder:
mkdir ~/Desktop/tracker-telegram-bot
cd ~/Desktop/tracker-telegram-bot
5.1 - main.py
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
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
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
brew install --cask google-cloud-sdk
gcloud auth login
gcloud config set project YOUR_GCP_PROJECT_ID
6.2 - Store secrets in Secret Manager
# 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
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:
https://tracker-telegram-bot-xxxxx-as.a.run.app
Save this URL.
7Register webhook with Telegram
Tell Telegram where to send incoming messages:
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:
{"ok":true,"result":true,"description":"Webhook was set"}
Verify:
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:
- Have them message @userinfobot β get their Telegram ID
- They send you their ID
- Update the ALLOWED_USERS env var on Cloud Run:
gcloud run services update tracker-telegram-bot \
--region asia-southeast1 \
--update-env-vars ALLOWED_USERS=123456789,987654321,555555555
- 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:
What's due this week?
Show all GPU Programming items
When's the next assignment?
Add queries:
Add a GPU quiz on Oct 15 at 6 PM IST
Add Ethics quiz next Friday
Confirmation flow:
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:
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
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):
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:
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:
curl "https://api.telegram.org/bot${BOT_TOKEN}/getWebhookInfo"
Look for last_error_message - often reveals the issue.
Check Cloud Run logs:
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:
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:
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]