Andrii PohuliailoSenior Backend Engineer

← log

Cutting Gemini API Costs by 87% on a Telegram News Pipeline

Living in Ukraine right now means drowning in an endless deluge of news. Every few minutes, phone screens light up with push notifications from dozens of Telegram channels. Some channels post breaking battlefield updates, others share local infrastructure alerts, and a surprising number publish paid political propaganda disguised as genuine reporting. In Ukrainian tech circles, we call these sponsored or hidden political posts "джинса" (dzhynsa).

Hand-drawn black ink illustration showing a person working on a laptop inside a house, while outside the window missiles arc across the sky, explosions burst into smoke plumes, and a Telegram paper plane logo flies among the rocket trails.

After months of checking Telegram every twenty minutes, I realized I was spending over two hours a day scrolling through repetitive headlines. Half the posts across different channels were identical copies forwarded from the same origin. The other half were low-value noise or unverified rumors. I felt completely lost in the flow.

I decided to fix this for myself by building a side project called Digest Curator. The idea was straightforward: run a backend bot that reads all the news channels I follow, filters out duplicates and paid noise, clusters similar stories using vector embeddings, and generates clean daily, weekly, monthly, and yearly summaries using Gemini. The bot posts digest links directly to a Telegram channel and Telegraph pages.

The initial proof of concept worked, but it immediately hit a wall. Within three days of running the ingestion pipeline, my Gemini API bill shot up to forty cents a day. That might sound like small change, but for a simple personal side project that runs automated crons indefinitely, forty cents a day means twelve dollars a month just to summarize public news posts. I knew I had to refactor the architecture, cut token consumption, and make the pipeline significantly smarter.

Along the way, I also built an interactive vector map of news posts at pogulailo.com/map, which converts text embeddings into 2D coordinates so you can visually explore topic clusters in real time. Here is the honest story of how I fixed the token burn, why Clean Architecture is even more critical when building with AI, and how Spec-Driven Development kept the codebase sane.

The Initial Prototype and API Costs

When I built the first version of Digest Curator, I took the fastest path to a working prototype. I set up a Telegram client worker to ingest incoming raw posts from twenty target channels into a database. Then, once a day, a scheduler gathered every raw message from the last twenty-four hours and sent them directly to Gemini in a single prompt.

My initial prompt construction looked something like this:

text
Summarize the following Telegram posts into coherent news topics:
- Channel: @kyiv_news_live | Fair Reference: true | Message ID: 1042 | Text: Explosions reported near Kyiv region...
- Channel: @kyiv_news_live | Fair Reference: true | Message ID: 1043 | Text: Air defense active over the region...
- Channel: @ukraine_now | Fair Reference: false | Message ID: 8819 | Text: Explosions reported near Kyiv region...

I repeated full channel handles, fair-reference boolean flags, and message IDs on every single line of text. Across twenty active channels, a typical day brought in between five hundred and nine hundred posts.

When I checked the Google Cloud Console telemetry after the first weekend, the numbers shocked me. The ingestion pipeline was consuming roughly 3 million prompt input tokens per day. Each daily digest summarization call sent nearly 200,000 input tokens because raw posts were filled with duplicated text, forwarded channel disclaimers, media captions, and verbose metadata labels. On top of that, Gemini was generating around 400,000 output tokens per day because I had not set any output limits on the response format.

