<?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: Tridib Banik</title>
    <description>The latest articles on DEV Community by Tridib Banik (@tridibbanik17).</description>
    <link>https://dev.to/tridibbanik17</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4023406%2Fcecb58b3-879f-4030-bba4-92207786a88b.jpg</url>
      <title>DEV Community: Tridib Banik</title>
      <link>https://dev.to/tridibbanik17</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tridibbanik17"/>
    <language>en</language>
    <item>
      <title>I built a Chrome extension that grades stocks with deterministic rules; here's the privacy architecture</title>
      <dc:creator>Tridib Banik</dc:creator>
      <pubDate>Wed, 26 Aug 2026 05:14:44 +0000</pubDate>
      <link>https://dev.to/tridibbanik17/i-built-a-chrome-extension-that-grades-stocks-with-deterministic-rules-heres-the-privacy-2lm8</link>
      <guid>https://dev.to/tridibbanik17/i-built-a-chrome-extension-that-grades-stocks-with-deterministic-rules-heres-the-privacy-2lm8</guid>
      <description>&lt;p&gt;Most stock tools want your broker login, your portfolio data, or give you opaque AI picks you can't verify. I built &lt;a href="https://chromewebstore.google.com/detail/gbgmfkgijoenfnpklkchfbcljnjgkfef" rel="noopener noreferrer"&gt;StockAgent&lt;/a&gt;, a Chrome extension that grades your watchlist using transparent, deterministic rules, and I wanted to share how I designed the privacy layer because I think the pattern is useful beyond finance apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  What StockAgent does (quick context)
&lt;/h2&gt;

&lt;p&gt;Each stock scores 0–5 on five financial metrics:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;+1 if&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Debt-to-Equity&lt;/td&gt;
&lt;td&gt;Below sector threshold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PEG&lt;/td&gt;
&lt;td&gt;Below threshold (varies by sector)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ROE&lt;/td&gt;
&lt;td&gt;Above 15% (or 8–12% for banks/utilities)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;200-day SMA&lt;/td&gt;
&lt;td&gt;Price above moving average&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RSI&lt;/td&gt;
&lt;td&gt;Below 35 (oversold)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On top of this, news headlines from Yahoo Finance RSS are scanned for risk keywords and scored by severity:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Severity&lt;/th&gt;
&lt;th&gt;Penalty&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Severe&lt;/td&gt;
&lt;td&gt;-2 per headline (cap -3)&lt;/td&gt;
&lt;td&gt;Fraud, bankruptcy, SEC, delisting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;td&gt;-1 per headline (cap -2)&lt;/td&gt;
&lt;td&gt;Lawsuit, downgrade, layoffs, earnings miss&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mild&lt;/td&gt;
&lt;td&gt;0 (informational only)&lt;/td&gt;
&lt;td&gt;Guidance cut, analyst concern&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Final score: 4–5 = STRONG BUY, 3 = HOLD, 0–2 = AVOID. Same data = same grade, every time. No AI in the scoring loop.&lt;/p&gt;

&lt;p&gt;The extension also sends scheduled email digests and optionally uses a BYOK Gemini key to explain grades in plain English (never influences the score).&lt;/p&gt;

&lt;h2&gt;
  
  
  The privacy problem
&lt;/h2&gt;

&lt;p&gt;Users track stocks with real money. They want grades, but they don't want their share counts or buy prices on someone else's server. So the challenge was: how do you build a useful cloud feature (email reports) without ever seeing the user's actual portfolio?&lt;/p&gt;

&lt;h2&gt;
  
  
  The solution: three-layer privacy enforcement
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Layer 1: Two-tier storage
&lt;/h3&gt;

