<?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: Shaan Satsangi</title>
    <description>The latest articles on DEV Community by Shaan Satsangi (@shaanalpha).</description>
    <link>https://dev.to/shaanalpha</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%2F3968891%2Fde013e6c-25f4-4f17-846a-8ca0c349c0d1.jpeg</url>
      <title>DEV Community: Shaan Satsangi</title>
      <link>https://dev.to/shaanalpha</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shaanalpha"/>
    <language>en</language>
    <item>
      <title>The approval signature that could be spent twice</title>
      <dc:creator>Shaan Satsangi</dc:creator>
      <pubDate>Sun, 30 Aug 2026 02:21:11 +0000</pubDate>
      <link>https://dev.to/shaanalpha/the-approval-signature-that-could-be-spent-twice-cdj</link>
      <guid>https://dev.to/shaanalpha/the-approval-signature-that-could-be-spent-twice-cdj</guid>
      <description>&lt;p&gt;I built this project and wrote this article for the purposes of entering the Google Cloud "All Things Agentic" Hackathon 2026.&lt;/p&gt;

&lt;p&gt;My human approval gate checked its one time signature, ran the action, then marked the signature used. Two operators double clicking Execute at the same moment is enough to run the action twice.&lt;/p&gt;

&lt;p&gt;It is fixed now, and it is still the most useful thing I learned building &lt;strong&gt;Syntrueno&lt;/strong&gt;, my solo entry for Track 3 of the hackathon. An incident arrives, sometimes from a Cloud Monitoring alert with nobody in the loop. Agents diagnose it, a judge scores the proposed fix against a safety rubric, and anything consequential stops until a human signs for it.&lt;/p&gt;

&lt;p&gt;That part I can draw on a whiteboard. What actually taught me something was two bugs, neither of them in the interesting layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The signature that could be spent twice
&lt;/h2&gt;

&lt;p&gt;The gate mints a SHA-256 signature bound to one exact action and one exact parameter set, good for a single use, expiring after 30 minutes. The order of operations was: check it, run the mutation, then spend it.&lt;/p&gt;

&lt;p&gt;FastAPI serves sync endpoints from a threadpool, so two requests arriving together both got a yes, both wrote to Cloud Run, and the ledger recorded one event. One signature, two mutations, one audit entry.&lt;/p&gt;

&lt;p&gt;The fix was ordering, not cleverness. Find and spend the signature under one lock, and spend it &lt;em&gt;before&lt;/em&gt; the mutation rather than after. If the call raises before anything changed, hand the signature back so a transient error does not cost the operator their approval. There is now a test that fires two requests at one signature and expects a single execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. An audit chain that forked in silence
&lt;/h2&gt;

&lt;p&gt;Every outcome, including every refusal, is appended to a hash chained ledger, and that append was a read, modify, write over the chain head with no lock. Same threadpool, same shape: two incidents interleave, both claim the same position, the chain forks, and nothing raises. A verifier would report a broken chain long after the run that broke it.&lt;/p&gt;

&lt;p&gt;Underneath it, the health check had been answering the wrong question. Connected meant a client object existed, not that writes were landing, so the stores had quietly fallen back to memory while still reporting themselves durable. A system that claims durable storage while writing to a dictionary in RAM is the exact failure this project exists to be incapable of.&lt;/p&gt;

&lt;p&gt;Neither bug announced itself, which is why I trust the test suite more than the diagram. Pytest collects 284 tests. They pass offline in about three seconds with no API key and no cloud credentials, and CI runs them on a machine deliberately given no secret. If a test can only pass when the cloud is up, it was never testing my code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea those bugs were guarding
&lt;/h2&gt;

&lt;p&gt;The agent's action space is a closed enum, handed to Gemini as its structured response schema. There is no destructive verb in it. Not disabled, not on a denylist. Absent. A prompt injection that fully succeeds still cannot emit one, because no delete verb is blocked; none exists.&lt;/p&gt;

