<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: landigf</title>
    <description>The latest articles on DEV Community by landigf (@landigf).</description>
    <link>https://dev.to/landigf</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3874166%2F88c5c1bf-95cc-421b-9299-87d3977eb6b8.jpeg</url>
      <title>DEV Community: landigf</title>
      <link>https://dev.to/landigf</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/landigf"/>
    <language>en</language>
    <item>
      <title>I built a Telegram bot that reads 70 arXiv papers a day so I don't have to</title>
      <dc:creator>landigf</dc:creator>
      <pubDate>Sat, 11 Apr 2026 22:18:52 +0000</pubDate>
      <link>https://dev.to/landigf/i-built-a-telegram-bot-that-reads-70-arxiv-papers-a-day-so-i-dont-have-to-19b5</link>
      <guid>https://dev.to/landigf/i-built-a-telegram-bot-that-reads-70-arxiv-papers-a-day-so-i-dont-have-to-19b5</guid>
      <description>&lt;h1&gt;
  
  
  I built a Telegram bot that reads 70 arXiv papers a day so I don't have to
&lt;/h1&gt;

&lt;h2&gt;
  
  
  the problem
&lt;/h2&gt;

&lt;p&gt;i was drowning in arXiv. I had 30 tabs of Zotero saved papers I'd never opened, an inbox full of unread newsletter digests, and the creeping certainty that someone, somewhere, had already published the exact thing I was about to spend 3 weeks "discovering."&lt;/p&gt;

&lt;p&gt;I tried everything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;arXiv RSS feeds&lt;/strong&gt; → too noisy. 100+ papers a day, no signal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email newsletters&lt;/strong&gt; → I never opened them. My subconscious classifies "newsletter" alongside "marketing email."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Twitter accounts that summarize papers&lt;/strong&gt; → algorithmic, not personalized to my niche.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Just trying harder&lt;/strong&gt; → did not work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The real question I kept failing to answer was: &lt;strong&gt;what changed in my exact subfield in the last 24 hours, and is it worth my time?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I built it.&lt;/p&gt;

&lt;h2&gt;
  
  
  what it is
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/landigf/Broletter" rel="noopener noreferrer"&gt;Broletter&lt;/a&gt; is a Telegram bot. Every morning it sends me &lt;strong&gt;one short message&lt;/strong&gt; with 4 sections:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🔬 Daily Science — Saturday, 12 Apr

Tap a section to read it. Skip the rest.

💡 Virtual Memory: The OS Magic You Use Daily
   Deep Curiosity — Ever wonder how your computer juggles so many apps?
   Dive into the surprisingly elegant system that makes it all possible.

📄 Benchmarking Science: Extracting Experiments from Papers
   Research Spotlight — Chong and Colindres introduce LitXBench, a new
   tool to automatically extract experimental details from scientific
   literature for materials science.

⚡ The Universe: Humanity's First Computer?
   Quick Bites — Could the entire universe have functioned as a giant
   computer running the laws of physics since the Big Bang?

🎯 Testing APIs Beyond Basic CRUD Operations
   Your Research Corner — Yang et al. propose a new log-based approach
   for API testing that accounts for complex business logic, going
   beyond simple OpenAPI specs.

   [📖 Curiosity] [📖 Research]
   [📖 Bites] [📖 Corner]
   [📖 Read all] [⏭ Skip today]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each section is a &lt;strong&gt;one-line preview + a tap-to-expand button&lt;/strong&gt;. I tap what looks interesting, full content arrives, the rest stays hidden. Reactions tune what I see tomorrow.&lt;/p&gt;

&lt;p&gt;That's the entire UX. It works because it removes the only thing that ever broke my reading habit: the wall of text that makes me say "I'll read this later" and never return.&lt;/p&gt;

&lt;p&gt;Useful mostly for STEM, but you can configure it for internships, startup news, funding rounds, lab updates, etc. It's a delivery mechanism, not a content silo.&lt;/p&gt;

&lt;h2&gt;
  
  
  how I got there (the embarrassing first version)
&lt;/h2&gt;

&lt;p&gt;Version 1 was a wall of text. 5 sections, each ~300 words, sent as separate Telegram messages with reaction buttons. Looked like an actual newsletter. I shipped it to a Telegram group of friends.&lt;/p&gt;

