<?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: Kushal Baral</title>
    <description>The latest articles on DEV Community by Kushal Baral (@kushal1o1).</description>
    <link>https://dev.to/kushal1o1</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%2F3891037%2Fca094ef2-6c41-476b-a44e-77a3a61af457.jpeg</url>
      <title>DEV Community: Kushal Baral</title>
      <link>https://dev.to/kushal1o1</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kushal1o1"/>
    <language>en</language>
    <item>
      <title>Millions of Clicks, 200 Tickets: Building an Endpoint That Cannot Oversell</title>
      <dc:creator>Kushal Baral</dc:creator>
      <pubDate>Sat, 22 Aug 2026 10:41:34 +0000</pubDate>
      <link>https://dev.to/kushal1o1/millions-of-clicks-200-tickets-building-an-endpoint-that-cannot-oversell-1a0n</link>
      <guid>https://dev.to/kushal1o1/millions-of-clicks-200-tickets-building-an-endpoint-that-cannot-oversell-1a0n</guid>
      <description>&lt;p&gt;Friday morning, 10 AM sharp. Tickets go live.&lt;/p&gt;

&lt;p&gt;200 seats. A band people have waited years to see. Within seconds, millions of fans are smashing the Buy button from phones on trains, laptops in offices, that one tablet at the back of a taxi.&lt;/p&gt;

&lt;p&gt;Somewhere between request number 199 and request number 201, your database starts lying to you.&lt;/p&gt;

&lt;p&gt;Not because it is broken. Because you wrote code that made perfect sense and quietly assumed the world is polite. It is not. This post is the story of every way that assumption breaks, and how to build an endpoint that stays honest no matter how many people click at once.&lt;/p&gt;

&lt;p&gt;Every claim below is proven under real load against a working project: k6, autocannon, or plain loops of curl, whatever you prefer. The repo is linked at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable truth about this bug
&lt;/h2&gt;

&lt;p&gt;Here is the part nobody warns you about: the naive solution works fine. Genuinely fine. For a shop selling ten tickets a day, for a demo, for your portfolio site, for everything you have probably ever shipped.&lt;/p&gt;

&lt;p&gt;These bugs do not exist at low traffic. They only exist when requests overlap in time, which means no code review catches them, no unit test fails, and your launch demo goes flawlessly. Then the crowd arrives and the lies begin.&lt;/p&gt;

&lt;p&gt;You do not need millions of users either. A laptop, one endpoint, and any load tool (k6, autocannon, even a dumb for-loop of curl) reproduce everything below. Scale only changes how fast the lie spreads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three quick deaths of a naive counter
&lt;/h2&gt;

&lt;p&gt;The version everyone writes first keeps &lt;code&gt;count&lt;/code&gt; in memory and does &lt;code&gt;if count &amp;lt; LIMIT: count += 1&lt;/code&gt;. It dies three ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The read-write gap&lt;/strong&gt;: &lt;code&gt;count += 1&lt;/code&gt; is really read, add, write. Two simultaneous requests both read 199, both write 200, ticket 200 gets sold twice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple workers&lt;/strong&gt;: run four processes for speed and there are four separate counters in four memory spaces, each selling its own private inventory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restarts&lt;/strong&gt;: deploy at noon, memory resets, this morning's tickets go back on sale.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Shared mutable state under concurrency is dead on arrival, so the counter moves to Postgres. Where it dies again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The database still lets you race
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchrow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT sold FROM tickets WHERE id = 1&lt;/span&gt;&lt;span class="sh"&gt;"&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;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPDATE tickets SET sold = sold + 1 WHERE id = 1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shared state, survives restarts, multiple workers all see the same number. Ship it?&lt;/p&gt;

&lt;p&gt;Still oversells. Run it under load and watch.&lt;/p&gt;

&lt;p&gt;The database serializes statements, not intentions. Your check and your write are two separate statements, and between them sits a network round trip where another connection happily runs its own check against the old value. Same movie as the in-memory counter, new costume.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: make the check and the write ONE thing
&lt;/h2&gt;

&lt;p&gt;This is the moment the whole problem collapses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;tickets&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;sold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sold&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;sold&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;total_limit&lt;/span&gt;
&lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;sold&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One statement. Postgres takes the row lock needed for the update and re-evaluates the WHERE condition against the current row version before applying it; if the condition fails you get zero rows back, which means sold out. There is no gap. There is no "between the check and the write" anymore, because they are the same operation.&lt;/p&gt;

&lt;p&gt;k6 against this version: 500 concurrent purchases racing for 200 tickets. In repeated runs, exactly 200 succeeded and the remaining 300 were rejected as sold out. And that outcome is not luck or statistics: the check and the write are one atomic statement, so there is no application-level gap between them, regardless of how many workers hammer it.&lt;/p&gt;

&lt;p&gt;Rule worth tattooing somewhere: never trust read-then-write across a network round trip. Push conditions INTO the statement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retries sell twice
&lt;/h2&gt;

&lt;p&gt;Correctness achieved, so naturally the network ruins the party again.&lt;/p&gt;

&lt;p&gt;Mobile networks retry. Users double-click. Payment gateways re-send notifications. Our endpoint charges one ticket per click, so a timeout followed by a retry now buys TWO tickets for one person.&lt;/p&gt;

&lt;p&gt;The fix has a fancy name, idempotency, and a simple idea behind it: the client sends a key, and the same key always maps to the same outcome.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;purchases&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;NOTHING&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First request: the row inserts as pending and the flow moves toward confirmation. Retry with the same key: the insert does nothing, we look up what already happened and replay that stored outcome instead of selling again. One key always maps to its stored outcome, for as long as that record is retained (real systems define a retention window for keys). Stripe built entire SDK features around this concept, which tells you how often networks misbehave in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proving it under fire
&lt;/h2&gt;

