<?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: Ritusmoi Kaushik</title>
    <description>The latest articles on DEV Community by Ritusmoi Kaushik (@ritusmoikaushik).</description>
    <link>https://dev.to/ritusmoikaushik</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%2F3890265%2Feccb3254-aef7-4c24-ba6f-6ee8563a0847.jpg</url>
      <title>DEV Community: Ritusmoi Kaushik</title>
      <link>https://dev.to/ritusmoikaushik</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ritusmoikaushik"/>
    <language>en</language>
    <item>
      <title>Metering a Paid API Without Overselling: the Credit Ledger Behind a Solo SaaS</title>
      <dc:creator>Ritusmoi Kaushik</dc:creator>
      <pubDate>Tue, 16 Jun 2026 07:10:36 +0000</pubDate>
      <link>https://dev.to/ritusmoikaushik/metering-a-paid-api-without-overselling-the-credit-ledger-behind-a-solo-saas-3jej</link>
      <guid>https://dev.to/ritusmoikaushik/metering-a-paid-api-without-overselling-the-credit-ledger-behind-a-solo-saas-3jej</guid>
      <description>&lt;p&gt;I charge per invoice extracted. One upload of a five-invoice PDF should cost five credits, a failed extraction should cost nothing, and a user with one credit left must never get two. That last rule is where it got interesting.&lt;/p&gt;

&lt;p&gt;This is the billing design behind &lt;a href="https://gstextract.com" rel="noopener noreferrer"&gt;GSTExtract&lt;/a&gt; — a tool that reads Indian GST invoice PDFs into Excel — and the concurrency bug a second-pass audit found in it. Writing it up because "meter a paid API correctly" is one of those problems that looks trivial until you hold it up to the light.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the problem
&lt;/h2&gt;

&lt;p&gt;Each extraction calls a vision model, which costs me real money. So the user spends a credit per invoice. The catch: I don't know how many invoices are in a PDF until &lt;em&gt;after&lt;/em&gt; the model reads it. A single page can hold three invoices; a "10-page" file might be one. I can't charge up front because I don't know the count, and I can't charge after without a window where a user with a zero balance has already burned my API budget.&lt;/p&gt;

&lt;p&gt;The answer most billing systems land on is &lt;strong&gt;reserve → settle → refund&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reserve&lt;/strong&gt; one credit before doing the expensive work. This is the gate — if it fails, the user is out of credits and nothing runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Settle&lt;/strong&gt; after success: charge the &lt;em&gt;actual&lt;/em&gt; count minus the one already reserved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refund&lt;/strong&gt; the reserve if the extraction failed, so failures are never billed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Money and credits are integers. Every change is also written to an append-only ledger with an idempotency key, so a retry can never double-charge. Balance is derived: the sum of remaining credits across non-expired "lots" (a lot is one grant — a signup bonus, a purchase). Consumption is FIFO by soonest expiry, so credits get used before they lapse.&lt;/p&gt;

&lt;p&gt;That part worked. The bug was in the word "atomic."&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug: two uploads, one credit
&lt;/h2&gt;

&lt;p&gt;The original charge function did the obvious thing:&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;# read the balance
&lt;/span&gt;&lt;span class="n"&gt;bal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;current_balance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&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;bal&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;n&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;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="c1"&gt;# ... then walk the lots and decrement in Python
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;lot&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lots&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;take&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;lot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;take&lt;/span&gt;          &lt;span class="c1"&gt;# &amp;lt;-- writes an ABSOLUTE value
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the balance, decide, write the new value. Single-threaded, this is fine. Now picture a user with exactly &lt;strong&gt;one&lt;/strong&gt; credit firing two uploads at the same moment (a double-click, or two tabs):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Request A reads &lt;code&gt;remaining = 1&lt;/code&gt;. Decides it's allowed.&lt;/li&gt;
&lt;li&gt;Request B reads &lt;code&gt;remaining = 1&lt;/code&gt;. Also decides it's allowed.&lt;/li&gt;
&lt;li&gt;A writes &lt;code&gt;remaining = 0&lt;/code&gt;, inserts a &lt;code&gt;-1&lt;/code&gt; ledger row.&lt;/li&gt;
&lt;li&gt;B writes &lt;code&gt;remaining = 0&lt;/code&gt;, inserts a &lt;code&gt;-1&lt;/code&gt; ledger row.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both succeed. The user spent one credit and got two extractions. The idempotency key didn't save me — the two requests had &lt;em&gt;different&lt;/em&gt; keys, because they're two genuinely different uploads. The read-then-write-absolute is the classic lost-update race.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: let the database do the deciding
&lt;/h2&gt;

