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 (1)

Collapse
 
beiluo profile image
刘北洛

The remaining judgment step is visual relevance. A unique cover can still be generic if it only mirrors keywords. I’d have the bot produce a one-line visual thesis first, then choose among literal scene, metaphor, or explanatory diagram, and reject covers too similar to the channel’s last 10 posts. The prompt, model, and source URL should travel with the asset for provenance.