Output tokens on Gemini cost roughly four times more per token than input tokens. The result was a daily charge of $0.40 per day, or about $12.40 per month.

  • Daily input tokens: ~3,000,000 tokens / day
  • Daily output tokens: ~400,000 tokens / day
  • Daily Gemini cost: ~0.40/day(0.40 / day (12.40 / month)

For a commercial enterprise app, twelve bucks a month is roundoff error. But for an automated personal tool running on a small server, spending twelve dollars every month on API calls for a cron job felt like poor engineering. Worse, the summaries were not even that accurate because raw prompt dumps overloaded the model context with unclustered noise.

Hand-drawn illustration of a developer scratching his head in confusion while holding a bill, with handwritten text asking HOW MUCH? next to a laptop and a torn Google envelope on the floor.

Optimizing Token Usage and Mapping Embeddings

To fix the cost problem without degrading the summary quality, I had to redesign how data was prepared before hitting the LLM. The goal was to cut token volume while giving Gemini clearer structure.

Compact Header Mapping

Instead of repeating channel handles and metadata flags on every message line, I moved channel definitions into a shared lookup header at the top of the prompt. Message lines were compressed into a minimal array format:

text
Channels:
[0] @kyiv_news_live (fair)
[1] @ukraine_now (unverified)

Messages:
[0] (c:0, id:1042): Explosions reported near Kyiv region.
[1] (c:0, id:1043): Air defense active over the region.
[2] (c:1, id:8819): Explosions reported near Kyiv region.

This single formatting change eliminated over 50% of repetitive prompt text.

Pre-Filtering and Noise Reduction

Next, I updated the ingestion domain logic to filter out non-informative posts before prompt assembly. The worker automatically dropped:

  • Empty posts or media-only messages containing no text content.
  • Standalone duplicate forwards that contained identical text to an earlier post from another channel.
  • Short promotional posts containing sponsored referral links.

All candidate-linked messages and high-priority posts were preserved, but raw noise was stripped out entirely.

Model Configuration and Token Limits

Throughout the project, I relied strictly on gemini-3.6-flash for all text summarization and content analysis via the @google/genai SDK. In the initial prototype setup, however, I was making calls without output token constraints or structured schemas. I updated the Gemini infrastructure adapter to enforce strict generation parameters and cap max output tokens:

typescript
const response = await this.ai.models.generateContent({
  model: 'gemini-3.6-flash',
  contents: compressedPrompt,
  config: {
    maxOutputTokens: 4096,
    temperature: 0.2,
    responseMimeType: 'application/json',
    responseSchema: MESSAGE_SUMMARY_SCHEMA,
  },
});

Capping maxOutputTokens prevented the model from generating unnecessarily long intros or repetitive paragraphs. I also implemented draft caching so that retrying a failed cron or re-running a digest draft for the same date range reuses the existing draft instead of making fresh Gemini API requests.

The telemetry numbers after applying these fixes were night and day:

  • Daily input tokens: < 800,000 tokens / day (73% reduction)
  • Daily output tokens: < 80,000 tokens / day (80% reduction)
  • Daily Gemini cost: < $0.05 / day (87%+ total cost drop)

The daily spend dropped from forty cents down to less than five cents a day, landing under $1.50 per month while maintaining 100% citation link accuracy (⟦LINK:n⟧ and ⟦SRC:id⟧ placeholders resolving cleanly to Telegraph and Telegram posts).

Mapping Post Embeddings

Once the text processing pipeline was clean and cheap, I wanted a visual way to see how news topics evolved over time. I added a vector embedding step using Gemini's text embedding model to convert every ingested post into a high-dimensional vector space representation.

To visualize these vectors on a regular browser screen, I implemented a dimensionality reduction pipeline (using UMAP coordinate projection) that translates high-dimensional post embeddings into 2D map coordinates (X, Y). I built a public Map API endpoint and integrated a WebGL rendering frontend hosted at pogulailo.com/map.

Hand-drawn napkin illustration of a stick-figure scuba diver swimming underwater among distinct, grouped schools of small fish clustering together across open water.

Now, instead of just reading text digests, I can open the map and see clusters of news topics forming visually. Breaking military events form tight, dense clusters, while local utility announcements group into distinct separate islands. It gives an immediate bird's-eye view of what Ukrainian media is discussing at any given hour.

Clean Architecture and Spec-Driven Development

Working on Digest Curator reinforced two core engineering beliefs that I think are vital for anyone building AI-powered software today.

Protecting Domain Logic from Volatile APIs

A lot of developers think that with modern LLMs, software architecture does not matter as much because "the AI handles the logic". In reality, the exact opposite is true. Non-deterministic AI APIs make clean architectural boundaries far more important than in traditional web applications.

AI providers change constantly. Models get deprecated, pricing tiers shift, context windows expand, and output formats occasionally hallucinate. If your core business logic (like news filtering, digest scheduling, or post clustering) is tightly coupled to direct Gemini SDK calls, any change from the provider will break your entire codebase.

In Digest Curator, I applied Clean Architecture and Domain-Driven Design (DDD) principles using NestJS:

text
app/src/
├── domain/            # Pure Entities: Digest, NewsItem, Channel, Cluster, MapPoint
├── application/       # Use Cases: SummarizePeriodDigest, RenderNewsMap
├── infrastructure/    # Adapters: GeminiProvider, TelegramBotWorker, TelegraphPublisher, PrismaDb
└── presentation/      # Controllers: MapApiController, TelegramWebhookController

The domain entities (Digest, NewsItem, Channel, Cluster) have zero dependencies on Gemini, Telegram, or Prisma. The Gemini integration lives entirely inside an infrastructure adapter (infrastructure/gemini) implementing a domain interface:

typescript
export interface ContentAnalysisPort {
  summarizeMessages(payload: CompressedPromptPayload): Promise<MessageSummaryResult>;
}

When I refactored the prompt payload compression format and added structured JSON response schemas to Gemini, I did not have to touch a single line of domain or application logic. I updated the infrastructure provider, verified the interface contract, and deployed. That isolation is what allowed me to experiment with cost optimizations without risking regressions in digest generation.

Hand-drawn illustration of a smiling developer working at a laptop under a large umbrella that shields him and his desk from a heavy rainstorm.

Spec-Driven Development with SpecKit

The second major takeaway from this project was the power of Spec-Driven Development (SDD). When building with AI coding tools, it is very easy to fall into a trap of informal prompting: typing vague requests, getting messy code back, and spending hours manually debugging side effects.

To keep the project structured, I used GitHub SpecKit (.specify/ directory structure with spec.md, plan.md, and tasks.md). Instead of asking an AI tool to "make the Gemini calls cheaper", the workflow required writing explicit requirements:

  1. spec.md: Defined measurable success criteria (e.g., "Reduce daily input token count by at least 60% while maintaining 100% citation link resolution").
  2. plan.md: Documented the technical approach, payload compression formats, and domain boundaries.
  3. tasks.md: Generated an actionable, dependency-ordered checklist mapping every model and service change to clear file paths.

Having clear specifications turned AI pair programming from a chaotic guessing game into a disciplined execution pipeline. The AI assistant knew exactly what files to modify, what constraints to respect, and how to verify success.

Building Digest Curator started as a personal attempt to survive news fatigue in Ukraine. It turned into a great sandbox for testing LLM cost engineering, vector embeddings visualization, and clean backend architecture.

If you are building your own AI side projects or cron services, keep your domain logic isolated from external AI SDKs, compress your prompt payloads before sending them over the wire, and structure your features with clear specifications before writing code. If you want to see what the post-embedding map looks like in action, check out pogulailo.com/map. I am currently working on expanding the clustering engine to detect automated bot networks across Telegram channels, and I will share those findings in a future logbook entry.

© 2026 Andrii Pohuliailo. All rights reserved.building software at scale