&lt;p&gt;Everything lives in &lt;code&gt;chrome.storage.local&lt;/code&gt;, but the code treats it as two separate tiers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;// PRIVATE (never transmitted)&lt;/span&gt;
&lt;span class="na"&gt;holdings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;NVDA&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;shares&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;50&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;buyPrice&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;120.00&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="na"&gt;geminiApiKey&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk-..."&lt;/span&gt;
&lt;span class="na"&gt;autoAnalyze&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="s"&gt;// CLOUD-ELIGIBLE (sent only if user opts into email)&lt;/span&gt;
&lt;span class="na"&gt;watchlist&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NVDA"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AAPL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SHOP.TO"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;delivery&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;email&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;..."&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;...&lt;/span&gt;&lt;span class="pi"&gt;},&lt;/span&gt; &lt;span class="nv"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Layer 2: Allowlist payload construction
&lt;/h3&gt;

&lt;p&gt;When the user clicks "Save &amp;amp; Subscribe," I don't copy the local state and strip private fields. That's fragile. One missed field, and you leak data. Instead, the outbound object is &lt;strong&gt;reconstructed from scratch&lt;/strong&gt; using only allowed fields:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildCloudPayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;delivery&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schedule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalizeSchedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;delivery&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;watchlist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;watchlist&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;watchlist&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="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;normalizeTicker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MAX_WATCHLIST&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;watchlist&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&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;Holdings and API keys are never referenced here. They can't leak because they're never included.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 3: Runtime blocklist assertion
&lt;/h3&gt;

&lt;p&gt;Even after allowlist construction, every payload passes through &lt;code&gt;assertNoPrivateLeak()&lt;/code&gt; before hitting the network:&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;FORBIDDEN_CLOUD_KEYS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;holdings&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="s2"&gt;shares&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="s2"&gt;buyPrice&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="s2"&gt;avgBuyPrice&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="s2"&gt;geminiApiKey&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="s2"&gt;geminiKey&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="s2"&gt;apiKey&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="s2"&gt;autoAnalyze&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="s2"&gt;netWorth&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="s2"&gt;portfolio&lt;/span&gt;&lt;span class="dl"&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;assertNoPrivateLeak&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;FORBIDDEN_CLOUD_KEYS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Refusing to transmit private field: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isPlainObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&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="c1"&gt;// Root-level allowlist — reject ANY unexpected field&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allowed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&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="s2"&gt;watchlist&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="s2"&gt;schedule&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="s2"&gt;enabled&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="s2"&gt;userId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Refusing unexpected cloud field: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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="nx"&gt;payload&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;This catches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A developer accidentally adding a field to the payload&lt;/li&gt;
&lt;li&gt;A merge conflict introducing a private key&lt;/li&gt;
&lt;li&gt;Any nested object containing forbidden fields&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Layer 4 (bonus): Server rejects it anyway
&lt;/h3&gt;

&lt;p&gt;The FastAPI backend uses Pydantic with &lt;code&gt;extra="forbid"&lt;/code&gt; on every schema:&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;class&lt;/span&gt; &lt;span class="nc"&gt;SubscribeRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;model_config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ConfigDict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;extra&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;forbid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EmailStr&lt;/span&gt;
    &lt;span class="n"&gt;watchlist&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&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="n"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ScheduleConfig&lt;/span&gt;
    &lt;span class="n"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a payload with &lt;code&gt;holdings&lt;/code&gt; or &lt;code&gt;geminiApiKey&lt;/code&gt; somehow arrived, the API returns 422 before touching the database. Belt and suspenders.&lt;/p&gt;

&lt;h2&gt;
  
  
  The call path
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User clicks "Save &amp;amp; Subscribe"
  → buildCloudPayload() reconstructs from allowlist
  → assertNoPrivateLeak() walks the object, throws on forbidden keys
  → POST /api/subscribe { email, watchlist, schedule, enabled }
  → Pydantic rejects extra fields (422 if violated)
  → Supabase stores ONLY email, tickers, schedule, timezone
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why this pattern matters beyond finance
&lt;/h2&gt;