&lt;p&gt;Correct when tried manually means nothing, as established. Time to be scientific. Load testing with k6:&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;const&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;scenarios&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;rush&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shared-iterations&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;vus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;// 500 virtual users&lt;/span&gt;
      &lt;span class="na"&gt;iterations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// each buys once&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;thresholds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;http_req_duration&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;p(95)&amp;lt;300&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;checks&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;rate&amp;gt;0.99&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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things being verified here. Correctness: never more than 200 confirmed sales. And speed: latency thresholds.&lt;/p&gt;

&lt;p&gt;p95 means 95 percent of requests finished faster than that number. Percentiles exist because averages lie: 99 fast requests plus one five-second disaster average out to a polite-looking blur, while p99 points directly at the disaster.&lt;/p&gt;

&lt;p&gt;One more production trick: when tickets run out, stop hammering the database. Cache a "sold out" flag in worker memory for a few seconds and reject instantly. The database remains the only source of truth: the flag can cause an early rejection, but it can never cause an oversell. And keep its lifetime short, because inventory can come BACK.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final boss: payments take seconds
&lt;/h2&gt;

&lt;p&gt;Everything so far assumed buying is instant. Real money is not.&lt;/p&gt;

&lt;p&gt;If we mark the ticket SOLD immediately and wait for payment, every declined card permanently eats one ticket. Inventory leaks, slowly, forever. If instead we wait for payment while holding our precious atomic row, the entire system queues behind one human typing their CVV at the speed of a sleepy sloth. Connection pools fill. The site dies. This is Amdahl's law wearing a payment form.&lt;/p&gt;

&lt;p&gt;Real systems escape with a state machine that separates GRABBING from OWNING:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 reserve (fast, atomic)
   AVAILABLE -------------------------&amp;gt; HELD
                                          | verify ok -&amp;gt; CONFIRMED (sold)
                                          | verify fail / TTL expiry
                                          v
                                       RELEASED -&amp;gt; AVAILABLE again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reserving a hold uses the same atomic trick as before, just counting holds instead of sales:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;tickets&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;held&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;held&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;sold&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;held&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;total_limit&lt;/span&gt;
&lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Milliseconds, no payment anywhere near the hot path. The customer gets a reservation id and a deadline. Payment gets verified outside the counter: a redirect back from the provider, a webhook, whatever the flow calls for. The state machine does not care who pulls the trigger.&lt;/p&gt;

&lt;p&gt;But holds pile up from people who close the tab. Someone has to take the tickets back. Enter the sweeper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;expired&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;purchases&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'released'&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'reserved'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;tickets&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;held&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;held&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;expired&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;held&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One statement, one transaction, crash safe. Run it opportunistically inside reserve requests plus once per second in the background, and expired holds flow back into availability automatically. Even eight workers sweeping simultaneously cannot corrupt anything: the release only matches rows still sitting in reserved, so any hold transitions to released exactly once, no matter who fires first.&lt;/p&gt;

&lt;p&gt;To be clear, that snippet is demonstration code for a single inventory bucket, one tickets row. Production schemas go further: each reservation references the exact inventory it holds, and many skip the separate held counter entirely, deriving availability from live reservations instead. That removes this entire class of bookkeeping from the equation.&lt;/p&gt;

&lt;p&gt;Confirming payment is conditional too: WHERE status = 'reserved' AND expires_at &amp;gt; now(). Lose the race against the sweeper and the customer gets a polite "hold expired". One disappointed human. Never two tickets. That trade is the whole philosophy: correctness over comfort, always.&lt;/p&gt;

&lt;p&gt;The expiry path proves itself quickly: set a short TTL, reserve a hold, watch &lt;code&gt;/status&lt;/code&gt; show held go up, let the deadline pass, watch held drain back to zero while the ledger flips the row from &lt;code&gt;reserved&lt;/code&gt; to &lt;code&gt;released&lt;/code&gt;. Try paying anyway and you get HTTP 410, which is not an error case but the refund trigger, because money may already have moved. Inventory became self-cleaning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing the TTL is a business decision
&lt;/h2&gt;

&lt;p&gt;How long should a hold live? Too short and customers lose their ticket mid checkout while typing card details. Rage, support tickets, lost sales. Too long and abandoned tabs lock inventory from real buyers: the event looks sold out when it is not.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TTL too short&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;customers lose tickets MID-CHECKOUT, rage ensues&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TTL too long&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;dead tabs hold all 200 tickets, event looks sold out&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There is no perfect number. Rule of thumb: measure how long your payment flow actually takes, set the TTL slightly above its p95, and for the love of users show a countdown timer. A silent expiry feels like theft. A visible countdown creates urgency. Production systems go further: renewing holds while the user is active on the payment page, grace windows for payments that provably succeeded seconds late, and automatic refunds whenever that "hold expired" response fires.&lt;/p&gt;

&lt;p&gt;What the TTL must never be allowed to break is correctness. An expiring hold can cost a sale. It can never create two tickets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build it yourself
&lt;/h2&gt;

&lt;p&gt;The whole thing lives as runnable code, one lesson per branch, each with its own README explaining what breaks and why:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Branch&lt;/th&gt;
&lt;th&gt;Lesson&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;step-1-theory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the overselling problem, no code yet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;step-2-in-memory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the naive counter, four workers four truths&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;step-3-await-demo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;watching a race condition happen live&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;step-4-naive-db&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;moving to Postgres, still overselling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;step-5-atomic&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the atomic conditional UPDATE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;step-6-idempotency&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;retries, keys, the purchases ledger&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;step-7-load-tests&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;k6 proof, percentiles, sold-out cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;step-8-reservation&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;holds, TTL sweeper, payments off the hot path&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The concepts are the point, not the stack: any language, framework or database tells these same ideas just as well.&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%2Fbwc6ibooyt16yamb0x9p.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%2Fbwc6ibooyt16yamb0x9p.png" alt="Repository Branches image" width="665" height="526"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/kushal1o1/XCounter" rel="noopener noreferrer"&gt;github.com/kushal1o1/XCounter&lt;/a&gt;&lt;/p&gt;

