TL;DR: A Telegram bot that takes a URL, topic, or voice message and generates a ready-to-publish post with a unique AI cover image. Stack: Node.js, Telegraf, Google Gemini 2.5 Flash, Pollinations (Flux), Supabase. Infrastructure cost: $0.
The Problem
If you run a Telegram channel, you know the pain: you need content every single day.
A typical workflow:
- Find an interesting article — 15 min
- Rewrite it in your own voice — 30 min
- Find or create an image — 15 min
- Format and publish — 10 min
~1 hour per post. At 2-3 posts per day, it's a full-time job. Hiring an SMM manager costs $200-500/month — too much for a small channel.
I decided to automate this pipeline with AI.
What the Bot Does
Four input modes, one output — a ready-to-publish post with a cover image:
| Mode | Input | What it does |
|---|---|---|
| 🔗 URL → Post | Article link | Parses, analyzes, rewrites |
| 💬 Topic → Post | Text ("Bitcoin hits $100K") | Generates post from scratch |
| 🎤 Voice → Post | Voice message | Transcribes → generates post |
| 📅 Content Plan | Channel topic | 7 posts for the week |
Each post comes with 4 style options (Professional, Hype, Expert, Brief) and a "Try Again" button for regeneration.
Architecture
┌────────────────────────────────────────┐
│ Telegram User │
│ (URL / text / voice / /plan) │
└──────────────────┬─────────────────────┘
↓
┌────────────────────────────────────────┐
│ Telegraf (Webhook) │
│ Express.js Server │
│ Render (Free Tier) │
└───┬──────────┬──────────┬──────────────┘
↓ ↓ ↓
┌────────┐ ┌────────┐ ┌──────────┐
│ Parser │ │ AI │ │ DB │
│Cheerio │ │Service │ │ Service │
│ │ │ │ │ │
│• HTML │ │•Gemini │ │•Supabase │
│ parse │ │ 2.5 │ │ (PgSQL) │
│• OG │ │•Pollin-│ │•Limits │
│ tags │ │ ations │ │•Users │
└────────┘ └────────┘ └──────────┘
Parsing Articles: Cascade Strategy
My first naive attempt — $('p').text() — worked on 30% of websites. The rest returned garbage: navigation, ads, footers.
The solution — a cascade with fallbacks:
// Remove noise
$('script, style, nav, footer, header, aside, .ads, .sidebar, .comments').remove();
// Title: OG > H1 > title tag
const title = $('meta[property="og:title"]').attr('content')
|| $('h1').first().text().trim()
|| $('title').text().trim();
// Content: article > main > p > body
let content = '';
if ($('article').length) {
$('article p').each((i, el) => {
const text = $(el).text().trim();
if (text.length > 30) content += text + '\n\n';
});
}
if (!content && $('main').length) {
$('main p').each((i, el) => { /* same logic */ });
}
if (!content) {
$('p').each((i, el) => { /* fallback */ });
}
Key insight: filtering by length (text.length > 30). Short <p> tags are usually image captions, buttons, and navigation. Real article paragraphs are always longer.
Prompt Engineering: 3 Lessons Learned
Lesson 1: HTML, not Markdown
Telegram supports both formats, but Markdown is extremely fragile. One unclosed asterisk breaks the entire message. AI loves using * for emphasis, and Telegram interprets it as formatting.
// Before: ~40% formatting errors
ctx.reply(postText, { parse_mode: 'Markdown' });
// After: ~2% formatting errors
ctx.reply(postText, { parse_mode: 'HTML' });
In the prompt:
RULES:
1. Use ONLY HTML tags: <b>, <i>, <code>.
2. DO NOT use Markdown (no * or #).
Lesson 2: Length constraints
Without explicit limits, Gemini writes 5000+ character essays. Telegram posts are read on the go — optimal length is 800-2000 characters.
Lesson 3: Ban URLs
AI loves making up URLs. Especially "authoritative" links to non-existent studies. Just ban them:
Do not add any links.
I add the source link programmatically.
Image Generation: Two-Step Pipeline
Problem: asking AI to "draw an image about bitcoin" produces generic abstract backgrounds.
Solution: Gemini generates a visual prompt, then Flux renders it.
Step 1: Gemini creates the prompt
async function generateImagePrompt(title, postText) {
const prompt = `
Create an image description for an AI image generator.
RULES:
1. English only.
2. Describe CONCRETE objects (buildings, people, devices).
3. Avoid abstractions ("innovation", "justice").
4. Specify style, lighting, composition.
5. Output ONLY the prompt text (1-2 sentences).
`;
return await callGemini(prompt);
}
Step 2: Flux renders the image
async function generateImage(imagePrompt) {
const seed = Math.floor(Math.random() * 1000000);
const encoded = encodeURIComponent(imagePrompt);
const url = `https://image.pollinations.ai/prompt/${encoded}` +
`?width=1024&height=1024&nologo=true&model=flux&seed=${seed}`;
const response = await axios.get(url, {
responseType: 'arraybuffer', timeout: 60000
});
return Buffer.from(response.data, 'binary');
}
Pollinations + Flux — completely free API, no keys required. Quality comparable to Midjourney v4. Random seed ensures uniqueness.
Voice Transcription via Gemini
A non-obvious capability: Gemini 2.5 Flash handles audio natively. Telegram sends voice messages as OGG — Gemini accepts it directly:
async function transcribeVoice(audioBuffer) {
const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" });
const result = await model.generateContent([
"Transcribe this voice recording. Output ONLY the spoken text.",
{
inlineData: {
data: audioBuffer.toString('base64'),
mimeType: 'audio/ogg'
}
}
]);
return result.response.text().trim();
}
Works with Russian speech, even with background noise. Speed: 2-3 seconds for a 30-second recording.
Auto-Failover for AI Models
Google periodically throttles model access by quota. To prevent downtime, I implemented a cascade:
const MODELS = ["gemini-2.5-flash", "gemini-2.5-flash-lite", "gemini-2.5-pro"];
async function callGemini(prompt) {
for (const modelName of MODELS) {
try {
const model = genAI.getGenerativeModel({ model: modelName });
const result = await model.generateContent(prompt);
return result.response.text();
} catch (e) {
continue; // Try next model
}
}
throw new Error('All models failed');
}
Zero downtime in a month of production use.
Freemium Monetization
- Free: 3 generations/day, all features
- Premium ($2/month): Unlimited generations, no watermark
const DAILY_LIMIT = 3;
async function checkUserLimit(userId) {
let { data: user } = await supabase
.from('users').select('*').eq('id', userId).single();
if (user?.is_premium) return { canGenerate: true };
const hours = (Date.now() - new Date(user.last_generation_at)) / 3600000;
if (hours > 24) {
// Reset daily counter
await supabase.from('users')
.update({ generations_count: 0 }).eq('id', userId);
return { canGenerate: true, remaining: DAILY_LIMIT };
}
return user.generations_count >= DAILY_LIMIT
? { canGenerate: false }
: { canGenerate: true, remaining: DAILY_LIMIT - user.generations_count };
}
Total Infrastructure Cost
| Component | Service | Cost |
|---|---|---|
| AI text | Gemini 2.5 Flash | $0 |
| AI images | Pollinations (Flux) | $0 |
| Database | Supabase Free | $0 |
| Hosting | Render Free | $0 |
| Total | $0/month |
Try It
The bot is free and runs 24/7: @generatortposBot
Landing page: project-3-411a.onrender.com
If you know Node.js and Telegram Bot API, you can build something like this in a weekend. The entire stack runs on free tiers.
Happy to answer questions in the comments!
Top comments (0)