&lt;p&gt;If you're building any Chrome extension that handles sensitive data — passwords, health info, personal notes — this three-layer approach works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Construct outbound payloads from scratch (don't filter existing state)&lt;/li&gt;
&lt;li&gt;Assert at runtime before every network call (catches future regressions)&lt;/li&gt;
&lt;li&gt;Reject unknown fields server-side (defense in depth)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It's more work upfront than just "don't include the field," but it turns a convention into an invariant. A future contributor can't accidentally break it without seeing a hard error in development.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rest of StockAgent
&lt;/h2&gt;

&lt;p&gt;The privacy architecture is one piece. The extension also has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic grading engine:&lt;/strong&gt; 5 financial metrics with sector-aware thresholds (banks, tech, pharma, crypto all scored differently)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tiered news risk scoring:&lt;/strong&gt; Yahoo RSS headlines penalize stocks by severity (fraud = -2, lawsuit = -1, guidance cut = informational)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scheduled email digests:&lt;/strong&gt; AWS EventBridge + Lambda fires every 5 min for punctual delivery&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optional AI explanations:&lt;/strong&gt; BYOK Gemini key explains grades in English — never influences the score&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-region:&lt;/strong&gt; US, Canada, and India exchanges&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://chromewebstore.google.com/detail/gbgmfkgijoenfnpklkchfbcljnjgkfef" rel="noopener noreferrer"&gt;Chrome Web Store&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/tridibbanik17/Stock_Agent.git" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt; (full source — read &lt;a href="https://github.com/tridibbanik17/Stock_Agent/blob/main/extension/lib/storage.js" rel="noopener noreferrer"&gt;storage.js&lt;/a&gt; for the privacy layer)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.youtube.com/watch?v=khjeu4JWylc" rel="noopener noreferrer"&gt;Short demo video&lt;/a&gt; (1 min 30 sec)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.youtube.com/watch?v=4ZP0hkDd_Dk" rel="noopener noreferrer"&gt;Detailed demo video&lt;/a&gt; (5 min)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>python</category>
      <category>privacy</category>
    </item>
    <item>
      <title>I Auto-Save Every Accepted HackerRank Solution to GitHub — Here's How</title>
      <dc:creator>Tridib Banik</dc:creator>
      <pubDate>Fri, 10 Jul 2026 04:33:04 +0000</pubDate>
      <link>https://dev.to/tridibbanik17/i-auto-save-every-accepted-hackerrank-solution-to-github-heres-how-4d3</link>
      <guid>https://dev.to/tridibbanik17/i-auto-save-every-accepted-hackerrank-solution-to-github-heres-how-4d3</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl19r3f7alretda2itn3m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl19r3f7alretda2itn3m.png" alt="Cover image showing accepted HackerRank code submissions are automatically captured by PrepPush Chrome extension and are pushed to the user's GitHub." width="440" height="280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I practice on &lt;strong&gt;HackerRank&lt;/strong&gt; a lot. The problems are fine, but I kept losing my own work.&lt;/p&gt;

&lt;p&gt;I'd solve something, move on, and weeks later I couldn't remember:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What trick I used&lt;/li&gt;
&lt;li&gt;The time/space complexity of &lt;em&gt;my&lt;/em&gt; solution&lt;/li&gt;
&lt;li&gt;Where I even saved the code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Copy-pasting into random files didn't scale. I wanted a &lt;strong&gt;GitHub repo that looked like interview prep&lt;/strong&gt;, not a junk drawer.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;&lt;a href="https://chromewebstore.google.com/detail/lkbbmepdmkokiapildnhkimcgnofokdd" rel="noopener noreferrer"&gt;PrepPush&lt;/a&gt;&lt;/strong&gt; — a Chrome extension that watches for &lt;strong&gt;Accepted&lt;/strong&gt; submissions and pushes them to &lt;strong&gt;your&lt;/strong&gt; GitHub repo automatically.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0xcd237ca5nufjnsxbjf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0xcd237ca5nufjnsxbjf.png" alt="Screenshot showing the split Chrome window, where the right window showing a HackerRank submission and the left window shows the code submission, including AI analysis as code comments, in the user's dedicated GitHub repository." width="799" height="425"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The workflow I wanted
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Solve a problem on HackerRank&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Submit Code&lt;/strong&gt; until it's &lt;strong&gt;Accepted&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Code lands on GitHub — organized, labeled, ready to revise before interviews&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No manual copy-paste. No forgetting which approach was &lt;code&gt;approach1&lt;/code&gt; vs &lt;code&gt;approach2&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What shows up on GitHub
&lt;/h2&gt;