&lt;p&gt;A friend wrote back the next day:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"no bro troppo lungo non me lo leggerò mai. Cioè tipo meglio che mi fai un sunto ultra veloce e io dico subito cosa mi interessa e cosa no e tu mi mandi il messaggio completo"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;(Italian for: "too long, I'll never read it. Better if you give me an ultra-quick summary and I tell you what interests me, then you send the full thing.")&lt;/p&gt;

&lt;p&gt;Then I checked Firestore. Across 8 users:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;7 votes for "shorter"&lt;/strong&gt;, 4 for "perfect", 0 for "longer"&lt;/li&gt;
&lt;li&gt;Only 3 of 8 users had ever pressed a reaction button at all&lt;/li&gt;
&lt;li&gt;The friend who wrote the feedback had &lt;strong&gt;zero&lt;/strong&gt; reactions before giving up&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's not a tuning problem. That's a fundamental UX failure. I rebuilt it in 2 days as the preview-card flow you see above.&lt;/p&gt;

&lt;h2&gt;
  
  
  the architecture (this is the part you came for)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    ┌─────────────────────┐
                    │  Cloud Scheduler    │
                    │  (cron, daily 8pm)  │
                    └──────────┬──────────┘
                               │
                    ┌──────────▼──────────┐
                    │  Cloud Run Job:     │
                    │  prefetch-papers    │  ← One arXiv fetch
                    │                     │     for ALL users
                    └──────────┬──────────┘
                               │ writes
                    ┌──────────▼──────────┐
                    │  Firestore          │
                    │  /papers_cache/     │
                    └──────────┬──────────┘
                               │ reads
                    ┌──────────▼──────────┐
                    │  Cloud Run Job:     │
                    │  generate-all       │  ← Generates per-user
                    │                     │     newsletters
                    └──────────┬──────────┘
                               │
                    ┌──────────▼──────────┐
                    │  Telegram Bot API   │
                    │  (preview cards)    │
                    └─────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python 3.13&lt;/strong&gt; + FastAPI (web container handling Telegram webhook)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemini 2.5 Flash&lt;/strong&gt; + &lt;strong&gt;Flash-Lite&lt;/strong&gt; (mixed — see cost section)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firestore&lt;/strong&gt; (multi-tenant, one doc per user)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud Run + Cloud Run Jobs&lt;/strong&gt; (web stays warm, batch jobs run on schedule)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Telegram Stars&lt;/strong&gt; payments — users pay in Telegram's native currency, no credit card&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  cost engineering (this is the part you really came for)
&lt;/h2&gt;

&lt;p&gt;I started on Gemini 2.5 Flash, with everything default. Projected cost at 100 users: &lt;strong&gt;~$1.20 per user per month&lt;/strong&gt;. I wanted to charge less than $3/month and still profit, so that was way too high. Three changes dropped it to ~5 cents per user per month.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. disable thinking tokens (the big one — 3-10x cost cut)
&lt;/h3&gt;

&lt;p&gt;Gemini 2.5 Flash uses "thinking" tokens by default. They're billed at output rate but they're invisible to you — you don't see them in the response, but they multiply your bill. For creative writing tasks (which is what newsletter generation is), thinking adds nothing. Disable it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;genai&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-2.5-flash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;contents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;genai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GenerateContentConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;system_instruction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;thinking_config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;genai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ThinkingConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thinking_budget&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single line cut my output token usage by 3-10x depending on the prompt. I verified by logging actual &lt;code&gt;usage_metadata.candidates_token_count&lt;/code&gt; before and after — the visible output tokens stayed the same, and the bill dropped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson&lt;/strong&gt;: thinking is for math and code. For creative writing, it's burning money without improving quality.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. two-tier model split
&lt;/h3&gt;

&lt;p&gt;Not all sections deserve the same model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Curiosity&lt;/strong&gt;, &lt;strong&gt;Research Spotlight&lt;/strong&gt;, &lt;strong&gt;Quick Bites&lt;/strong&gt; — these are the same for any user with overlapping interests. They benefit from Flash-Lite ($0.10 in / $0.40 out per 1M tokens, 6x cheaper than Flash standard).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Personal research section&lt;/strong&gt; — this is hyper-personalized to each user's exact research keywords and feedback history. It needs the better model. I use Flash standard here.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PoolGenerator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Cheap shared sections — Flash-Lite, generic system prompt&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-2.5-flash-lite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NewsletterGenerator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Per-user personal section + Sunday recap — Flash, personalized prompt&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-2.5-flash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. content pool architecture
&lt;/h3&gt;

