<?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: Tanuj</title>
    <description>The latest articles on DEV Community by Tanuj (@replygen).</description>
    <link>https://dev.to/replygen</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%2F3948860%2F88904f6e-2528-4dbc-ae61-6fee32881117.png</url>
      <title>DEV Community: Tanuj</title>
      <link>https://dev.to/replygen</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/replygen"/>
    <language>en</language>
    <item>
      <title>I Built a Chrome Extension That Generates AI Replies for LinkedIn, X and Threads — Here's What I Learned</title>
      <dc:creator>Tanuj</dc:creator>
      <pubDate>Sun, 24 May 2026 11:10:04 +0000</pubDate>
      <link>https://dev.to/replygen/i-built-a-chrome-extension-that-generates-ai-replies-for-linkedin-x-and-threads-heres-what-i-55gl</link>
      <guid>https://dev.to/replygen/i-built-a-chrome-extension-that-generates-ai-replies-for-linkedin-x-and-threads-heres-what-i-55gl</guid>
      <description>&lt;p&gt;Building &lt;a href="https://www.replygen.app" rel="noopener noreferrer"&gt;Replygen&lt;/a&gt; started as a personal itch. As a solo SaaS founder, I needed to stay consistently active on LinkedIn, X, and Threads to drive organic distribution — but crafting thoughtful replies at scale was burning time I didn't have. So I built an AI engagement co-pilot that lives in your browser and suggests context-aware replies as you scroll your feed.&lt;/p&gt;

&lt;p&gt;This post is a technical walkthrough of the key architecture decisions, the unexpected challenges, and the lessons that only come from shipping a real Chrome extension to production users.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Architecture
&lt;/h2&gt;

&lt;p&gt;The extension follows the standard &lt;strong&gt;content script + background service worker&lt;/strong&gt; pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Content script&lt;/strong&gt; — injected into LinkedIn/X/Threads DOM. Handles UI injection (the suggestion overlay), reads post content via DOM selectors, sends data to the background worker via &lt;code&gt;chrome.runtime.sendMessage&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background service worker&lt;/strong&gt; — handles LLM API calls, manages auth tokens, persists preferences via &lt;code&gt;chrome.storage.sync&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Side panel&lt;/strong&gt; — settings, tone configuration, usage stats&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is clean in theory. In practice, the DOM layer is where all the pain lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  The DOM Fragility Problem
&lt;/h2&gt;

&lt;p&gt;LinkedIn, X, and Threads update their frontend constantly. Selectors that worked last week break silently this week — no errors, just a blank suggestion box.&lt;/p&gt;

&lt;p&gt;The solution we landed on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;POST_SELECTORS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;linkedin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.feed-shared-update-v2__description&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.feed-shared-text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-test-id="main-feed-activity-card"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;  &lt;span class="c1"&gt;// fallback&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;twitter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-testid="tweetText"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.tweet-text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;  &lt;span class="c1"&gt;// legacy fallback&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;threads&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;._a9zs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[class*="x1iorvi4"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;  &lt;span class="c1"&gt;// brittle, needs frequent updates&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;We run a &lt;strong&gt;selector health check on extension startup&lt;/strong&gt; — if the primary selector returns null, it falls through to the next. When all fallbacks fail, the UI gracefully shows "Content unavailable" rather than throwing an error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompt Design: Making Replies Sound Like You
&lt;/h2&gt;

&lt;p&gt;The first version generated generic, professional replies. Users hated it. The fix was a &lt;strong&gt;three-layer prompt structure&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Layer 1: Platform context&lt;br&gt;
"You are writing a LinkedIn comment. LinkedIn has a professional, insight-driven tone..."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Layer 2: Post context&lt;br&gt;
"The post you are replying to says: [post_content]&lt;br&gt;
Thread context: [thread_context]"&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Layer 3: User tone profile&lt;br&gt;
"The user's writing style: [tone_descriptor]&lt;br&gt;
Examples of their recent comments: [sample_1], [sample_2]"&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Constraints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Under 280 chars for X replies&lt;/li&gt;
&lt;li&gt;Never start with "Great post!" or similar filler&lt;/li&gt;
&lt;li&gt;Always add a unique perspective, not just agreement text&lt;/li&gt;
&lt;li&gt;The tone profile is built from the user's last 15–20 posts, summarized into a descriptor. This one change improved day-7 retention significantly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Handling API Costs at Scale
&lt;/h2&gt;

&lt;p&gt;Two optimizations that cut costs by ~40%:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Prompt caching&lt;/strong&gt; — platform context and user tone profile are static between requests. Using Anthropic's prompt caching on these portions means you only pay full tokens for the dynamic post content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Generation gating&lt;/strong&gt; — only fire the API call when the user explicitly clicks "Generate Reply." Early versions pre-generated proactively and burned tokens on posts users never engaged with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Auth Architecture
&lt;/h2&gt;

&lt;p&gt;Users authenticate with Google OAuth for account management and cross-device sync. Platform sessions (LinkedIn, X, Threads) are never stored — the extension operates on whatever browser session is already active. Zero credential storage liability, zero OAuth integration work per platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  The UX Insight That Changed Everything
&lt;/h2&gt;

&lt;p&gt;Early version had a two-step flow: generate → confirm → post. Collapsed to a single-click post flow (with a 3-second undo toast) — this &lt;strong&gt;doubled daily active usage&lt;/strong&gt; in the week after shipping.&lt;/p&gt;

&lt;p&gt;Every extra click in a workflow tool is a reason to abandon the habit. Optimize ruthlessly for speed of the happy path.&lt;/p&gt;




&lt;p&gt;If you're building something in the Chrome extension + AI space, happy to compare notes. Try the extension at &lt;a href="https://www.replygen.app" rel="noopener noreferrer"&gt;replygen.app&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>chromeextension</category>
      <category>ai</category>
      <category>saas</category>
      <category>buildinpublic</category>
    </item>
  </channel>
</rss>