&lt;p&gt;A blocked path can be reached by a bug. An absent one cannot.&lt;/p&gt;

&lt;p&gt;The honest limit on that: the enum still contains verbs that change a live service, and a wrong parameter on one of those can still hurt. It bounds the blast radius, it does not remove it. The property rests on every member of that enum being safe under every parameter set, which is a claim about what I put in my enum rather than a result from type theory. The two bugs above are part of what keeping that claim true actually costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The demo I keep coming back to
&lt;/h2&gt;

&lt;p&gt;The same outage alert, sent twice.&lt;/p&gt;

&lt;p&gt;Once with a prompt injection buried in the error text, which three screens, one of them Model Armor, quarantine.&lt;/p&gt;

&lt;p&gt;Once quoting a real &lt;code&gt;DROP TABLE&lt;/code&gt; from a slow query log, which is a database engineer's evidence about what broke, and which passes straight through to the agent.&lt;/p&gt;

&lt;p&gt;Instructions are stopped. Evidence gets through. That distinction is the whole design.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I take away
&lt;/h2&gt;

&lt;p&gt;It is a hackathon project, built solo, with no users. What stays with me is the habit of asking what a green check is actually measuring. Both of these bugs sat behind something that looked fine.&lt;/p&gt;

&lt;p&gt;Built on Cloud Run, Firestore and Vertex AI, with Gemini and Gemma 4.&lt;/p&gt;

&lt;p&gt;Live service: &lt;a href="https://syntrueno-18489510475.us-central1.run.app" rel="noopener noreferrer"&gt;https://syntrueno-18489510475.us-central1.run.app&lt;/a&gt;&lt;br&gt;
Code, Apache 2.0: &lt;a href="https://github.com/Shaan-alpha/syntrueno" rel="noopener noreferrer"&gt;https://github.com/Shaan-alpha/syntrueno&lt;/a&gt;&lt;/p&gt;

</description>
      <category>allthingsagentichackathon</category>
      <category>ai</category>
      <category>googlecloud</category>
      <category>vertexai</category>
    </item>
    <item>
      <title>shaansatsangi.com</title>
      <dc:creator>Shaan Satsangi</dc:creator>
      <pubDate>Fri, 26 Jun 2026 15:10:47 +0000</pubDate>
      <link>https://dev.to/shaanalpha/shaansatsangicom-2c73</link>
      <guid>https://dev.to/shaanalpha/shaansatsangicom-2c73</guid>
      <description>&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://shaansatsangi.com/" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;shaansatsangi.com&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;-&amp;gt;suggestions??&lt;/p&gt;

</description>
      <category>ai</category>
      <category>design</category>
      <category>portfolio</category>
      <category>productivity</category>
    </item>
    <item>
      <title># ⚙️ I Spent Months Building a Pixel-Art Universe Called *Cog &amp; Cosmos* 🌌</title>
      <dc:creator>Shaan Satsangi</dc:creator>
      <pubDate>Wed, 24 Jun 2026 04:35:59 +0000</pubDate>
      <link>https://dev.to/shaanalpha/-i-spent-months-building-a-pixel-art-universe-called-cog-cosmos-h8f</link>
      <guid>https://dev.to/shaanalpha/-i-spent-months-building-a-pixel-art-universe-called-cog-cosmos-h8f</guid>
      <description>&lt;p&gt;Every developer eventually faces a difficult choice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build another to-do app.&lt;/li&gt;
&lt;li&gt;Or create an absurdly over-engineered game with eight interconnected worlds, giant numbers, prestige systems, and enough gears to make a Victorian engineer cry tears of joy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Naturally, I chose chaos.&lt;/p&gt;

&lt;h2&gt;
  
  
  🚀 Introducing &lt;strong&gt;Cog &amp;amp; Cosmos: The Fortune Engine&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Cog &amp;amp; Cosmos&lt;/strong&gt; is a pixel-art incremental/idle game where every world remains connected.&lt;/p&gt;