</description>
      <category>backend</category>
      <category>scalability</category>
      <category>reliability</category>
      <category>python</category>
    </item>
    <item>
      <title>ZeroRelay: Direct Browser-to-Browser Sharing - No Server Ever Sees Your Data</title>
      <dc:creator>Kushal Baral</dc:creator>
      <pubDate>Wed, 08 Jul 2026 10:35:51 +0000</pubDate>
      <link>https://dev.to/kushal1o1/zerorelay-direct-browser-to-browser-sharing-no-server-ever-sees-your-data-4bn3</link>
      <guid>https://dev.to/kushal1o1/zerorelay-direct-browser-to-browser-sharing-no-server-ever-sees-your-data-4bn3</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A fully open-source P2P mesh app for sharing text, code, notes, and files directly between browsers. No signup, no accounts, no database.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Pain Point
&lt;/h2&gt;

&lt;p&gt;You're sitting next to a colleague. You need to send them a 200MB video file.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Email?&lt;/strong&gt; 25MB limit + compression&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drive/Dropbox?&lt;/strong&gt; Need an account + files sit on their servers forever&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WhatsApp/Telegram?&lt;/strong&gt; Compresses media + stores everything on their servers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AirDrop?&lt;/strong&gt; Apple-only, doesn't work half the time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;USB?&lt;/strong&gt; Who carries one anymore?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SCP/iPerf?&lt;/strong&gt; Overkill for a quick share&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;ZeroRelay:&lt;/strong&gt; Open a tab,drag the file. Done. Direct browser-to-browser. Nothing touches a server.&lt;/p&gt;




&lt;h2&gt;
  
  
  Demo
&lt;/h2&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%2Fcyh3lvaqk82aee6iryt3.gif" 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%2Fcyh3lvaqk82aee6iryt3.gif" alt="Zero Relay Demo" width="720" height="422"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What's ZeroRelay?
&lt;/h2&gt;

&lt;p&gt;A peer-to-peer mesh app where every browser connects directly to every other browser over WebRTC. The signaling server (Cloudflare Durable Object) only exchanges join/leave messages and ICE candidates - it never stores or even sees your messages, files, or avatars.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Each peer is its own database&lt;/strong&gt; - everything lives in IndexedDB (Dexie) on your browser. When you share something, it's broadcast over WebRTC data channels directly to connected peers. There is no central storage. There is no cloud. Your data never touches a server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    ┌─────────────────┐
                    │  Cloudflare DO   │  ← signaling only
                    │  (per room)      │    (join/leave/ICE relay)
                    └────────┬────────┘
                             │
              ┌──────────────┼──────────────┐
              ▼              ▼              ▼
         ┌────────┐    ┌────────┐    ┌────────┐
         │ Peer A │◄──►│ Peer B │◄──►│ Peer C │  ← WebRTC mesh
         │(Dexie) │    │(Dexie) │    │(Dexie) │     (data channels)
         └────────┘    └────────┘    └────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each browser stores its own copy of everything in IndexedDB. When you share something, it's broadcast to all connected peers over WebRTC data channels. The signaling server never sees your messages, files, or avatars.&lt;/p&gt;

