🇧🇷 Português | 🇺🇸 English
A bot that monitors whey protein prices across 8 Brazilian online stores, calculates the best cost per gram of protein, and automatically sends alerts via Telegram and WhatsApp — with messages generated by AI (Groq / Llama 3.3 70B).
- Collects prices twice a day (8:00 AM and 8:00 PM, Brasília time) from 8 stores
- Calculates a ranking by cost per gram of protein — the lower, the better
- Automatically detects promotions: if a product drops ≥ 5% below its 7-day average, an alert is fired
- Sends a daily ranking at 8:05 AM with each product's photo and purchase link
- Sends price-band offers from Growth Supplements (12:00 PM) and ProFit Labs (4:00 PM)
- Sends flash sales from Soldiers Nutrition (8:10 PM)
- AI-generated messages: every product goes through Groq (Llama 3.3 70B) before being sent — with tone calibrated by ranking position and automatically detected whey type
- REST API protected by API Key to query the ranking and trigger dispatches manually
| Layer | Technology |
|---|---|
| Language | Java 17 |
| Framework | Spring Boot 3.3.5 |
| HTTP Client | Spring WebFlux (WebClient) |
| Scheduling | Spring Scheduler (@Scheduled) |
| Database | PostgreSQL |
| ORM | Spring Data JPA / Hibernate |
| Message generation | Groq API (Llama 3.3 70B) |
| Notifications | Telegram Bot API + Baileys sidecar (WhatsApp) |
| Containerization | Docker + Docker Compose |
| Deployment | Railway |
cost per gram of protein = price / total protein in package (g)
Total protein comes from an internal nutrition table (nutrition_info), matched by product name, brand, and package weight. The lower the value, the better the deal.
On every collection run, the service compares the current price against the 7-day moving average from price_history. If the drop is ≥ 5% and there are at least 3 samples in the window, an alert is fired with the product photo, discount percentage, and purchase link. The threshold and window are configurable via environment variables.
Scheduler
│
├─ 08:00 / 20:00 ──► StoreCollectorService (8 stores)
│ └─ OfferPersistenceService (upsert + price_history)
│ └─ RankingService (calculates cost/g protein)
│ └─ PromotionService (compares vs 7-day average)
│ └─ GroqMessageService (generates caption via AI)
│ ├─ TelegramNotificationService
│ └─ WhatsAppNotificationService → Baileys sidecar
│
├─ 08:05 ──► Daily ranking (Telegram + WhatsApp)
├─ 12:00 ──► Growth price-band offers (Telegram + WhatsApp)
├─ 16:00 ──► ProFit Labs promotions (Telegram + WhatsApp)
└─ 20:10 ──► Soldiers Nutrition flash sale (Telegram + WhatsApp)
All endpoints require the X-API-Key header.
GET /api/health
GET /api/products
GET /api/products/available
GET /api/products/by-store?store=GROWTH
GET /api/rankings/whey/top-cost-benefit?top=10
GET /api/rankings/whey/top-cost-benefit?top=5&store=DARK_LAB
GET /api/growth/offers
GET /api/growth/ofertas
GET /api/growth/ofertas/bands
GET /api/darklab/offers
GET /api/profitlabs/offers
GET /api/profitlabs/promocoes
GET /api/profitlabs/promocoes/bands
GET /api/soldiers/offers
GET /api/soldiers/oferta-relampago
GET /api/blackskull/offers
GET /api/nutrata/offers
GET /api/adaptogen/offers
GET /api/absolut/offers
GET /api/offers/whey
POST /api/telegram/trigger/ranking?top=10
POST /api/telegram/trigger/promotions
POST /api/telegram/trigger/growth-ofertas
POST /api/telegram/trigger/profitlabs-promocoes
POST /api/telegram/trigger/soldiers-relampago
POST /api/whatsapp/trigger/ranking?top=10
POST /api/whatsapp/trigger/promotions
POST /api/whatsapp/trigger/growth/ofertas
POST /api/whatsapp/trigger/profitlabs/promocoes
POST /api/whatsapp/trigger/soldiers/oferta-relampago
Requirements: Docker, Java 17+, Maven 3.9+
# 1. Start the database
docker-compose up -d
# 2. Run the application
mvn spring-boot:runThe API starts at http://localhost:8080. On the first run, StartupCollector automatically triggers a data collection if the database is empty. Wait ~30 seconds and query the ranking:
GET http://localhost:8080/api/rankings/whey/top-cost-benefit?top=10
| Variable | Description | Default |
|---|---|---|
SPRING_DATASOURCE_URL |
PostgreSQL JDBC URL | jdbc:postgresql://localhost:5432/whey_db |
SPRING_DATASOURCE_USERNAME |
Database user | whey_user |
SPRING_DATASOURCE_PASSWORD |
Database password | whey_pass |
API_KEY |
Key to protect API endpoints | (empty = no protection) |
TELEGRAM_BOT_TOKEN |
Telegram bot token | (empty = no sending) |
TELEGRAM_CHAT_ID |
Target group or channel ID | — |
WHATSAPP_BASE_URL |
Baileys sidecar base URL | (empty = no sending) |
WHATSAPP_PHONE |
Target JID (channel @newsletter, group @g.us or number) |
— |
GROQ_API_KEY |
Groq API key (AI messages) | (empty = uses fixed templates) |
PORT |
HTTP port | 8080 |
The project uses two Railway services: the Java bot and the WhatsApp sidecar.
- New Project → Deploy from GitHub repo
- Add a PostgreSQL plugin
- Set the environment variables in the Railway dashboard
- Railway builds and deploys automatically on every push to
main
The sidecar is a Node.js service that maintains the WhatsApp connection via Baileys.
- New Service → Deploy from GitHub repo (
baileys-sidecar/folder) - Add a Volume mounted at
/app/authto persist the session across deploys - After deploy, open
GET /qrat the service's public URL and scan the QR code from WhatsApp - Set
WHATSAPP_BASE_URLin the Java bot pointing to the sidecar's internal URL
The session is stored in the volume and survives redeploys. The QR code only needs to be scanned again if the session expires.
src/main/java/com/devlil0/whey_promotion_bot/
├── client/ # HTTP clients per store (8 stores)
├── config/ # WebClient, API Key interceptor, nutrition seeder
├── controller/ # REST endpoints, Telegram and WhatsApp triggers
├── dto/ # ProductOfferResponse, RankingItemResponse, PromotionAlert, etc.
├── entity/ # JPA: ProductOffer, NutritionInfo, ProductScore, PriceHistory
├── repository/ # Spring Data JPA repositories
├── scheduler/ # Twice-daily collection + dispatch schedules
└── service/ # Ranking, promotions, collection, notifications, AI (Groq), nutrition matching
baileys-sidecar/ # Node.js service for WhatsApp (Baileys)
├── index.js # Express + Baileys: /send-text, /send-image, /health, /qr
├── Dockerfile
└── railway.toml
| Table | Description |
|---|---|
product_offer |
Products collected from stores with price, availability, and URL |
nutrition_info |
Nutrition table: protein per serving, total per package |
product_score |
Calculated ranking: cost/g of protein + position |
price_history |
Price snapshot on every collection run — basis for the 7-day average |
Schema is created and updated automatically by Hibernate (ddl-auto: update).
| Parameter | Default | Description |
|---|---|---|
promotion.discount-threshold |
0.05 |
Minimum drop relative to the average (5%) |
promotion.history-days |
7 |
Average window in days |
promotion.min-history-samples |
3 |
Minimum samples required to trigger an alert |