&lt;p&gt;Each accepted solution goes here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;solutions/{language}/{difficulty}/{problem-slug}/solution-approach1.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you solve the same problem with a &lt;strong&gt;different algorithm&lt;/strong&gt;, you get &lt;code&gt;solution-approach2.py&lt;/code&gt;, and so on.&lt;/p&gt;

&lt;p&gt;Re-submitting the &lt;strong&gt;same logic&lt;/strong&gt; updates the same file — no duplicate spam.&lt;/p&gt;

&lt;p&gt;Example header PrepPush adds to the file:&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="c1"&gt;# Link        https://www.hackerrank.com/challenges/alternating-characters/problem?isFullScreen=true
# Problem     Alternating Characters 
# Difficulty  Easy
# Subdomain   Strings
# Language    python3
# Submitted   2026-07-25, 10:46 a.m.
# Technique   linear-scan-adjacent-comparison
# Time        O(n)
# Space       O(1)
# Insight     The algorithm counts the total number of adjacent character pairs that are identical, which corresponds to the minimum number of deletions required to eliminate all consecutive duplicates.
# Interview   Before: "I could use a stack to track characters and pop duplicates." After: "A linear scan is more efficient at O(n) time and O(1) space, as we only need to compare each character with its immediate predecessor to identify necessary deletions."
# Pitfalls    (1) Failing to handle empty strings or single-character strings, which the code explicitly guards against to return zero.  (2) Incorrectly indexing the loop by starting at zero instead of one, which would cause an index out of bounds error when accessing the previous character.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;HackerRank link&lt;/strong&gt; is the first row so you can jump back to the problem anytime.&lt;/p&gt;




&lt;h2&gt;
  
  
  How PrepPush captures submissions
&lt;/h2&gt;

&lt;p&gt;PrepPush runs only on &lt;code&gt;hackerrank.com&lt;/code&gt; — including contest and prep-kit pages.&lt;/p&gt;

&lt;p&gt;When your submission is accepted:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;PrepPush captures the solution from HackerRank's submission data&lt;/li&gt;
&lt;li&gt;It pushes to your GitHub repo (it can create the repo on first push if your token allows)&lt;/li&gt;
&lt;li&gt;Optionally, &lt;strong&gt;Gemini&lt;/strong&gt; adds technique, Big-O, trick, and hint in the header&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A small toast appears on the page when capture succeeds. Open the extension from your toolbar if you want to see status or the AI analysis — push and analysis still run in the background either way.&lt;/p&gt;




&lt;h2&gt;
  
  
  One-time setup (~2 minutes)
&lt;/h2&gt;

&lt;p&gt;You bring your own keys. PrepPush does not host your code or credentials.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. GitHub personal access token
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Create a &lt;strong&gt;classic&lt;/strong&gt; token with &lt;strong&gt;&lt;code&gt;repo&lt;/code&gt;&lt;/strong&gt; scope
&lt;a href="https://github.com/settings/tokens/new?scopes=repo&amp;amp;description=PrepPush" rel="noopener noreferrer"&gt;Generate token on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Expiration: &lt;strong&gt;90 days&lt;/strong&gt; or &lt;strong&gt;No expiration&lt;/strong&gt; — your choice&lt;/li&gt;
&lt;li&gt;Paste it in PrepPush &lt;strong&gt;Settings&lt;/strong&gt; → &lt;strong&gt;Test GitHub&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Gemini API key (optional)
&lt;/h3&gt;