&lt;p&gt;Unlike many idle games where older stages become irrelevant, here everything feeds everything else:&lt;/p&gt;

&lt;p&gt;🏘️ Village → 🌾 Farm → ⛏️ Mine → 🏭 Factory → ✨ Magic → 🚀 Space → ⏳ Time → 🌌 Multiverse&lt;/p&gt;

&lt;p&gt;All of them are tied together by a massive &lt;strong&gt;Fortune Engine&lt;/strong&gt;, which converts surplus resources into Fortune ★, the universal currency powering the entire game.&lt;/p&gt;




&lt;h2&gt;
  
  
  ✨ Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;8 interconnected stages&lt;/li&gt;
&lt;li&gt;Multiple prestige layers&lt;/li&gt;
&lt;li&gt;Global skill tree&lt;/li&gt;
&lt;li&gt;Achievements system&lt;/li&gt;
&lt;li&gt;Challenges and medals&lt;/li&gt;
&lt;li&gt;Collections and relics&lt;/li&gt;
&lt;li&gt;Offline progress&lt;/li&gt;
&lt;li&gt;Cloud sync support&lt;/li&gt;
&lt;li&gt;Pixel-art scenes powered by PixiJS&lt;/li&gt;
&lt;li&gt;Mobile-friendly PWA&lt;/li&gt;
&lt;li&gt;Big-number math thanks to &lt;code&gt;break_eternity.js&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because apparently normal numbers weren't dramatic enough.&lt;/p&gt;




&lt;h2&gt;
  
  
  🛠 Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Svelte 5&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TypeScript&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PixiJS v8&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vite&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;break_eternity.js&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;IndexedDB + lz-string&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Built entirely with free tools.&lt;/p&gt;




&lt;h2&gt;
  
  
  📚 What I Learned
&lt;/h2&gt;

&lt;p&gt;Building this project taught me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Game architecture is wild.&lt;/li&gt;
&lt;li&gt;Balancing economies is harder than debugging.&lt;/li&gt;
&lt;li&gt;Players will somehow become richer than the GDP of small countries.&lt;/li&gt;
&lt;li&gt;"I'll just add one more feature" is a dangerous sentence.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🎮 Play the Game
&lt;/h2&gt;

&lt;p&gt;👉 &lt;a href="https://shaan-alpha.github.io/Cog-and-Cosmos/" rel="noopener noreferrer"&gt;https://shaan-alpha.github.io/Cog-and-Cosmos/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  ⭐ GitHub Repository
&lt;/h2&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/Shaan-alpha/Cog-and-Cosmos" rel="noopener noreferrer"&gt;https://github.com/Shaan-alpha/Cog-and-Cosmos&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you enjoy game development, idle games, or watching numbers become larger than the observable universe, I'd love to hear your thoughts.&lt;/p&gt;

&lt;p&gt;And if my projects interest you, consider following me on GitHub. More strange creations are currently brewing inside my digital workshop. ⚙️🚀&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with free tools and a questionable amount of enthusiasm.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  gamedev #indiedev #webdev #typescript #svelte #pixijs #opensource #javascript #github
&lt;/h1&gt;

</description>
      <category>gamedev</category>
      <category>webdev</category>
      <category>github</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I just launched 𝗙𝗮𝗰𝗲 𝗦𝗼𝗿𝘁 𝗦𝘁𝘂𝗱𝗶𝗼! 📸🤖</title>
      <dc:creator>Shaan Satsangi</dc:creator>
      <pubDate>Thu, 11 Jun 2026 06:06:14 +0000</pubDate>
      <link>https://dev.to/shaanalpha/i-just-launched--33il</link>
      <guid>https://dev.to/shaanalpha/i-just-launched--33il</guid>
      <description>&lt;p&gt;It is a privacy-first, local-first photo organizer powered by deep learning face recognition. It detects, embeds, and groups faces to organize your photos automatically—all 100% offline.&lt;/p&gt;