&lt;p&gt;Sections that don't need to be unique per user shouldn't be generated per user. I generate a daily "pool" of curiosity articles (one per theme), research spotlights (one per top paper), and quick bites (3 sets) — once per day, globally. Then for each user, I assemble their newsletter from the pool by matching their interests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_daily_pool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Runs ONCE per day, regardless of user count.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;themes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;collect_unique_themes_across_all_users&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;theme&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;themes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;save_pool_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curiosity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                       &lt;span class="n"&gt;pool_gen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;curiosity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="c1"&gt;# Same for research spotlights and quick bites
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;assemble_for_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Runs per user. Picks from pool + generates only personal sections.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;sections&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;sections&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curiosity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_pool_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curiosity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sections&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;research&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_pool_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;research&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;best_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;sections&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quick_bites&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_pool_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quick_bites&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rotating&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sections&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;personal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;personal_gen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sections&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The math at 100 users:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pool generation: ~16 LLM calls/day (8 themes + 5 papers + 3 quick bite sets), once. &lt;strong&gt;Fixed cost: ~$0.12/month&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Per-user generation: 1 personal call + 1 Sunday recap call. &lt;strong&gt;Per-user cost: ~$0.045/month&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Total at 100 users: ~$4.62/month&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At $1/month per user revenue: &lt;strong&gt;95% margin&lt;/strong&gt;. Even at $0.50/month: 89%.&lt;/p&gt;

&lt;h3&gt;
  
  
  bonus: LLM-generated preview hooks (the part where I learned to not be lazy)
&lt;/h3&gt;

&lt;p&gt;The preview card needs a one-line hook per section. My first attempt: parse the first bold phrase or first sentence of each generated section.&lt;/p&gt;

&lt;p&gt;The user (same friend, different feedback) responded:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"How do you think this could be a good solution just putting the first sentence? You need to write a short summary, use an API call idk, but need more details to understand if I like it."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;He was right. The first sentence is a hook for the section author, not for the section reader. I added a separate Flash-Lite call after generation that takes all sections as input and returns structured JSON &lt;code&gt;{title, teaser}&lt;/code&gt; for each. Cost per call: ~$0.0002. So &lt;strong&gt;$0.0066 per user per month&lt;/strong&gt; for genuinely useful previews.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_generate_previews&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sections&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--- &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ---&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
             &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PREVIEW_PROMPT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sections_text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-2.5-flash-lite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;contents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;genai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GenerateContentConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;thinking_config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;genai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ThinkingConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thinking_budget&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;response_mime_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  what I learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Default to "off" for thinking tokens&lt;/strong&gt; unless your task is reasoning. The default in the SDK is "auto" which means "on for Flash."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The first user feedback that hurts is the most valuable.&lt;/strong&gt; I was proud of v1. The "troppo lungo" text was the best thing that happened to the product.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architecture is for the cost sheet, not the org chart.&lt;/strong&gt; A multi-tenant content pool sounds enterprise-y. It's actually 80 lines of Python and saves 90% of your LLM bill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A 1-line &lt;code&gt;thinking_budget=0&lt;/code&gt; is worth more than any prompt engineering you'll do this week.&lt;/strong&gt; Try it on your existing app right now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Telegram Stars are underrated for indie devs.&lt;/strong&gt; No Stripe setup, no chargebacks, no PCI compliance, no credit card forms. You get paid in Stars → withdraw to TON → swap to fiat. Telegram takes 0% on the withdrawal.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  try it
&lt;/h2&gt;

&lt;p&gt;It's open source: &lt;a href="https://github.com/landigf/Broletter" rel="noopener noreferrer"&gt;github.com/landigf/Broletter&lt;/a&gt; — MIT, you can self-host.&lt;/p&gt;

&lt;p&gt;Or try the hosted version: &lt;a href="https://t.me/BroletterBot?start=devto" rel="noopener noreferrer"&gt;@BroletterBot&lt;/a&gt; on Telegram. 7-day free trial, no credit card. Then 50/100/150 Stars per month (~$0.75–$2.25). If you find a bug, &lt;code&gt;/feedback&lt;/code&gt; in the bot goes straight to my Telegram.&lt;/p&gt;

&lt;p&gt;Brutal feedback wanted. The product is ~2 weeks old.&lt;/p&gt;

</description>
      <category>telegram</category>
      <category>gemini</category>
      <category>ai</category>
      <category>indiehackers</category>
    </item>
  </channel>
</rss>