&lt;p&gt;The cure is to stop computing the new value in Python and let the write itself be the gate — a conditional &lt;code&gt;UPDATE&lt;/code&gt; that only fires if there's still enough left, checked under the row lock:&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&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="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CreditLot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CreditLot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;lot_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CreditLot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;take&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remaining&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;CreditLot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;take&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="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rowcount&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;taken&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;take&lt;/span&gt;      &lt;span class="c1"&gt;# we actually got it
# rowcount 0 → someone else took it first; move on or roll back
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;remaining = remaining - take&lt;/code&gt; instead of &lt;code&gt;remaining = &amp;lt;number I computed&amp;gt;&lt;/code&gt;. The &lt;code&gt;WHERE remaining &amp;gt;= take&lt;/code&gt; re-checks the balance &lt;em&gt;at write time&lt;/em&gt;, and SQLite serializes writers, so the second request blocks, re-evaluates against the now-committed &lt;code&gt;remaining = 0&lt;/code&gt;, gets &lt;code&gt;rowcount = 0&lt;/code&gt;, and is correctly told it can't reserve. Exactly one upload wins. Balance can never go negative.&lt;/p&gt;

&lt;p&gt;I proved it with a test that fires N concurrent reservers at a one-credit account on a real file-backed SQLite DB (WAL mode, busy timeout — same as production, because an in-memory shared-connection DB won't reproduce writer contention). Exactly one succeeds, every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second bug: a retry that costs me money
&lt;/h2&gt;

&lt;p&gt;There was a sneakier one, found in the same audit. The workspace retries an upload once on a dropped connection, reusing the same &lt;code&gt;request_id&lt;/code&gt; so it doesn't double-charge. And it didn't double-charge — the ledger keys saw to that. But the idempotency cache was written &lt;em&gt;after&lt;/em&gt; the model call, not before. So a retry that arrived while the first call was still in flight sailed past the cache check and fired a &lt;strong&gt;second&lt;/strong&gt; vision-model call. No double charge, but I paid for two API calls and logged a duplicate usage event.&lt;/p&gt;

&lt;p&gt;Fix: mark the request in-flight &lt;em&gt;before&lt;/em&gt; the expensive call, while everything above it is still synchronous (so two requests can't both pass the check), and have a retry return &lt;code&gt;409&lt;/code&gt; instead of re-running:&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;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_REQUESTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rid&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;cached&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;cached&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;in_flight&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;JSONResponse&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;in_progress&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;409&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;JSONResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="c1"&gt;# ... reserve the credit ...
&lt;/span&gt;&lt;span class="n"&gt;_REQUESTS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rid&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;in_flight&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;# before the model call
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lesson that keeps repeating: &lt;strong&gt;idempotency has to cover the side effect, not just the database row.&lt;/strong&gt; The charge was idempotent. The expensive API call wasn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why an append-only ledger earns its keep
&lt;/h2&gt;

&lt;p&gt;The thing that made all of this debuggable: nothing is ever updated or deleted in the transaction log. Every grant, charge, and refund is an immutable row with a signed delta. That gives you a reconciliation invariant you can assert on a schedule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;the sum of all ledger deltas for a user == the sum of &lt;code&gt;remaining&lt;/code&gt; across all their lots&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If those two numbers ever disagree, a write went wrong, and an alert fires. (I also learned to &lt;em&gt;not&lt;/em&gt; fold a cache-freshness check into that alert — a balance cache that goes stale when a credit quietly expires is not corruption, and it had me chasing phantom "drift" emails for a day.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Don't read-modify-write a balance. Make the write conditional (&lt;code&gt;WHERE remaining &amp;gt;= n&lt;/code&gt;) and trust &lt;code&gt;rowcount&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Reserve → settle → refund cleanly separates "can they pay" from "what did it actually cost."&lt;/li&gt;
&lt;li&gt;Idempotency must guard the &lt;em&gt;expensive side effect&lt;/em&gt;, not only the ledger insert.&lt;/li&gt;
&lt;li&gt;An append-only ledger gives you a cheap, always-true invariant to reconcile against.&lt;/li&gt;
&lt;li&gt;Write the concurrency test against a real on-disk DB, or it won't reproduce the race you're trying to kill.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The hosted tool has a free daily tier and twenty free credits on signup, with prepaid packs for heavier use — so this ledger runs on every extraction, and I'd rather it be boringly correct. If you want to see it in action, it's at &lt;a href="https://gstextract.com" rel="noopener noreferrer"&gt;gstextract.com&lt;/a&gt;; the extraction engine is &lt;a href="https://github.com/ritusmoikaushik/gstextract-core" rel="noopener noreferrer"&gt;open source&lt;/a&gt;, though the billing layer above is part of the hosted product.&lt;/p&gt;

&lt;p&gt;If you've metered a paid API differently — especially how you handle the count-unknown-until-after problem — I'd genuinely like to hear it.&lt;/p&gt;

</description>
      <category>python</category>
      <category>sqlite</category>
      <category>saas</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Open-Sourced the Core of My GST Tool. Here's What I Kept Private.</title>
      <dc:creator>Ritusmoi Kaushik</dc:creator>
      <pubDate>Tue, 21 Apr 2026 07:12:18 +0000</pubDate>
      <link>https://dev.to/ritusmoikaushik/i-open-sourced-the-core-of-my-gst-tool-heres-what-i-kept-private-37kk</link>
      <guid>https://dev.to/ritusmoikaushik/i-open-sourced-the-core-of-my-gst-tool-heres-what-i-kept-private-37kk</guid>
      <description>&lt;p&gt;A few days ago I published the core extraction engine of GSTExtract on GitHub, MIT licensed. The &lt;a href="https://github.com/ritusmoikaushik/gstextract-core" rel="noopener noreferrer"&gt;repository is here&lt;/a&gt;. It's not the full product. It's a seven-week-old snapshot, and that gap is the whole strategy.&lt;/p&gt;

&lt;p&gt;This post is about the decisions I made picking what to open-source and what to keep private. The deciding took longer than writing the code did, which surprised me. Writing it up in case anyone else is staring at the same question on their own project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context, Briefly
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://gstextract.com" rel="noopener noreferrer"&gt;GSTExtract&lt;/a&gt; reads Indian GST invoice PDFs and pulls out the fields (GSTIN, invoice number, amounts, taxes) into Excel. Small businesses and accountants use it at month-end to avoid hand-typing supplier bills into Tally. The stack is Python — pdfplumber for digital PDFs, Tesseract OCR as fallback, regex + keyword-anchored extraction, openpyxl for Excel output. FastAPI wraps it for the hosted version. The hosted tool is free during early access, with a paid tier planned for high-volume users later.&lt;/p&gt;

&lt;p&gt;So the question was: how do I open-source without giving away the commercial edge?&lt;/p&gt;

&lt;h2&gt;
  
  
  What Went Public
&lt;/h2&gt;

&lt;p&gt;The deterministic regex plus keyword extraction pipeline. GSTIN parsing with the Luhn mod-36 checksum. Zone segmentation that splits an invoice into header and footer and routes fields by zone. Tax-line detection with keyword anchoring. Excel export. The test suite. Basically, the commoditised parsing work that anyone doing invoice extraction would eventually re-implement themselves if they cared enough.&lt;/p&gt;

&lt;p&gt;If someone wants to understand &lt;em&gt;how&lt;/em&gt; you pull GSTINs and invoice totals out of a messy PDF, the code is all there. Forkable, patchable, usable in their own pipelines.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Stayed Private
&lt;/h2&gt;

&lt;p&gt;The seven weeks of engine refinements since the cut point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multi-vendor accuracy tuning for Amazon, Flipkart, Swiggy, Zomato, BookMyShow, RedBus, and Myntra invoices (each one has a slightly different layout that breaks generic extraction)&lt;/li&gt;
&lt;li&gt;Table-based tax extraction for borderless PDFs&lt;/li&gt;
&lt;li&gt;Multi-invoice detection for combined bundles&lt;/li&gt;
&lt;li&gt;Per-field confidence scoring refinements&lt;/li&gt;
&lt;li&gt;Proprietorship invoice edge cases (handwritten-style templates that use bare "FROM" labels instead of "BILL FROM", single-digit invoice serials, and so on)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plus the entire webapp: FastAPI layer, rate limiting, CSRF, file validation, the invoice validation gate that rejects credit notes and proforma bills, the batch upload flow, the learning-data logging. All of that stays closed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Time-Lag
&lt;/h2&gt;

&lt;p&gt;This is the part I thought about most. Just publishing the latest engine would let anyone stand up a competing extraction site with the same accuracy I have. Publishing a snapshot from seven weeks ago means competitors get a working baseline, but not the current edge. The engineering work I do this month becomes public (maybe) in a few months, not immediately.&lt;/p&gt;

&lt;p&gt;Redis, MongoDB, and Elastic have all done versions of this. The open-core pattern: community gets a real, working version of the core. Commercial version stays ahead by some time delta. Nobody feels cheated, but the pricing power stays intact.&lt;/p&gt;

&lt;p&gt;For me, the specific cut was commit &lt;code&gt;0aa5f07&lt;/code&gt; from 2 March 2026, labeled "Phase 12 production hardening" in the private repo. Engine is solid from there. Everything after is refinement.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Expect to Get From This
&lt;/h2&gt;

&lt;p&gt;Honestly, the single biggest thing is the backlink. GitHub has domain authority 96. Having a public repo that links to gstextract.com from the README gives my young domain an authority signal it couldn't easily get any other way. On a site that's only a couple of months old and trying to rank for competitive GST queries, that one dofollow link is legitimately meaningful.&lt;/p&gt;

&lt;p&gt;After that, in descending likelihood:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Credibility signal to technical readers who want to see how the extraction works. Some fraction of people evaluating a hosted tool will check if the code is worth trusting.&lt;/li&gt;
&lt;li&gt;Issues and pull requests from people hitting edge cases I haven't seen. GST invoice formats are wilder than you'd think. Someone will inevitably send me a PDF from a vendor I've never heard of that breaks something.&lt;/li&gt;
&lt;li&gt;Forks from people who need self-hosted extraction for their own use cases. Rare, but they become potential collaborators.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I'm not expecting: hundreds of stars, viral adoption, a community forming around the repo. For a narrow Indian-GST tool, the realistic audience is small. A handful of serious users finding it and either using it or contributing is the bar, not hockey-stick open-source growth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest Tradeoffs
&lt;/h2&gt;

&lt;p&gt;What this might cost me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Competitors can clone and study the extraction logic. They would eventually anyway, but I've shortened the path.&lt;/li&gt;
&lt;li&gt;Time maintaining the public version even if nobody uses it. If I ignore it for months, it rots and becomes a bad signal.&lt;/li&gt;
&lt;li&gt;Some fraction of potential paying customers might think "well, the core is free, I'll just self-host" and walk away. I don't think this is many people, but it's nonzero.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I'm betting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The moat is distribution plus the continuous-improvement time-lag, not the code itself.&lt;/li&gt;
&lt;li&gt;Most competitors don't actually want to self-host and maintain a parser, deal with Tesseract and Poppler, manage their own uptime. Running the hosted tool is a service, not just code.&lt;/li&gt;
&lt;li&gt;People who'll pay for the hosted version are paying for the current-version edge, the no-setup convenience, and whatever the product becomes post-launch. The snapshot on GitHub is closer to educational than competitive.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We'll see if that's right. If self-hosting forks start cutting into hosted usage in a measurable way, I'll revisit the cut-point. For now, the calculus feels fine.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/ritusmoikaushik/gstextract-core" rel="noopener noreferrer"&gt;github.com/ritusmoikaushik/gstextract-core&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Hosted tool: &lt;a href="https://gstextract.com" rel="noopener noreferrer"&gt;gstextract.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building something similar and thinking through an open-core cut, happy to compare notes. And if you run into parsing edge cases on unusual invoice formats, open an issue on the repo.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>python</category>
      <category>saas</category>
      <category>indiehackers</category>
    </item>
  </channel>
</rss>