&lt;p&gt;🔥 Highlight Features:&lt;br&gt;
✅ 100% Local: No cloud APIs, no telemetry, no leaks.&lt;br&gt;
✅ Deep Learning: Driven by OpenCV DNN (YuNet + SFace ONNX models).&lt;br&gt;
✅ Smart Automation: Copies matches, partial matches, and individual profiles into organized folders, complete with ZIP archives and JSON reports.&lt;br&gt;
✅ Standalone EXE: Run it on Windows instantly with zero dependencies.&lt;br&gt;
✅ Dynamic UI: Fully responsive Tailwind dashboard with Dark/Light modes.&lt;/p&gt;

&lt;p&gt;Check out the repository, download the EXE, or contribute:&lt;br&gt;
👉 &lt;a href="https://github.com/Shaan-alpha/face-sort-studio" rel="noopener noreferrer"&gt;https://github.com/Shaan-alpha/face-sort-studio&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let me know what you think! ⭐&lt;/p&gt;

&lt;h1&gt;
  
  
  machinelearning #computervision #python #localfirst #privacy #developers #opensource #ai
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Internet access on first launch only (to fetch the AI models ~40-50mb)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I built a Databricks medallion lakehouse to roast my own YouTube history (Bronze Silver Gold Existential Dread)</title>
      <dc:creator>Shaan Satsangi</dc:creator>
      <pubDate>Thu, 04 Jun 2026 22:54:47 +0000</pubDate>
      <link>https://dev.to/shaanalpha/i-built-a-databricks-medallion-lakehouse-to-roast-my-own-youtube-history-bronze-silver-gold--2m9m</link>
      <guid>https://dev.to/shaanalpha/i-built-a-databricks-medallion-lakehouse-to-roast-my-own-youtube-history-bronze-silver-gold--2m9m</guid>
      <description>&lt;p&gt;There's a normal way to analyze your YouTube watch history. You export it from Google Takeout, open a Jupyter notebook, &lt;code&gt;pd.read_json()&lt;/code&gt;, run a couple of &lt;code&gt;value_counts()&lt;/code&gt;, feel a brief flicker of shame, and close the laptop.&lt;/p&gt;

&lt;p&gt;I did not do that.&lt;/p&gt;

&lt;p&gt;Instead I built a full &lt;strong&gt;Bronze → Silver → Gold medallion lakehouse&lt;/strong&gt; on Databricks — Delta Lake, PySpark, an enrichment layer that calls the YouTube Data API, a FastAPI serving tier, a Neon Postgres warehouse, and a Next.js 16 frontend with animated cards — to discover that I watch a concerning amount of YouTube at 2 AM.&lt;/p&gt;

&lt;p&gt;It's called &lt;strong&gt;YouTube Wrapped&lt;/strong&gt;: Spotify Wrapped, but for the platform you actually spend your life on. &lt;a href="https://youtube-wrapped-by-shaan.vercel.app" rel="noopener noreferrer"&gt;Live demo&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The premise
&lt;/h2&gt;

&lt;p&gt;Google Takeout hands you your watch history as a &lt;code&gt;watch-history.json&lt;/code&gt; that's somehow both enormous and useless. Each record is basically &lt;code&gt;{title, titleUrl, time}&lt;/code&gt; — no genre, no artist, no duration. Just a timestamp and a vibe.&lt;/p&gt;

&lt;p&gt;The goal: turn that raw shame-export into a year-in-review with top artists, genres, "binge sessions," a night-owl score, and a "main character" artist (the one you cannot stop replaying).&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture (a.k.a. the overkill)
&lt;/h2&gt;