&lt;p&gt;Only needed if you want AI notes in the file header.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Free tier: &lt;a href="https://aistudio.google.com/apikey" rel="noopener noreferrer"&gt;Google AI Studio&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Paste in PrepPush &lt;strong&gt;Settings&lt;/strong&gt; → &lt;strong&gt;Test AI&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F53wmu8dg20n2ftnhw8uw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F53wmu8dg20n2ftnhw8uw.png" alt="Settings page of PrepPush Chrome extension containing spaces to insert GitHub token and Gemini key and test each of them out." width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's it. Submit an accepted solution and check your repo.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I built it as an extension (not a script)
&lt;/h2&gt;

&lt;p&gt;HackerRank doesn't give you a clean "export all my solutions" button for the workflow I cared about.&lt;/p&gt;

&lt;p&gt;I needed something that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fires on &lt;strong&gt;Accepted&lt;/strong&gt; — not on Run/Compile&lt;/li&gt;
&lt;li&gt;Works on &lt;strong&gt;full-screen&lt;/strong&gt; and &lt;strong&gt;prep-kit&lt;/strong&gt; URLs&lt;/li&gt;
&lt;li&gt;Writes to &lt;strong&gt;my&lt;/strong&gt; repo structure, not a third-party server&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A Chrome extension made sense: local keys, no PrepPush backend, you own the data.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I learned shipping it
&lt;/h2&gt;

&lt;p&gt;Early users helped a lot. A few real-world fixes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SQL / DB2&lt;/strong&gt; problems needed correct file paths (&lt;code&gt;.sql&lt;/code&gt;, not &lt;code&gt;.py&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prep-kit&lt;/strong&gt; pages use different APIs than classic challenges — capture had to follow both&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI headers&lt;/strong&gt; had to reference &lt;em&gt;your actual code&lt;/em&gt;, not generic interview tips&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's a small project (~50 early users) but it solves a real problem for me every week.&lt;/p&gt;




&lt;h2&gt;
  
  
  Limitations (honest list)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Platform&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;HackerRank only — not LeetCode or Codeforces&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Trigger&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Submit Code → &lt;strong&gt;Accepted&lt;/strong&gt; — Run Code doesn't count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Keys&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;You provide GitHub token + optional Gemini key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Privacy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Keys stay in Chrome on your device — see &lt;a href="https://github.com/tridibbanik17/PrepPush/blob/main/PRIVACY.md" rel="noopener noreferrer"&gt;privacy notes&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chrome Web Store:&lt;/strong&gt; &lt;a href="https://chromewebstore.google.com/detail/lkbbmepdmkokiapildnhkimcgnofokdd" rel="noopener noreferrer"&gt;Install PrepPush&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source / issues:&lt;/strong&gt; &lt;a href="https://github.com/tridibbanik17/PrepPush" rel="noopener noreferrer"&gt;github.com/tridibbanik17/PrepPush&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Demo video:&lt;/strong&gt; &lt;a href="https://www.youtube.com/watch?v=dXOZskTX2fM" rel="noopener noreferrer"&gt;YouTube walkthrough&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you use HackerRank for interview prep, I'd love feedback — what breaks, what's missing, what would make this a weekly habit for you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;PrepPush is an independent project. Not affiliated with HackerRank, GitHub, or Google.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Question for you:&lt;/strong&gt; How do you track HackerRank solutions today — GitHub, Notion, or not at all? I'd love to hear what would make this workflow stick for you.&lt;/p&gt;

</description>
      <category>hackerrank</category>
      <category>chromeextension</category>
      <category>ai</category>
      <category>github</category>
    </item>
  </channel>
</rss>
