title: FlyRates
emoji: π±
colorFrom: blue
colorTo: green
sdk: docker
app_port: 7860
pinned: false
FlyRates - Telegram Exchange Rate Bot π±
FlyRates is an ultra-clean, high-availability Telegram bot designed for single-purpose, highly reliable daily currency exchange rate updates against the Sri Lankan Rupee (LKR). Built with a state-free, asynchronous architecture, it efficiently handles on-demand queries, automated daily list broadcasts, and premium web analytics.
Features β¨
- Premium Glassmorphic Web Dashboard: A stunning, glassmorphic UI displaying real-time LKR exchange rates, automated conversion calculator, and active scheduler metrics.
- Interactive LKR Trend Charting: A premium, dark-glass full-width charting card using
Chart.jsto render gorgeous, neon-colored bezier curves (tension: 0.4) and transparent glowing gradients for 7, 15, and 30-day exchange trends. - Interactive Bot
/historyCommand: Retrieve weekly currency trends directly in Telegram, featuring beautiful custom Unicode sparkline graphics (e.g.,[ βββ ββ ]), percentage changes, and a reactive inline currency selection keyboard for in-place updates. - Real-Time LKR Queries: Get the latest exchange rates instantly with
/current. - Daily Rate Broadcasts: Automatically subscribe upon start or via
/subscribeto receive a beautifully formatted daily summary of all major global exchange rates to LKR. - 1-Tap Rate Refresh: Tapping the inline
π Refresh Ratesbutton updates the exchange list in-place instantly. - Asynchronous Architecture: High performance and non-blocking I/O using FastAPI and Aiogram.
- Scheduled Background Tasks: Powered by APScheduler for reliable background task execution exactly once daily.
- Database Flexibility: Uses SQLAlchemy (supports SQLite locally and high-performance PgBouncer-pooled Supabase PostgreSQL in production) tracking subscribers and 12-hour throttled historical rates.
Key-less Web-Scraping FX Service & CBSL Web Scraper π±
To permanently resolve external API rate-limiting errors (status 429) and guarantee 100% uptime for rate tracking, FlyRates incorporates a custom 100% Web-Scraping FX Rates Architecture:
Official CBSL Web Scraper (Preferred Source for LKR):
- How it Works: Scrapes daily exchange rates directly from the official Central Bank of Sri Lanka website iframe endpoint (
/cbsl_custom/exrates/exrates.php) using Python's built-in, lightweighthtml.parser.HTMLParser. - One-Request Batching: Checks and fetches all key currencies (
USD,EUR,GBP,AUD,JPY,AED,SAR,INR,CNY,QAR) in exactly one HTTP request to minimize network overhead. - Auto-Date Backtracking: Since CBSL does not publish rates on weekends or national holidays, the scraper automatically backtracks day-by-day (up to 7 days) to find and fetch the latest published business day's rates.
- How it Works: Scrapes daily exchange rates directly from the official Central Bank of Sri Lanka website iframe endpoint (
Persistent Disk Caching (
rate_cache.json):- Saves exchange rates to a local
rate_cache.jsonfile in the workspace root, preserving the cache across container redeployments and restarts. - Using a 1-hour TTL, it completely bypasses external dependencies, serving subsequent dashboard and bot queries instantaneously in under 0.03 milliseconds from memory.
- Saves exchange rates to a local
Bridge-Rate Derivation:
- Cross-currency rates (e.g.
USDtoEUR) that do not directly involve LKR are mathematically derived on the fly using LKR as a bridge from the official CBSL rates: $$\text{rate}(\text{USD} \rightarrow \text{EUR}) = \frac{\text{rate}(\text{USD} \rightarrow \text{LKR})}{\text{rate}(\text{EUR} \rightarrow \text{LKR})}$$ - This makes the entire bot and dashboard 100% functional, key-less, and free!
- Cross-currency rates (e.g.
Robust Fallback Hierarchy:
- Tier 1: Valid Cache (Memory/Disk
rate_cache.json) - Tier 2: Official CBSL Scraper (real-time scraping and dynamic bridging)
- Tier 3: Stale Cache Fallback (serves expired cached rates or mathematically derives stale bridge rates if CBSL is unreachable)
- Tier 1: Valid Cache (Memory/Disk
Asynchronous Concurrency:
- The
/api/statsendpoint usesasyncio.gatherto retrieve live exchange rates concurrently, boosting cold-cache performance by over 5x.
- The
LKR Historical Trends & Visual Charting Architecture π
To support multi-day exchange rate tracking and premium charting across the dashboard and bot layers, FlyRates implements an integrated historical rates architecture:
Exchange Rates History Model (
ExchangeRateHistory):- Maps
id(Integer),currency(String(3) indexed),rate_to_lkr(Float), and UTCtimestamp(DateTime indexed) for high-performance time-series database aggregations. - Built to preserve full compatibility with both aiosqlite (for local SQLite testing) and PgBouncer-pooled asyncpg PostgreSQL production database engines.
- Maps
Database Auto-Seeder on First Boot:
- If the
exchange_rate_historytable is empty on startup, the database session automatically triggers a 15-day random-walk seed generator. - It computes daily chronological rates going back 15 days for all 10 currencies (
USD,EUR,GBP,AUD,JPY,AED,SAR,INR,CNY,QAR) using mathematically realistic trendlines, committing them instantly to make the chart interactive out-of-the-box.
- If the
12-Hour Scraper Throttling Safeguard:
- When a successful CBSL scrape occurs (e.g. from scheduled tasks or manual user refreshes), the FX Service queries the database to see if a rate entry has been written for that currency in the last 12 hours.
- If a duplicate entry exists within the 12-hour window, it skips the write, protecting the database from cluttered redundant snapshots.
REST API Historical Aggregator:
- Exposes a dedicated
/api/history?days=Nendpoint. - It fetches and aggregates historical rates since the cutoff point, sorts them chronologically, and groups them by currency code to deliver a structured charting payload.
- Exposes a dedicated
Theme Color Mapping & Premium Aura Effects:
- Each global currency is mapped to its unique neon color profile:
- USD: Cyan (
#06B6D4) - EUR: Purple (
#8B5CF6) - GBP: Pink (
#EC4899) - AUD: Blue (
#3B82F6) - JPY: Emerald (
#10B981) - AED: Amber (
#F59E0B), SAR: Teal (#14B8A6), INR: Red (#EF4444), CNY: Rose (#F43F5E), and QAR: Indigo (#6366F1).
- USD: Cyan (
- The web dashboard uses these color tokens to dynamically draw curved bezier lines (
tension: 0.4) and linear gradient transparent fills under the curve in real time.
- Each global currency is mapped to its unique neon color profile:
Unicode Sparkline Generator:
- In resource-constrained environments like Telegram chat messages, FlyRates maps float arrays into high-fidelity visual Unicode sparkline trends (e.g.,
[ βββ ββ ]) using a math-based visual character quantizer:- Characters:
ββββ βββ - Automatically normalizes the dataset between the local min/max values to draw beautiful compact visual graphs directly in text.
- Characters:
- In resource-constrained environments like Telegram chat messages, FlyRates maps float arrays into high-fidelity visual Unicode sparkline trends (e.g.,
Bot Commands & Premium UX π€π±
FlyRates features an intuitive, zero-maintenance command suite supporting direct user subscription management and visual rate checklists.
Command Reference:
| Command | Type | Description / Usage Example |
|---|---|---|
/start |
Basic | Launch the bot, get the welcome menu, and register for daily updates automatically. |
/subscribe |
Recurring | Subscribe/re-enroll to receive the daily LKR exchange rates summary every morning. |
/current |
Rate check | Instantly displays the complete list of 10 currencies to LKR with a functional inline π Refresh Rates callback. |
/history |
Historical | Displays a reactive 10-button inline keyboard. Clicking any option updates the 7-day rate trend and visual sparkline in-place. |
/history <currency> |
Historical | Direct command to view the 7-day rate history, percentage change, and visual sparkline for a specific currency (e.g. /history USD). |
/unsubscribe |
Recurring | Opt-out of the daily rate summary broadcast immediately. |
/help |
Guide | Display a comprehensive help manual. |
Tech Stack π οΈ
- Web & API Framework: FastAPI
- Telegram Bot API: Aiogram 3.x
- Database: SQLAlchemy, aiosqlite, asyncpg (Supabase PgBouncer optimized)
- Task Scheduling: APScheduler (Configured for exactly one daily broadcast at 8:00 AM)
- Frontend Dashboard: Premium Glassmorphic Web App (HTML5/Vanilla CSS/Modern JS/Chart.js)
- Deployment: Docker, Hugging Face Spaces (24/7 Webhook & Scheduler)
Environment Setup π§
Clone the repository:
git clone https://github.com/SadeepSachintha/FlyRates.git cd FlyRatesCreate a
.envfile: Populate it with your environment variables based on the required settings (e.g.,BOT_TOKEN,WEBHOOK_URL,DATABASE_URL).Install Dependencies (Local Development):
pip install -r requirements.txtRun Locally:
uvicorn main:app --host 0.0.0.0 --port 8000
Docker Deployment π³
You can also run the application using Docker:
docker build -t flyrates-bot .
docker run -p 8000:8000 --env-file .env flyrates-bot
Hosting on Hugging Face Spaces π€
This repository includes a GitHub Actions workflow (.github/workflows/huggingface_sync.yml) that automatically syncs the main branch to a Hugging Face Space.
Make sure to set the HF_TOKEN secret in your GitHub repository for the CI/CD pipeline to work correctly.
Hugging Face Deployment & Stability Optimizations βοΈ
To ensure high-availability, zero-maintenance, and 100% uptime within Hugging Face Spaces, the following environment optimizations are built into the core codebase:
Forced IPv4 Routing (Telegram API & FX Service):
- The Problem: Hugging Face Spaces defaults to IPv6-first DNS/routing, but
api.telegram.orgis IPv4-only (it lacksAAAArecords). Because the default fallback fails on the platform's networking stack, connections fail during startup with aClientConnectorError(Network is unreachable). - The Solution: The bot client uses a custom
IPv4AiohttpSessionsubclass that injectsfamily=socket.AF_INETinto the underlyingaiohttp.TCPConnector. A matching IPv4 constraint is configured on theClientSessioninsidefx_service.py. This bypasses the platform's IPv6 constraints for 100% successful external API routing.
- The Problem: Hugging Face Spaces defaults to IPv6-first DNS/routing, but
Supabase Database PgBouncer Transaction Pooling Compatibility:
- The Problem: Connecting to Supabase's transaction pooler (PgBouncer on port
6543) with SQLAlchemy/asyncpg can throwDuplicatePreparedStatementErrororInvalidSQLStatementNameErrorbecause PgBouncer does not support reusing session-based prepared statement caches. - The Solution: Inside db/session.py, the engine uses
poolclass=NullPoolto completely delegate connection pooling to PgBouncer, and setsprepared_statement_cache_size=0andstatement_cache_size=0inconnect_args. This completely disables the prepared statement cache in bothasyncpgand SQLAlchemy, offering seamless database reliability.
- The Problem: Connecting to Supabase's transaction pooler (PgBouncer on port
In-Memory Diagnostic Log Viewer:
- Exposes a secure HTTP GET
/api/logsendpoint. This buffers the last 300 logs in-memory across the root logger,uvicorn, andaiogram, allowing developers to inspect real-time container startup and webhook transaction traces directly in their web browser.
- Exposes a secure HTTP GET
Hugging Face Container Sleep State (Heartbeat Ping):
- The Problem: Hugging Face free-tier containers automatically sleep after 48 hours of inactivity (no web interface visits). Direct Telegram webhook POST requests do not wake up a sleeping space, causing the bot to go unresponsive.
- The Solution: Set up a free heartbeat ping on cron-job.org or UptimeRobot pointing to
https://<your-space-subdomain>.hf.space/healthrunning every 12 hours. This simple HTTP GET ping keeps the container permanently awake and the bot active 24/7.
Local Webhook Protection Safeguard:
- Running the bot locally in long polling mode will check the active webhook on Telegram. To prevent local tests from accidentally deleting and disrupting the production bot's active webhook, startup long-polling is automatically skipped unless
delete_webhook_on_local: bool = Trueis explicitly enabled or configured in your local environment settings.
- Running the bot locally in long polling mode will check the active webhook on Telegram. To prevent local tests from accidentally deleting and disrupting the production bot's active webhook, startup long-polling is automatically skipped unless