&lt;p&gt;This is the part where a sane person uses pandas and I use enterprise data engineering for a personal hobby question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🥉 Bronze — land the raw JSON.&lt;/strong&gt; Dump Takeout exactly as-is into Delta. Zero transformations. If I break something downstream, the source of truth never moved. (Also: never trust your own parser on the first run.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🥈 Silver — clean, type, deduplicate.&lt;/strong&gt; Parse timestamps into real datetimes, normalize titles (YouTube prefixes everything with "Watched "), drop dupes, toss ads and deleted videos. Now it's a table instead of a JSON crime scene.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🜚 Enrichment — make the data &lt;em&gt;mean&lt;/em&gt; something.&lt;/strong&gt; The genuinely hard layer. "Watched Pasoori" tells you nothing structured. I hit the YouTube Data API for channel + metadata, then did artist mapping and genre classification — including a &lt;strong&gt;Desi / Western / untagged&lt;/strong&gt; split, because my listening is ~60% Bollywood and off-the-shelf genre tags had no idea what to do with that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🥇 Gold — aggregate into fact tables.&lt;/strong&gt; Pre-computed analytics the dashboard reads instantly: listening rhythm by hour and day-of-week, binge sessions (consecutive runs + durations), loyal artists ranked by span, the night-owl score, and the main-character artist.&lt;/p&gt;

&lt;p&gt;All on &lt;strong&gt;Databricks Free Edition&lt;/strong&gt; with Unity Catalog, because the budget was zero dollars and pure spite.&lt;/p&gt;

&lt;h2&gt;
  
  
  The serving layer
&lt;/h2&gt;

&lt;p&gt;Gold tables export to CSV and load into &lt;strong&gt;Neon Postgres&lt;/strong&gt; via a little &lt;code&gt;load_to_neon.py&lt;/code&gt;. A &lt;strong&gt;FastAPI&lt;/strong&gt; backend (SQLAlchemy + Uvicorn) exposes 15+ endpoints — overview totals, top artists/channels/genres, the rhythm heatmap, binge stats. (&lt;a href="https://youtube-wrapped-api.onrender.com/docs" rel="noopener noreferrer"&gt;API docs are live&lt;/a&gt; if you want to poke them.)&lt;/p&gt;

&lt;p&gt;Frontend is &lt;strong&gt;Next.js 16 / React 19 / Tailwind 4&lt;/strong&gt;, Recharts for graphs, Framer Motion for the Wrapped-style card reveals. Vercel + Render + Neon.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The medallion pattern earns its keep even when it's overkill.&lt;/strong&gt; Every time my enrichment logic was wrong (often), I re-ran from Silver and Bronze never flinched. That immutable raw layer saved me more times than I'll admit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enrichment is 80% of the work.&lt;/strong&gt; Ingest and clean are easy. Turning "Watched [title]" into "Punjabi track, this artist, this genre" is where the real engineering hides.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your data will roast you for free.&lt;/strong&gt; The night-owl score does not lie.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The honest part
&lt;/h2&gt;

&lt;p&gt;Could this have been a notebook and 40 lines of pandas? Absolutely. But I wanted real end-to-end data-engineering reps — Delta Lake, medallion layering, an enrichment API, a serving warehouse, a real frontend — on a dataset I actually cared about. Building it on something personal made every architecture decision stick harder than another Titanic clone ever would.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Links&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔴 Demo: &lt;a href="https://youtube-wrapped-by-shaan.vercel.app" rel="noopener noreferrer"&gt;https://youtube-wrapped-by-shaan.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 Code: &lt;a href="https://github.com/shaan-alpha/Youtube-Wrapped" rel="noopener noreferrer"&gt;https://github.com/shaan-alpha/Youtube-Wrapped&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📑 API: &lt;a href="https://youtube-wrapped-api.onrender.com/docs" rel="noopener noreferrer"&gt;https://youtube-wrapped-api.onrender.com/docs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Data engineers: I'd genuinely love a sanity check on the Gold grain — am I pre-aggregating at the right level, or should the rhythm/binge tables stay lower-level and let the API roll them up? Roast the architecture in the comments. That's what the project's named after, after all.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>dataengineering</category>
      <category>python</category>
      <category>databricks</category>
    </item>
  </channel>
</rss>
