DEV Community

Cover image for # I Built an AI Content Hub for Telegram That Writes Posts in 30 Seconds — For $0/month
Alexandr Zavoznikov
Alexandr Zavoznikov

Posted on

# I Built an AI Content Hub for Telegram That Writes Posts in 30 Seconds — For $0/month

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:

  1. Find an interesting article — 15 min
  2. Rewrite it in your own voice — 30 min
  3. Find or create an image — 15 min
  4. 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    │
└────────┘ └────────┘ └──────────┘
Enter fullscreen mode Exit fullscreen mode

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 */ });
}
Enter fullscreen mode Exit fullscreen mode

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' });
Enter fullscreen mode Exit fullscreen mode

In the prompt:

RULES:
1. Use ONLY HTML tags: <b>, <i>, <code>.
2. DO NOT use Markdown (no * or #).
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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');
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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');
}
Enter fullscreen mode Exit fullscreen mode

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 };
}
Enter fullscreen mode Exit fullscreen mode

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)