&lt;p&gt;For full architecture deep-dive, see &lt;a href="https://github.com/kushal1o1/ZeroRelay/blob/main/ARCHITECTURE.md" rel="noopener noreferrer"&gt;ARCHITECTURE.md&lt;/a&gt; on GitHub.&lt;/p&gt;




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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Full P2P Mesh&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Every peer connects directly to every other - no central relay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Zero Server Storage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No database, no logs, no message history on any server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;No Signup or Accounts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Open the app, join a room - done&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Share Anything&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Text, code, notes, or files of any size&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Password-Protected Rooms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Room-level passwords enforced by Cloudflare DO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Retention Per Item&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Session, 5 min, 1 hour, 1 day, or forever&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Open Source&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;MIT - fully auditable and self-hostable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Important Note
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Peer Discovery&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Works &lt;strong&gt;globally&lt;/strong&gt; - anyone on the internet can see you in the room&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data Transfer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Works on &lt;strong&gt;same LAN&lt;/strong&gt; - messages and files flow when peers are on the same network&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cross-Network&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Not yet - needs a TURN server for symmetric NAT traversal&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Peer discovery goes through the Cloudflare DO (WebSocket over TCP) so it works anywhere. Data transfer uses WebRTC which needs STUN/TURN to cross the public internet. Currently STUN-only. &lt;strong&gt;TURN support and contributions are welcome:)&lt;/strong&gt;&lt;/p&gt;




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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Frontend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Next.js 15 + React 19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;State&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Zustand (in-memory + localStorage persist)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Persistence&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Dexie v4 (IndexedDB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;P2P Transport&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;WebRTC (full mesh topology)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Signaling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cloudflare Workers + Durable Objects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Styling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tailwind CSS v4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tooling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Biome, TypeScript, Husky&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Why Open Source?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Transparency&lt;/strong&gt; - anyone can audit exactly what the server does (and doesn't) store&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-host&lt;/strong&gt; - deploy your own signaling worker if you want&lt;/li&gt;
&lt;/ul&gt;




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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/kushal1o1/ZeroRelay" rel="noopener noreferrer"&gt;https://github.com/kushal1o1/ZeroRelay&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Landing Page:&lt;/strong&gt; &lt;a href="https://0relay.vercel.app/landing" rel="noopener noreferrer"&gt;https://0relay.vercel.app/landing&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live Demo:&lt;/strong&gt; &lt;a href="https://0relay.vercel.app" rel="noopener noreferrer"&gt;https://0relay.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architecture Deep-Dive:&lt;/strong&gt; &lt;a href="https://github.com/kushal1o1/ZeroRelay/blob/main/ARCHITECTURE.md" rel="noopener noreferrer"&gt;ARCHITECTURE.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;README:&lt;/strong&gt; &lt;a href="https://github.com/kushal1o1/ZeroRelay/blob/main/README.md" rel="noopener noreferrer"&gt;README.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;MIT License&lt;/p&gt;

</description>
      <category>webrtc</category>
      <category>opensource</category>
      <category>p2p</category>
    </item>
    <item>
      <title>I got tired of Googling "resize,compress ,optimize image online " so I built a CLI for it</title>
      <dc:creator>Kushal Baral</dc:creator>
      <pubDate>Sun, 14 Jun 2026 07:30:03 +0000</pubDate>
      <link>https://dev.to/kushal1o1/i-got-tired-of-googling-resizecompress-optimize-image-online-so-i-built-a-cli-for-it-3lle</link>
      <guid>https://dev.to/kushal1o1/i-got-tired-of-googling-resizecompress-optimize-image-online-so-i-built-a-cli-for-it-3lle</guid>
      <description>&lt;p&gt;Every time I need to resize a batch of images, strip metadata, or convert to WebP, I end up in the same loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Google "resize image online"&lt;/li&gt;
&lt;li&gt;Pick a site, upload my photos (hope they don't keep them)&lt;/li&gt;
&lt;li&gt;Wait for download&lt;/li&gt;
&lt;li&gt;Repeat for the next thing&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Or I open GIMP. Or I write yet another 10-line Pillow script that I'll lose by tomorrow.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;ImageX&lt;/strong&gt;. It's the dumbest, simplest thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;imagex
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/Pictures &lt;span class="c"&gt;# works cross-platform with Python installed&lt;/span&gt;
imagex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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.amazonaws.com%2Fuploads%2Farticles%2Fdvzfv80xbqeni24hvhkd.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.amazonaws.com%2Fuploads%2Farticles%2Fdvzfv80xbqeni24hvhkd.png" alt="imagex menu image" width="800" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A menu pops up. Pick what you want. Done. No uploads, no ads, no "premium" upsells.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in the box
&lt;/h2&gt;

&lt;p&gt;Rotate, resize, convert, compress, watermark, strip metadata, rename batch, add noise. That covers 90% of what I ever need. Each one is an interactive prompt , no flags or command to memorize.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;Every feature is its own &lt;code&gt;.py&lt;/code&gt; file in a folder. Drop a new one in with a &lt;code&gt;NAME&lt;/code&gt;, &lt;code&gt;DESCRIPTION&lt;/code&gt;, and a &lt;code&gt;run()&lt;/code&gt; function, and it shows up in the menu automatically. That's 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="n"&gt;NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Rotate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;DESCRIPTION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Rotate images 90° Left, 90° Right, or 180°&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;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;rotated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transpose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;rotated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Added in 20 lines. No config, no registration, no boilerplate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why you should contribute
&lt;/h2&gt;

&lt;p&gt;If you've ever thought "I should make an open source PR someday" — this is a great place to start. The codebase is small, pure Python + Pillow, no framework, no build system. Adding a feature is literally writing one file. Want to add auto-color correction? Blur? Border? Side-by-side merge? Go for it.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/kushal1o1/ImageX" rel="noopener noreferrer"&gt;github.com/kushal1o1/ImageX&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PyPI: &lt;code&gt;pip install imagex&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;CONTRIBUTION.md in the repo for the quickstart&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PRs welcome. Even if it's just a feature in 20 lines.&lt;/p&gt;

</description>
      <category>python</category>
      <category>cli</category>
      <category>opensource</category>
      <category>beginners</category>
    </item>
    <item>
      <title>devmcp-context: A Simple AI Memory Layer for Your Agent</title>
      <dc:creator>Kushal Baral</dc:creator>
      <pubDate>Sun, 17 May 2026 13:37:55 +0000</pubDate>
      <link>https://dev.to/kushal1o1/devmcp-context-a-simple-ai-memory-layer-for-your-agent-176f</link>
      <guid>https://dev.to/kushal1o1/devmcp-context-a-simple-ai-memory-layer-for-your-agent-176f</guid>
      <description>&lt;p&gt;AI assistants are useful, but they often forget important details between sessions. That makes it hard to keep track of decisions, project notes, bugs, and tasks.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;devmcp-context&lt;/code&gt; solves that by giving your agent a simple memory layer that lives in your project folder. It is built as a Model Context Protocol (MCP) server, so once you connect it to an agent, the agent can use the MCP tools automatically when it needs to save, read, search, edit, or delete memory.&lt;/p&gt;

&lt;p&gt;This post is a quick, easy overview of what it does, how it works, and how you can try it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Built It
&lt;/h2&gt;

&lt;p&gt;When working with AI tools, I kept running into the same problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The agent would forget previous decisions.&lt;/li&gt;
&lt;li&gt;Important context would get buried in chat history.&lt;/li&gt;
&lt;li&gt;I needed a way to edit memory manually when something changed.&lt;/li&gt;
&lt;li&gt;I wanted a solution that was visible, simple, and file-based.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built &lt;code&gt;devmcp-context&lt;/code&gt; as a lightweight memory system for AI agents.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;devmcp-context&lt;/code&gt; stores memory in plain text files inside an &lt;code&gt;ai-context/&lt;/code&gt; folder.&lt;/p&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You can see what the agent remembers.&lt;/li&gt;
&lt;li&gt;You can edit memory directly.&lt;/li&gt;
&lt;li&gt;You can change or delete entries directly from the project folder.&lt;/li&gt;
&lt;li&gt;You can search across saved context.&lt;/li&gt;
&lt;li&gt;You do not need a database.&lt;/li&gt;
&lt;li&gt;The memory survives across sessions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Memory Categories
&lt;/h2&gt;

&lt;p&gt;The project organizes memory into five categories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;project&lt;/code&gt; for long-term project notes and conventions&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;decisions&lt;/code&gt; for architecture choices and reasoning&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;errors&lt;/code&gt; for bugs, failures, and fixes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tasks&lt;/code&gt; for work in progress&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ephemeral&lt;/code&gt; for short-lived scratchpad notes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each category helps keep the memory easy to understand instead of turning into one giant text dump.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;You do not normally tell the agent to remember every single thing by hand.&lt;/p&gt;

&lt;p&gt;Instead, you connect &lt;code&gt;devmcp-context&lt;/code&gt; as an MCP server in your agent setup, and then the agent can call the tools when needed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;context_save&lt;/code&gt; to create or update memory&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;context_load&lt;/code&gt; to read memory from a category&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;context_search&lt;/code&gt; to find matching entries&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;context_delete&lt;/code&gt; to remove an entry&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;context_status&lt;/code&gt; to see category summaries&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;context_purge_expired&lt;/code&gt; to clean up expired entries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the main idea: the agent uses the MCP tools, and the memory stays stored in your project folder where you can inspect it anytime.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Looks
&lt;/h2&gt;

&lt;p&gt;The memory is stored as markdown files, so it is easy to inspect and edit.&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.amazonaws.com%2Fuploads%2Farticles%2F47q697ugowfcclyky0dj.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.amazonaws.com%2Fuploads%2Farticles%2F47q697ugowfcclyky0dj.png" alt=" " width="341" height="568"&gt;&lt;/a&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Fuzqazvpmb8heourlo4vp.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.amazonaws.com%2Fuploads%2Farticles%2Fuzqazvpmb8heourlo4vp.png" alt=" " width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Use It
&lt;/h2&gt;

&lt;p&gt;First, install the package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;devmcp-context
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you use &lt;code&gt;uv&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv add devmcp-context
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, connect the server in your agent's MCP config. Once it is connected, the agent can use the tools automatically when it needs them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example Workflow
&lt;/h2&gt;

&lt;p&gt;Here is the simple idea:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You connect &lt;code&gt;devmcp-context&lt;/code&gt; to your agent.&lt;/li&gt;
&lt;li&gt;The agent uses the MCP tools automatically when it needs to save, read, search, edit, or delete memory.&lt;/li&gt;
&lt;li&gt;The memory is stored in plain markdown files inside your project folder.&lt;/li&gt;
&lt;li&gt;If you want, you can still open those files and edit them manually yourself.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That gives you both automation and manual control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Is Useful
&lt;/h2&gt;

&lt;p&gt;This setup is helpful when you want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Better continuity across sessions&lt;/li&gt;
&lt;li&gt;Less repetition in agent conversations&lt;/li&gt;
&lt;li&gt;Clear project memory you can audit&lt;/li&gt;
&lt;li&gt;A simple workflow that fits into Git-based projects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For me, the biggest win is that memory is no longer a black box.&lt;/p&gt;

&lt;h2&gt;
  
  
  Small Demo
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2F44il5sxae78t8al43f68.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.amazonaws.com%2Fuploads%2Farticles%2F44il5sxae78t8al43f68.png" alt=" " width="607" height="163"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In a real setup, the agent calls the MCP tools through the server, and you still keep full control of the files in the project folder.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;context_save(category="decisions", key="auth-strategy", value="Use JWT with refresh tokens", tags=["security"])
context_load(category="decisions")
context_search(query="JWT")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;devmcp-context&lt;/code&gt; is meant to be simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;file-based&lt;/li&gt;
&lt;li&gt;human-readable&lt;/li&gt;
&lt;li&gt;searchable&lt;/li&gt;
&lt;li&gt;editable&lt;/li&gt;
&lt;li&gt;persistent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is what makes it useful: the agent can use the MCP tools automatically, but the memory still lives in plain files you can open, edit, or delete whenever you want.&lt;/p&gt;

&lt;p&gt;If you are building AI-assisted workflows and want memory you can trust, this is a good place to start.&lt;/p&gt;

&lt;p&gt;If you want to try it, check out the project docs and give it a spin.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/kushal1o1/devmcp-context" rel="noopener noreferrer"&gt;github.com/kushal1o1/devmcp-context&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PyPI: &lt;a href="https://pypi.org/project/devmcp-context/" rel="noopener noreferrer"&gt;pypi.org/project/devmcp-context&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://kushal1o1.github.io/devmcp-context/" rel="noopener noreferrer"&gt;kushal1o1.github.io/devmcp-context&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>python</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Creativity Is Theft. And Nature Stole First.</title>
      <dc:creator>Kushal Baral</dc:creator>
      <pubDate>Thu, 07 May 2026 14:41:57 +0000</pubDate>
      <link>https://dev.to/kushal1o1/creativity-is-theft-and-nature-stole-first-3l94</link>
      <guid>https://dev.to/kushal1o1/creativity-is-theft-and-nature-stole-first-3l94</guid>
      <description>&lt;p&gt;Let me ask you something weird.&lt;/p&gt;

&lt;p&gt;Why does the human brain look like a neural network?&lt;/p&gt;

&lt;p&gt;Not metaphorically. Actually. Literally. The thing we built and called "artificial intelligence" — the thing we're all losing sleep over — looks suspiciously like the thing already sitting inside your skull. Neurons firing. Weights adjusting. Patterns recognizing patterns. Learning from data. Getting better over time.&lt;/p&gt;

&lt;p&gt;We didn't invent that. We copied it. And then we called it innovation.&lt;/p&gt;




&lt;p&gt;It doesn't stop at the brain either.&lt;/p&gt;

&lt;p&gt;The camera? That's an eye. Lens, aperture, light hitting a surface and creating an image. We looked at the human eye and said "yeah we want that" and spent centuries trying to build it out of glass and metal.&lt;/p&gt;

&lt;p&gt;Sonar? Bats had it first. We just figured out how to put it in submarines.&lt;/p&gt;

&lt;p&gt;Flight? Birds. Obviously. The Wright brothers didn't sit down and think from scratch. They watched things that already flew and asked "what if we did that but with wood and fabric?"&lt;/p&gt;

&lt;p&gt;Velcro was invented by a guy who kept getting burrs stuck to his dog's fur.&lt;/p&gt;

&lt;p&gt;The whole field of biomimicry exists because at some point engineers collectively admitted — &lt;em&gt;nature figured this out already, let's just copy it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So here's the uncomfortable question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is human creativity actually just... really sophisticated theft?&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;Okay stay with me here because this is where it gets interesting.&lt;/p&gt;

&lt;p&gt;Now science will tell you — nature didn't design any of this intentionally. No plan. No intention. Just random mutations and survival. And maybe that's true.&lt;/p&gt;

&lt;p&gt;But honestly? When I sit with that explanation something feels off to me.&lt;/p&gt;

&lt;p&gt;Like — the eye. Think about the eye for a second. Something that detects a single photon of light. That adjusts in real time. That self repairs. That runs on almost zero energy. That connects directly to the most complex thinking machine we've ever encountered.&lt;/p&gt;

&lt;p&gt;And we're supposed to believe that just... stumbled into existence?&lt;/p&gt;

&lt;p&gt;I'm not saying it didn't. I'm not smart enough to say that. I'm just saying — when I look at how perfect these solutions are. How every answer was already there before we thought to ask the question. It doesn't feel like accident to me. It feels like something was already trying to be understood.&lt;/p&gt;




&lt;p&gt;Over millions of years. Through billions of failures. Through creatures that couldn't see well enough dying before they reproduced and creatures that could see slightly better surviving just long enough to pass that slight improvement forward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evolution is just iteration with death as the feedback loop.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And what came out the other side? The most optimized, elegant, efficient solutions to problems that we are still — with all our intelligence and technology — struggling to fully replicate.&lt;/p&gt;

&lt;p&gt;The brain runs on roughly 20 watts of power. Your laptop charger uses more energy than the most complex thinking machine ever created. A bird's bone structure is so optimized for weight and strength that aerospace engineers still study it.&lt;/p&gt;

&lt;p&gt;Nature had billions of years and infinite iterations and the strictest possible quality control — if it didn't work, it died. Of course what survived is optimal. Of course we keep copying it. What else would we copy?&lt;/p&gt;




&lt;p&gt;So back to AI.&lt;/p&gt;

&lt;p&gt;When we built neural networks we weren't being clever. We were being honest. We looked at the only working example of general intelligence we had — the human brain — and we said "let's start there."&lt;/p&gt;

&lt;p&gt;Convolutional Neural Networks that power image recognition? Directly inspired by how the visual cortex processes information. The way your brain doesn't look at an image all at once but breaks it into edges, shapes, patterns, then assembles them — CNNs do exactly that.&lt;/p&gt;

&lt;p&gt;Reinforcement learning? That's just evolution again. Try things. Reward what works. Punish what doesn't. Repeat until something intelligent emerges.&lt;/p&gt;

&lt;p&gt;Transformers, attention mechanisms, the whole architecture behind ChatGPT and everything like it — it's all trying to approximate something the brain does naturally, effortlessly, while also somehow remembering to breathe and regulate your heartbeat at the same time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We are not building artificial intelligence. We are building a very early, very rough, very power hungry draft of what nature already perfected.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;And here's where the philosophy hits.&lt;/p&gt;

&lt;p&gt;If we copied nature. And nature copied nothing because nature invented the template. Then what is originality actually?&lt;/p&gt;

&lt;p&gt;Picasso said &lt;em&gt;"good artists borrow, great artists steal."&lt;/em&gt; But nature didn't even steal. Nature just ran the algorithm long enough that something extraordinary fell out the other side.&lt;/p&gt;

&lt;p&gt;Maybe that's what creativity actually is. Not thinking of something from nothing. Nobody does that. Nothing comes from nothing.&lt;/p&gt;

&lt;p&gt;Maybe creativity is just &lt;strong&gt;iteration with taste.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You take what exists. You combine it differently. You run it through your own specific set of experiences and failures and weird 2am thoughts. And something comes out the other side that didn't exist before. Not because you invented from zero. But because you are a specific, unrepeatable combination of everything you've ever absorbed.&lt;/p&gt;

&lt;p&gt;Just like evolution. Just like nature. Just like the neural network sitting in your skull right now reading these words and deciding whether they mean something.&lt;/p&gt;




&lt;p&gt;So is AI creative?&lt;/p&gt;

&lt;p&gt;It was trained on everything humans ever made. Every book. Every image. Every pattern. Every idea. It iterates. It combines. It produces something that didn't exist before.&lt;/p&gt;

&lt;p&gt;Sounds familiar right?&lt;/p&gt;

&lt;p&gt;The difference — and maybe this is the only difference that matters — is that &lt;strong&gt;AI doesn't have a why.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Nature iterated because death was the alternative. Humans create because something inside us reaches toward meaning. Because we feel things. Because we're trying to say something to someone. Because we're afraid of being forgotten. Because we love someone and don't know how else to say it. Because we're confused about being alive and making something feels like an answer even when it isn't.&lt;/p&gt;

&lt;p&gt;AI creates because you asked it to.&lt;/p&gt;

&lt;p&gt;That gap — between &lt;em&gt;creating because you must&lt;/em&gt; and &lt;em&gt;creating because you were prompted&lt;/em&gt; — might be the last real distinction worth talking about.&lt;/p&gt;




&lt;p&gt;And maybe this is just me thinking out loud. Maybe this is where the science ends and something else begins.&lt;/p&gt;

&lt;p&gt;But when I look at how perfectly optimized nature is. How every solution was already there before we thought to look for it. How we keep arriving at the same answers from completely different directions.&lt;/p&gt;

&lt;p&gt;It feels less like random accident to me. And more like something was already trying to be understood.&lt;/p&gt;

&lt;p&gt;Maybe evolution isn't nature figuring things out. Maybe it's something figuring itself out. Through nature. Through us. Through whatever comes next.&lt;/p&gt;

&lt;p&gt;Every tradition, every belief system, every culture that ever existed — they all pointed at something like this. Just with different words. Different stories. Different names. We rebranded it as science. And forgot to ask if they were describing the same thing all along.&lt;/p&gt;




&lt;p&gt;But that's just what I think. I'm not asking you to agree. I'm just asking you to sit with it.&lt;/p&gt;

&lt;p&gt;So here's my question for you.&lt;/p&gt;

&lt;p&gt;If creativity is just sophisticated iteration — if nature iterated blindly, humans iterated with feeling, and AI iterates with data — &lt;strong&gt;at what point in that chain did something genuinely new actually enter the world?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Or did it never? Was it always just the same thing learning about itself through different shapes?&lt;/p&gt;

&lt;p&gt;I genuinely don't know. Tell me what you think.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This was written with AI. But the question kept me up at night long before I typed the first prompt. So whose thought is it really? Maybe that's just another version of the same question.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>creativity</category>
      <category>kushal1o1</category>
    </item>
    <item>
      <title>Haul :) a tiny file organizer daemon for Linux</title>
      <dc:creator>Kushal Baral</dc:creator>
      <pubDate>Sat, 25 Apr 2026 11:21:47 +0000</pubDate>
      <link>https://dev.to/kushal1o1/haul-a-tiny-file-organizer-daemon-for-linux-4a9p</link>
      <guid>https://dev.to/kushal1o1/haul-a-tiny-file-organizer-daemon-for-linux-4a9p</guid>
      <description>&lt;p&gt;I hate messy Downloads folders. You know the feeling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;report.pdf
report(1).pdf
report(2).pdf
screenshot.png
screenshot(1).png
screenshot(2).png
video_final.mp4
video_final_FINAL.mp4
notes.docx
notes_copy.docx
notes_copy_FINAL(1).docx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At some point you just stop caring and let it rot.&lt;/p&gt;

&lt;p&gt;Cron jobs felt overkill — why poll every minute just to watch an empty folder? So I built &lt;strong&gt;haul&lt;/strong&gt;. Drop a file in a watched folder, it moves it to the right place. That's it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trick — zero CPU when idle
&lt;/h2&gt;

&lt;p&gt;haul uses &lt;code&gt;inotify&lt;/code&gt;, a filesystem event API built into the Linux kernel. Instead of running a loop, it just waits for the kernel to say "hey, a file arrived" — sorts it — then exits. systemd restarts it immediately for the next file.&lt;/p&gt;

&lt;p&gt;No polling. No persistent process. Nothing running between events.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;file lands
    ↓
kernel fires inotify
    ↓
haul wakes up
    ↓
file sorted → ~/Data/subfolder
    ↓
haul exits
    ↓
systemd restarts it
    ↓
(waiting for next file...)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;inotify-tools &lt;span class="nt"&gt;-y&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/kushal1o1/haul/main/install.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;haul install      set up and start
haul sweep        sort files already sitting in watched folders
haul start/stop   manage the service
haul logs         tail the live log
haul uninstall    remove haul (your ~/Data/ is untouched)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What it sorts
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Folder&lt;/th&gt;
&lt;th&gt;Extensions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PDFs&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.pdf&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Images&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.jpg&lt;/code&gt; &lt;code&gt;.jpeg&lt;/code&gt; &lt;code&gt;.png&lt;/code&gt; &lt;code&gt;.gif&lt;/code&gt; &lt;code&gt;.webp&lt;/code&gt; &lt;code&gt;.svg&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Videos&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.mp4&lt;/code&gt; &lt;code&gt;.mkv&lt;/code&gt; &lt;code&gt;.avi&lt;/code&gt; &lt;code&gt;.mov&lt;/code&gt; &lt;code&gt;.webm&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audio&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.mp3&lt;/code&gt; &lt;code&gt;.flac&lt;/code&gt; &lt;code&gt;.wav&lt;/code&gt; &lt;code&gt;.ogg&lt;/code&gt; &lt;code&gt;.m4a&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.py&lt;/code&gt; &lt;code&gt;.js&lt;/code&gt; &lt;code&gt;.ts&lt;/code&gt; &lt;code&gt;.sh&lt;/code&gt; &lt;code&gt;.json&lt;/code&gt; &lt;code&gt;.yaml&lt;/code&gt; &lt;code&gt;.sql&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zips&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.zip&lt;/code&gt; &lt;code&gt;.tar.gz&lt;/code&gt; &lt;code&gt;.rar&lt;/code&gt; &lt;code&gt;.7z&lt;/code&gt; &lt;code&gt;.deb&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WordFiles&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.doc&lt;/code&gt; &lt;code&gt;.docx&lt;/code&gt; &lt;code&gt;.odt&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Excel&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.xlsx&lt;/code&gt; &lt;code&gt;.xls&lt;/code&gt; &lt;code&gt;.csv&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Others&lt;/td&gt;
&lt;td&gt;everything else&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Screenshots are detected by source folder, not filename — set &lt;code&gt;SCREENSHOTS&lt;/code&gt; to wherever your tool saves them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fully configurable
&lt;/h2&gt;

&lt;p&gt;Everything — watched folders, destination, subfolder names, extension rules — is just bash variables at the top of &lt;code&gt;~/.local/bin/haul&lt;/code&gt;. Open it in any editor and change what you want.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;DATA&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/Files"&lt;/span&gt;          &lt;span class="c"&gt;# change destination&lt;/span&gt;
&lt;span class="nv"&gt;DOWNLOADS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/Desktop"&lt;/span&gt;   &lt;span class="c"&gt;# change watched folder&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After editing: &lt;code&gt;haul restart&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Duplicate handling
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Same file arrives again → deleted silently&lt;/li&gt;
&lt;li&gt;Same name, different content → renamed with a timestamp before moving&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Logs
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;[2024-01-15 14:30:22] MOVED report.pdf → ~/Data/PDFs/
[2024-01-15 14:31:45] DUPLICATE (identical) skipped: report.pdf
[2024-01-15 14:32:10] CONFLICT renamed: notes_20240115_143210.md
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Requirements:&lt;/strong&gt; Linux with systemd, bash 4+, inotify-tools. Tested on Ubuntu 22.04 / 24.04.&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/kushal1o1/haul" rel="noopener noreferrer"&gt;github.com/kushal1o1/haul&lt;/a&gt; — MIT licensed.&lt;/p&gt;

&lt;p&gt;If you've been living with a messy Downloads folder, give &lt;code&gt;haul sweep&lt;/code&gt; a try first — it sorts whatever's already there without touching the daemon.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>bash</category>
      <category>opensource</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Real MCP has a lot going on - but the core idea is this simple</title>
      <dc:creator>Kushal Baral</dc:creator>
      <pubDate>Tue, 21 Apr 2026 17:18:37 +0000</pubDate>
      <link>https://dev.to/kushal1o1/real-mcp-has-a-lot-going-on-but-the-core-idea-is-this-simple-3e6c</link>
      <guid>https://dev.to/kushal1o1/real-mcp-has-a-lot-going-on-but-the-core-idea-is-this-simple-3e6c</guid>
      <description>&lt;p&gt;MCP (Model Context Protocol) is everywhere right now. Claude uses it, Cursor uses it, a ton of AI tooling is being built on top of it. There's an official SDK, servers, transports, capabilities negotiation... it can feel like a lot.&lt;/p&gt;

&lt;p&gt;But when I actually dug into what MCP is doing at its core, I realized — it's just structured JSON going back and forth. That's the whole idea. So I built a minimal version from raw Python, no libraries, to make sure I actually understood it before touching the real thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What MCP actually is
&lt;/h2&gt;

&lt;p&gt;Strip away the SDK and the spec details — here's the mental model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a server exposes tools (functions an AI can call)&lt;/li&gt;
&lt;li&gt;a client discovers those tools and calls them by name&lt;/li&gt;
&lt;li&gt;everything is JSON, every message has a predictable shape&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The real MCP spec adds things like capability negotiation, resource types, prompt templates, SSE transport, and more. But that core loop — client asks, server responds with a tool result — is the same.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Think of it like HTTP but designed specifically for AI agents calling tools. The protocol exists so any agent can talk to any server without needing custom glue code.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What I built to understand it
&lt;/h2&gt;

&lt;p&gt;Two versions, zero external dependencies — just Python stdlib:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Basic version&lt;/strong&gt; — the absolute minimum:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;client sends a JSON request&lt;/li&gt;
&lt;li&gt;server reads it from &lt;code&gt;stdin&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;server runs a tool&lt;/li&gt;
&lt;li&gt;server replies with JSON on &lt;code&gt;stdout&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Richer version&lt;/strong&gt; — closer to real MCP, adds a JSON-RPC style flow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;initialize&lt;/code&gt; — handshake, server returns its name and version&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tools/list&lt;/code&gt; — client asks what tools exist&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tools/call&lt;/code&gt; — client calls a tool by name with arguments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That three-step flow is basically what happens every time an AI agent connects to a real MCP server.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a protocol works (the bit no one explains)
&lt;/h2&gt;

&lt;p&gt;Two processes agree on a message format, then just talk. That's it. In this case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the server sits there reading &lt;code&gt;stdin&lt;/code&gt; line by line&lt;/li&gt;
&lt;li&gt;the client writes one JSON object per line, reads one JSON response back&lt;/li&gt;
&lt;li&gt;no sockets, no framework — just text in an agreed shape&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Real MCP uses stdio transport for local servers and SSE for remote ones. Same idea, just fancier plumbing.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 basic-mcp-server.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Type a raw request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"tool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"say_hello"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bro"&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Get back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hello bro"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No library. No magic. Just Python reading and writing JSON — same as what the real SDK wraps.&lt;/p&gt;

&lt;p&gt;Full code here → &lt;a href="https://github.com/kushal1o1/Mcp-basics" rel="noopener noreferrer"&gt;https://github.com/kushal1o1/Mcp-basics&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Now I want to build an actual MCP module on top of this foundation — something useful, something real. A devtool?  A workflow helper for AI agents?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drop your idea in the comments&lt;/strong&gt; — what MCP module should I build next? I'll pick the most interesting one and document the whole thing.&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>kushal1o1</category>
      <category>mcp</category>
    </item>
  </channel>
</rss>
