<?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: Archit Mittal</title>
    <description>The latest articles on DEV Community by Archit Mittal (@automate-archit).</description>
    <link>https://dev.to/automate-archit</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%2F583571%2Fc2c0b665-b457-4281-a519-d6e76245e21f.png</url>
      <title>DEV Community: Archit Mittal</title>
      <link>https://dev.to/automate-archit</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/automate-archit"/>
    <language>en</language>
    <item>
      <title>Automations die because staff work around them, not because of bugs</title>
      <dc:creator>Archit Mittal</dc:creator>
      <pubDate>Tue, 01 Sep 2026 02:45:06 +0000</pubDate>
      <link>https://dev.to/automate-archit/automations-die-because-staff-work-around-them-not-because-of-bugs-4ajf</link>
      <guid>https://dev.to/automate-archit/automations-die-because-staff-work-around-them-not-because-of-bugs-4ajf</guid>
      <description>&lt;p&gt;Most automations I have seen fail were still running on the day they were declared dead. The server was up. The workflow triggered on time. The logs were clean. What killed them was not a bug. It was a person in your office who decided, quietly and without any announcement, to stop feeding the system, and went back to the notebook, the WhatsApp group, or the Excel file they trusted before you arrived with your improvement.&lt;/p&gt;

&lt;p&gt;This is the failure mode nobody selling automation talks about, because it cannot be fixed with better software. Your automation does not have a technical problem. It has a political problem. And if you introduced it the way most owners do, that problem started on day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The distributor order system that nobody was using
&lt;/h2&gt;

&lt;p&gt;A pattern I have watched play out with distributor-led businesses goes like this. Orders used to come in on phone calls and WhatsApp messages to one senior sales coordinator. She kept everything in her head and in a register. So the owner gets an order-capture system built. Distributors get a link or a WhatsApp flow, orders land in a sheet, stock gets checked automatically, the dispatch team gets a clean list every morning.&lt;/p&gt;

&lt;p&gt;Three months later, the owner opens the dashboard and finds a fraction of the real order volume in it. The rest is still flowing through the coordinator's phone. When a distributor calls her directly, she does not tell him to use the link. She takes the order herself, and enters it into the system later, sometimes days later, sometimes never. Dispatch has learnt that the morning list is incomplete, so they call her anyway to confirm. Which means the list is now decorative, which means the automation is dead. It runs every day. It is dead.&lt;/p&gt;

&lt;p&gt;Nobody sabotaged anything in a way you could point to. The coordinator never refused. She simply kept doing her job the way that worked, and the system starved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your best people are the ones who kill it
&lt;/h2&gt;

&lt;p&gt;Here is the uncomfortable part. The person working around your automation is usually not lazy or stupid. It is often your most capable employee, and their reasons are rational.&lt;/p&gt;

&lt;p&gt;First, the automation was aimed at them. The tasks worth automating are the ones someone currently controls: enquiry handling, order entry, follow-ups, report preparation. When you automate that, the person who owned it hears something you never said out loud: the thing that made you valuable is now a machine. You may see efficiency. They see a demotion, or a rehearsal for one.&lt;/p&gt;

&lt;p&gt;Second, the automation made their job worse before it made anything better. Every new system has a period where the person must do both, run the old way and feed the new way. If entering data into your system is extra work on top of the real work, and nobody reduced their load to make room for it, working around it is not sabotage. It is triage.&lt;/p&gt;

&lt;p&gt;Third, they carry the blame for its mistakes. When the automated WhatsApp reply quotes an outdated price, the customer does not shout at the workflow. He shouts at whoever picks up the phone. Staff learn very quickly that when the system is wrong, they get burnt, and when they quietly bypass the system, everything works. You would bypass it too.&lt;/p&gt;

&lt;h2&gt;
  
  
  The signs it is already happening
&lt;/h2&gt;

&lt;p&gt;You will not be told. You have to look. These are the signals I check for, and any two of them together mean the automation is on life support.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A parallel record exists.&lt;/strong&gt; There is a personal Excel file, a private WhatsApp group, or a physical register that holds the real data. The system holds a delayed, cleaned-up copy. Ask casually where someone would check a detail from last Tuesday. Watch where they actually look.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data arrives in batches.&lt;/strong&gt; Entries appear in the system in clumps, ten at a time, late in the evening or just before your review meeting. That means nobody is working inside the system. Someone is backfilling it to keep you satisfied. The automation has become a reporting chore, not a workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"The system was down."&lt;/strong&gt; Every workaround needs a socially acceptable excuse, and this is the universal one. If you hear it more than rarely, check the logs. When the logs say it was up, you have found not a technical issue but a permission structure: your team has agreed among themselves that this excuse is allowed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One person has become the priest of the system.&lt;/strong&gt; Only she knows how to fix stuck entries, everyone routes through her, and when she is on leave the automation pauses. That is not adoption. That is the old single-point dependency wearing a new costume.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exceptions grow instead of shrinking.&lt;/strong&gt; Every workflow starts with genuine special cases. In a healthy rollout, they get folded in and the exception list shortens. If "this one is different, I will handle it manually" covers more cases every month, the manual path is winning, and it will win completely.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to change about how you introduce it
&lt;/h2&gt;

&lt;p&gt;The fix is almost never in the software. It is in the rollout, and most of it must happen before the first automated message is ever sent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Name the person whose power it touches, and recruit them first.&lt;/strong&gt; Before building, sit with the person who currently owns the task. Not to inform them. To have them design the exception handling, because they are the only one who knows where the bodies are buried, which distributor always pays late, which customer must never get an automated reply. Make them the system's expert rather than its victim, and say plainly what their job becomes afterwards. If you cannot answer that question honestly, they will answer it for themselves, pessimistically, and act accordingly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remove the old path deliberately, or accept that it will win.&lt;/strong&gt; As long as calling the coordinator works better than using the link, the link loses. Adoption is not a motivation problem, it is a routing problem. Either the old channel formally redirects into the new one, with time allocated for that work, or you have built a second door next to an open one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Let the automation take the blame in public.&lt;/strong&gt; The first time it sends a wrong price or misses an enquiry, how you react decides everything. If the operator gets scolded, everyone learns to bypass. If you say, in front of the team, that the system got it wrong and the system will be fixed, you have made it safe to keep using. You get perhaps two or three such moments. Do not waste them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never let the automation double as a surveillance tool by stealth.&lt;/strong&gt; If the same dashboard that routes enquiries also silently ranks staff response times, they will discover it, and they will manage the metric instead of the work. If you want to measure people, say so separately. Mixing the two poisons the tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  And sometimes, do not automate it
&lt;/h2&gt;

&lt;p&gt;Some of the best advice I can give a client is to leave a process alone, and this is exactly where.&lt;/p&gt;

&lt;p&gt;If a process survives on one person's judgement, automate the drudgery around the judgement, never the judgement itself. Your accountant reconciling mismatched invoices before a GST filing is doing detective work; automate the data pull and the matching report, and leave the decision human. The senior salesperson's WhatsApp relationship with your twenty biggest buyers is an asset built over years; put an automated catalogue in her hands to send, but do not put a bot between her and them. And a process that changes every month, because the business itself is still finding its shape, should stay manual until it stops moving. Automating an unstable process just means rebuilding the automation every month, and each rebuild resets your team's trust to zero.&lt;/p&gt;

&lt;p&gt;An automation your team feels replaced by will die. An automation your team feels armed by gets defended, extended, and shown off to visitors.&lt;/p&gt;

&lt;h2&gt;
  
  
  This week
&lt;/h2&gt;

&lt;p&gt;Pick the one automation you have already paid for that feels underused. Do not open its dashboard. Instead, sit next to the person who is supposed to live in it and ask one question: "Where do you keep your own copy?" Then stay quiet. The notebook, sheet, or chat thread they show you is the real system, and everything it captures that your automation does not is your actual requirements document. Rebuild from that, with them, and this time the system will have a defender instead of an assassin.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write about automation and the systems I actually run at &lt;a href="https://architmittal.com/blog/staff-will-kill-your-automation?src=devto" rel="noopener noreferrer"&gt;architmittal.com&lt;/a&gt;. Originally published there.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>teamadoption</category>
      <category>indiansmb</category>
    </item>
    <item>
      <title>Stock Screener Automation: How a Trader Replaced ₹47K/Month Advisory with a Python Script</title>
      <dc:creator>Archit Mittal</dc:creator>
      <pubDate>Mon, 31 Aug 2026 12:45:05 +0000</pubDate>
      <link>https://dev.to/automate-archit/stock-screener-automation-how-a-trader-replaced-47kmonth-advisory-with-a-python-script-729</link>
      <guid>https://dev.to/automate-archit/stock-screener-automation-how-a-trader-replaced-47kmonth-advisory-with-a-python-script-729</guid>
      <description>&lt;p&gt;₹47,000 every month. That's what my friend Rohit was paying a fund advisory service to tell him which stocks matched his criteria. Six filters — RSI below 40, volume spike above 200% of 20-day average, 200-DMA crossover, sector rotation signals, delivery percentage above 60%, and promoter holding changes in the last quarter. The advisory ran these filters on the same NSE data that's freely available to every broker account holder in India.&lt;/p&gt;

&lt;p&gt;One Sunday afternoon, a Python script, and a free broker API later — Rohit doesn't pay that ₹47,000 anymore. "Bhai, pehle 2 ghante screening mein jaate the. Ab chai peete peete list aa jaati hai." (Earlier 2 hours went into screening. Now the list arrives while I'm having chai.)&lt;/p&gt;

&lt;p&gt;This is the story of how a retail trader replaced an expensive advisory with a script that costs literally nothing to run — and why this pattern applies to far more than just stock screening.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: 2 Hours Every Morning, Staring at Screens
&lt;/h2&gt;

&lt;p&gt;Rohit is a swing trader based in Pune. He's been trading for seven years, and his edge has always been discipline — he never enters a trade without all six of his filters confirming. The problem was execution speed. Every morning before market open, he'd manually check 150+ stocks across three different screening tools, cross-reference the results, and build his watchlist.&lt;/p&gt;

&lt;p&gt;Two hours. Every single trading day. That's roughly 500 hours a year spent on a task that follows the exact same logic every time.&lt;/p&gt;

&lt;p&gt;The advisory service he subscribed to ran similar filters, but their shortlist arrived at 9:30 AM — 15 minutes after market open. By then, the best entry prices on momentum stocks were already gone. He was paying ₹47,000 a month for a list that arrived late.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: A Python Script That Runs Before You Wake Up
&lt;/h2&gt;

&lt;p&gt;Here's what we built over that weekend. I'm not going to dump code — if you want the technical template, it's in my &lt;a href="https://github.com/archit-akg13/awesome-business-automation" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt;. Instead, let me walk you through the architecture so you understand what's actually happening.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Flow
&lt;/h3&gt;

&lt;p&gt;The script connects to Rohit's broker API at 9:15 AM — one minute before market opens. It pulls the previous day's closing data for all NSE-listed stocks (roughly 2,000 symbols). Then it runs six filters in sequence: RSI calculation using a 14-day window, volume spike detection against the 20-day moving average, 200-DMA crossover check, sector rotation scoring based on relative strength, delivery percentage from NSE's Bhavcopy data, and promoter holding changes from the latest SAST filings.&lt;/p&gt;

&lt;p&gt;The entire filtering process takes about 12 seconds. By 9:16 AM, Rohit receives a WhatsApp message with a shortlist of 5-12 stocks that passed all six filters. He's looking at his curated watchlist before the market even opens for trading.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The numbers that matter:&lt;/strong&gt; 2 hours/day reduced to 0 active time. ₹47,000/month advisory cancelled. The broker API costs a one-time ₹2,000 fee (Kite Connect). The WhatsApp Business API is free under 1,000 messages per month. Total recurring cost: ₹0.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why This Works Better Than an Advisory Service
&lt;/h2&gt;

&lt;p&gt;The advisory service wasn't bad. Their analysts were competent, and their filters were reasonable. But they had three structural disadvantages that no amount of talent could fix.&lt;/p&gt;

&lt;p&gt;First, speed. An automated script delivers results in seconds. A team of analysts reviewing the same data and formatting a report takes 30-45 minutes after market data is available. In momentum trading, those minutes are the difference between a 2% gain and a missed entry.&lt;/p&gt;

&lt;p&gt;Second, customisation. Rohit's six filters are specific to his trading style. The advisory service used their own proprietary filters, which overlapped about 70% with what Rohit wanted. That remaining 30% mismatch meant he was still doing manual screening on top of the advisory's shortlist. With his own script, the filters match his strategy exactly — because he defined them.&lt;/p&gt;

&lt;p&gt;Third, adaptability. When Rohit wanted to add a seventh filter last month (institutional buying from bulk deal data), he messaged me on a Saturday morning. By Saturday evening, the filter was live. Try asking an advisory service to customise their screening methodology for one subscriber. It doesn't happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cost Breakdown: ₹5.64L/Year Saved
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Advisory Service&lt;/th&gt;
&lt;th&gt;Python Script&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Monthly cost&lt;/td&gt;
&lt;td&gt;₹47,000&lt;/td&gt;
&lt;td&gt;₹0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Annual cost&lt;/td&gt;
&lt;td&gt;₹5,64,000&lt;/td&gt;
&lt;td&gt;₹2,000 (one-time API fee)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delivery time&lt;/td&gt;
&lt;td&gt;9:30 AM (15 min after open)&lt;/td&gt;
&lt;td&gt;9:16 AM (before open)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Customisation&lt;/td&gt;
&lt;td&gt;Fixed filters, no changes&lt;/td&gt;
&lt;td&gt;Fully custom, instant updates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual screening still needed&lt;/td&gt;
&lt;td&gt;Yes (30% filter mismatch)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time spent daily&lt;/td&gt;
&lt;td&gt;45 min (reviewing + supplementing)&lt;/td&gt;
&lt;td&gt;0 min (WhatsApp alert)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The ROI here isn't subtle. Rohit saved ₹5.64 lakh in the first year and reclaimed 500+ hours of screen time. The script paid for itself on day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Actually Need to Build This
&lt;/h2&gt;

&lt;p&gt;You don't need to be a software engineer. Rohit isn't one — he's a commerce graduate who learned basic Python from YouTube tutorials. Here's what the stack looks like.&lt;/p&gt;

&lt;p&gt;You need Python 3.x with Pandas for data crunching, a broker API account (Kite Connect from Zerodha at ₹2,000 one-time, or Fyers API v3 which is completely free), a server or always-on machine to run the script on schedule (a ₹500/month DigitalOcean droplet works, or even an old laptop running Ubuntu), and the WhatsApp Business API for notifications (free under 1,000 messages/month).&lt;/p&gt;

&lt;p&gt;The conceptual approach is straightforward. You authenticate with your broker's API using OAuth tokens. You fetch historical OHLCV (Open, High, Low, Close, Volume) data for your universe of stocks. You calculate your technical indicators using Pandas and NumPy. You apply your filter logic — simple conditional statements. And you send the results via WhatsApp or Telegram using their respective APIs.&lt;/p&gt;

&lt;p&gt;If you've ever written an IF statement in Excel, you can understand the logic behind a stock screener script. The broker API handles the hard part — getting clean, real-time market data. Your script just applies the rules you already know.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Lesson: Automation Is Cheapest Where Expertise Is Expensive
&lt;/h2&gt;

&lt;p&gt;Rohit's story isn't really about stock screening. It's about a pattern I see in every consulting engagement: the most expensive services are often the ones running the simplest logic on publicly available data.&lt;/p&gt;

&lt;p&gt;I've seen the same pattern in &lt;a href="https://dev.to/blog/weekend-python-script-ca-firm-209-hours-itr"&gt;CA firms spending lakhs on seasonal staff&lt;/a&gt; to do repetitive ITR data entry. I've seen it in businesses paying for SaaS tools when &lt;a href="https://dev.to/blog/zero-rupee-automation-stack-enterprise-workflows"&gt;a ₹0 stack of free tools&lt;/a&gt; does the same job. And I've seen it in the &lt;a href="https://dev.to/blog/over-automation-trap-when-not-to-automate"&gt;opposite direction too&lt;/a&gt; — people automating things that should have stayed manual.&lt;/p&gt;

&lt;p&gt;The sweet spot for automation is repetitive tasks with clear rules and expensive human execution. Stock screening is a textbook case: the rules are explicit (RSI below X, volume above Y), the data is public, the task repeats daily, and the human alternative costs either time or money. "Jo kaam roz same tarike se hota hai, woh script ka kaam hai — insaan ka nahi." (Work that happens the same way every day is a script's job — not a human's.)&lt;/p&gt;

&lt;p&gt;Before you automate anything in your workflow, ask yourself these three questions. Is the logic explicit enough that you could explain it to a 10-year-old? Does the task repeat on a predictable schedule? Is the current cost of doing it manually more than the one-time cost of building the script? If all three answers are yes, you've found your next automation project.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happened After
&lt;/h2&gt;

&lt;p&gt;Six months into using the script, Rohit's trading performance improved — not because the script picked better stocks, but because it freed his morning for what actually matters: reading quarterly results, analysing management commentary, and making judgment calls on his filtered shortlist. The screening was never the value-add. His analysis was. The script just removed the busywork standing between him and the work that actually moves the needle.&lt;/p&gt;

&lt;p&gt;That's what good automation does. It doesn't replace your expertise. It removes the mechanical layer sitting on top of it so you can spend 100% of your time on the 20% of work that requires human judgment.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can I automate stock screening with a free broker API in India?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. Brokers like Zerodha (via Kite Connect) and Fyers offer APIs that let you pull live market data and run custom filters programmatically. Kite Connect charges a one-time ₹2,000 fee but has no monthly cost. Fyers API v3 is completely free. Combined with Python libraries like Pandas, you can build a fully automated stock screener at near-zero recurring cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much time does an automated stock screener save compared to manual screening?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the case study described in this article, the trader went from 2 hours of manual screening every morning to zero active screening time. The script runs automatically at 9:16 AM and delivers a filtered shortlist of 5-12 stocks via WhatsApp within 30 seconds of market open.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is it legal to use trading automation scripts in India?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Using scripts for stock screening, alerts, and analysis is perfectly legal. Automated order execution (algo trading) for retail investors requires broker approval and exchange registration under SEBI guidelines. The screener described here only filters and alerts — it does not place trades automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What programming skills do I need to build a stock screener bot?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Basic Python knowledge is sufficient. You need familiarity with Pandas for data manipulation, the requests library for API calls, and a basic understanding of technical indicators like RSI, moving averages, and volume analysis. Most broker APIs have excellent documentation with code samples.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write about automation and the systems I actually run at &lt;a href="https://architmittal.com/blog/stock-screener-automation-trader-replaced-47k-advisory-python?src=devto" rel="noopener noreferrer"&gt;architmittal.com&lt;/a&gt;. Originally published there.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>trading</category>
      <category>python</category>
      <category>automation</category>
      <category>finance</category>
    </item>
    <item>
      <title>The second follow-up fails because it says the same thing as the first</title>
      <dc:creator>Archit Mittal</dc:creator>
      <pubDate>Mon, 31 Aug 2026 02:45:07 +0000</pubDate>
      <link>https://dev.to/automate-archit/the-second-follow-up-fails-because-it-says-the-same-thing-as-the-first-28n</link>
      <guid>https://dev.to/automate-archit/the-second-follow-up-fails-because-it-says-the-same-thing-as-the-first-28n</guid>
      <description>&lt;p&gt;Most sales follow-up does not fail because there are too few messages. It fails because the second and third messages are the first message wearing a different date. The prospect has already read your pitch once. Sending it again with "just following up" on top gives them nothing new to respond to, so they do not respond, and the salesperson takes the silence as a no. That is the point where most sequences quietly die: not at touch six, at touch two.&lt;/p&gt;

&lt;p&gt;I have built follow-up systems for distributors, agencies, coaching businesses and manufacturers, and I have watched the same pattern in every one of them. The tooling was fine. The cadence was fine. The content of touches two and three was the problem, and no amount of scheduling fixes content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the second message is the hard one
&lt;/h2&gt;

&lt;p&gt;The first message is easy. You have a reason to write it: the enquiry came in, the meeting happened, the quotation went out. There is a natural subject.&lt;/p&gt;

&lt;p&gt;The second message has no natural subject. Nothing new has happened. So the salesperson reaches for the only thing available, which is to repeat the ask. "Did you get a chance to look at the proposal?" "Any update on this?" "Checking in on the quote I sent." Every one of these lines asks the prospect to do work (reopen the file, form an opinion, write back) while offering nothing in return.&lt;/p&gt;

&lt;p&gt;From the prospect's side, the calculation is simple. Replying costs effort. The message provides no new information that would change their decision. So they do the rational thing and leave it. It is not rejection. It is just an unbalanced trade.&lt;/p&gt;

&lt;p&gt;By touch three the salesperson feels awkward. They have now asked twice with no reply. The third message either gets softer ("no pressure, just checking") or does not go out at all. This is where I see pipelines leak the most, and it is almost never visible in the CRM, because the deal is still marked "proposal sent" for months.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix is a ladder, not a schedule
&lt;/h2&gt;

&lt;p&gt;A follow-up sequence works when each message could stand on its own as something worth reading, even if the prospect had never heard from you before. That means every message after the first must carry something the previous one did not. Not a new tone. New information.&lt;/p&gt;

&lt;p&gt;Here is the ladder I use as a base. The exact wording changes by business, but the job of each rung does not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Touch one: the thing they asked for.&lt;/strong&gt; The quotation, the answer to their question, the meeting summary. This is the only message where the ask is the whole point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Touch two: the constraint they have not considered.&lt;/strong&gt; Something about their situation that affects the decision and that you did not cover in touch one. For a distributor it might be a lead time that changes after a certain date. For a services business it might be a dependency on their side that needs to be in place before work can start. The message informs; the ask is implicit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Touch three: a worked example of a similar case.&lt;/strong&gt; Not a testimonial with a number attached. A short, specific description of how someone in a comparable position went about the decision, what they worried about, and what they did first. This gives the prospect a template for thinking, which is the thing most stalled buyers are actually missing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Touch four: a direct question with a small answer.&lt;/strong&gt; Not "any update?" but a question that can be answered in one word and that tells you which branch you are on. "Is this on hold for now, or still being compared?" Either answer is useful to you, and both are easy to give.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Touch five: the close-the-file message.&lt;/strong&gt; You state plainly that you will stop following up, leave the door open, and say what to do if they come back later. Written well, this message gets more replies than touches two and three combined, because it removes all pressure and makes silence an active choice.&lt;/p&gt;

&lt;p&gt;Notice that only touches one and five contain the word "proposal" or a direct request. The middle three are giving, not asking. That is the whole trick.&lt;/p&gt;

&lt;h2&gt;
  
  
  A worked example: distributor orders after GST filing week
&lt;/h2&gt;

&lt;p&gt;Take a packaging supplier selling to FMCG distributors. Their enquiries cluster in the last week of the month, when distributors are placing orders for the next cycle. The supplier sends a quote, and then the distributor goes silent because it is now the first week of the month and their accountant has taken over their attention with GST filings.&lt;/p&gt;

&lt;p&gt;The old sequence looked like this: quote on the 28th, "did you see the quote" on the 2nd, "following up on the quote" on the 6th, then nothing. The distributor was never saying no. They were doing GSTR-1 and had no bandwidth.&lt;/p&gt;

&lt;p&gt;The laddered version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;28th:&lt;/strong&gt; quote, as before.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;4th:&lt;/strong&gt; a one-line note that the current rate holds until a specific date because of how the supplier buys raw material, and that orders after that will be quoted fresh. This is true, it is new, and it tells the distributor when they actually need to decide.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;9th:&lt;/strong&gt; a short description of how another distributor of a similar size handles the month-start crunch by placing a standing order for their two fastest-moving SKUs and quoting only the rest. No numbers, just the shape of the arrangement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;13th:&lt;/strong&gt; "Is this parked until after filing, or are you comparing suppliers?" One word answers it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;18th:&lt;/strong&gt; the close-the-file message, with a note that the first-of-month rate can be re-quoted in one message on WhatsApp whenever they are ready.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The distributor is now receiving a sequence that respects the fact they are busy and gives them a reason to reply at each step. Whether they order is still up to them. But the sequence no longer dies at touch two because the salesperson felt awkward.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to automate, and what to leave alone
&lt;/h2&gt;

&lt;p&gt;This is where I differ from most people selling automation. The ladder has parts that should be automated and parts that should not, and getting that split wrong is worse than not automating at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Automate the timing and the reminders.&lt;/strong&gt; A simple system that watches the CRM stage, counts days since the last outbound message, and puts a task in front of the salesperson each morning saying "touch three due for these four prospects" removes the single biggest cause of dropped sequences, which is forgetting. This can be built in most CRMs natively, or with a sheet and a scheduled script if you are not on a CRM yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Automate touches one and five.&lt;/strong&gt; Touch one is a template with the quote attached. Touch five is a template with one name field. Both are safe to send from a system, over WhatsApp Business API or email, because they do not pretend to be personal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not automate touches two, three and four.&lt;/strong&gt; I have tried. Touch two requires knowing what constraint applies to this prospect, and a generic "prices may change" line reads as a pressure tactic and gets the message ignored or the sender blocked. Touch three requires choosing a genuinely comparable case, which a system cannot judge without more data than most small businesses hold. Touch four is a question, and questions from a bot get bot-level replies.&lt;/p&gt;

&lt;p&gt;What works instead is a middle path: the system drafts touch two and three from a short library the salesperson has written (three or four constraint notes, three or four case sketches), tags the most likely one based on the prospect's category, and holds it for a human to approve or swap before sending. Approval takes seconds. The message that goes out is still one a human chose.&lt;/p&gt;

&lt;p&gt;I have seen businesses fully automate the middle rungs because a vendor said they could. Reply rates went to nothing within a few weeks, and worse, the salespeople stopped reading the sequence because "the system handles it". You end up with the same dead pipeline you had before, now with a subscription attached.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other place this goes wrong
&lt;/h2&gt;

&lt;p&gt;Even with a ladder, sequences fail when the library is thin. If a salesperson has one constraint note and one case sketch, every prospect gets the same touch two and touch three, and within a month the team hears back "you sent this to my partner as well". The library needs refreshing roughly as often as the product or the season changes, and someone has to own that. In a small business that is usually the founder, because nobody else knows enough cases to write them.&lt;/p&gt;

&lt;p&gt;The second failure is measuring the wrong thing. Counting touches sent tells you nothing. The number that matters is how many prospects reach touch four and give you a one-word answer, because that is the point where you learn something. If you are not tracking which rung a prospect is on, you cannot see where the sequence leaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  One thing to do this week
&lt;/h2&gt;

&lt;p&gt;Open your CRM, or your WhatsApp Business chat list, and find every prospect marked "quote sent" or "proposal sent" where the last message went out more than ten days ago and there has been no reply. Do not send them another "following up". Instead, write touch two for each one by hand: one genuine constraint they have not been told about, in two or three lines, with no ask at the end.&lt;/p&gt;

&lt;p&gt;Send those. Then note, for each, whether the reply came. That list, and the responses to it, is the first draft of your library. Once you have it, the timing is the only part worth handing to a machine.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write about automation and the systems I actually run at &lt;a href="https://architmittal.com/blog/the-third-follow-up-message?src=devto" rel="noopener noreferrer"&gt;architmittal.com&lt;/a&gt;. Originally published there.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>salesfollowup</category>
      <category>whatsappautomation</category>
      <category>smallbusinessindia</category>
    </item>
    <item>
      <title>Most of Your CRM Is Dead. Prove It Before You Buy Another Lead</title>
      <dc:creator>Archit Mittal</dc:creator>
      <pubDate>Sun, 30 Aug 2026 12:45:03 +0000</pubDate>
      <link>https://dev.to/automate-archit/most-of-your-crm-is-dead-prove-it-before-you-buy-another-lead-1oil</link>
      <guid>https://dev.to/automate-archit/most-of-your-crm-is-dead-prove-it-before-you-buy-another-lead-1oil</guid>
      <description>&lt;p&gt;Every owner I meet wants more leads. Almost none of them can tell me what happened to the leads they already paid for.&lt;/p&gt;

&lt;p&gt;Open your CRM right now, or your Excel sheet, or the WhatsApp chats your sales fellow keeps on his personal phone, which is where most Indian businesses actually keep their pipeline. Count the people in there who enquired, got a quote, and then went quiet. In every business I have looked inside, from a Ludhiana components manufacturer to a Gurgaon interiors firm, that number is larger than the number of active deals. Usually much larger.&lt;/p&gt;

&lt;p&gt;Here is the uncomfortable arithmetic. Every name in that pile cost you money once. You paid for the ad, the exhibition stall, the Justdial listing, the referral dinner. The person raised their hand, which is the single hardest thing to get a stranger to do. And then, because nobody followed up past the second call, they were quietly reclassified from "lead" to "old data" and you went back to the market to buy a fresh stranger.&lt;/p&gt;

&lt;p&gt;Reviving someone who already enquired costs you a message and a phone call. Acquiring a new stranger costs you whatever your ad platform is charging this quarter, and that price only moves in one direction. You do not need my numbers for this; pull your own ad spend and divide it by qualified enquiries. Then compare it with the cost of a WhatsApp template message. The old list wins before you finish the calculation.&lt;/p&gt;

&lt;p&gt;But, and this is the part everyone skips, you cannot just hand the old list to your sales team and say "call everyone". That is how revival campaigns die in a week. The list has to be sorted first, and the sorting is the actual work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "call the old list" always fails
&lt;/h2&gt;

&lt;p&gt;I have watched this play out. The owner has the same realisation you just had, forwards the Excel to the sales head, and says get on it. Three days later the team has stopped, for reasons that are entirely predictable.&lt;/p&gt;

&lt;p&gt;The list is full of numbers that don't connect, people who bought from a competitor two years ago, one fellow who has died, and three who get angry because this is the fourth company calling them this month. The team makes forty calls, gets two conversations and one abuse, and concludes the old list is rubbish. The owner concludes the team is lazy. Both go back to buying new leads, now with proof that revival "doesn't work here".&lt;/p&gt;

&lt;p&gt;The list was never rubbish. It was unsorted. Dead and dormant were mixed together, and the dead ones poisoned the effort before the dormant ones could pay for it.&lt;/p&gt;

&lt;p&gt;Dead means the reason to buy has expired. Dormant means the reason still exists but the conversation lapsed. The entire game is separating the two before a human being dials a single number.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sort: four questions before anyone calls
&lt;/h2&gt;

&lt;p&gt;You sort the list by asking four questions of every record, in order. The first three can be answered from data you already have. Only the fourth needs a human.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One: can this reason to buy recur?&lt;/strong&gt; A CA firm's list of people who enquired about ITR filing is not dead in December; it is dormant until next June, because ITR season comes back every year like the monsoon. The same is true of GST registration enquiries, insurance renewals, annual maintenance contracts, school admission season, wedding-season orders. If the need is cyclical, the lead cannot be dead, only sleeping until the cycle turns. Tag these first and set the date they wake up. This is the highest-value tag in the whole exercise and it requires nothing but knowing your own business.&lt;/p&gt;

&lt;p&gt;If the need was one-time and has clearly been met elsewhere, the buyer of a flat who enquired about interiors three years ago and has since moved in, mark it dead and stop grieving.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two: does the person still exist in that role?&lt;/strong&gt; For business buyers, the enquiry came from a person at a company, and Indian companies churn purchasing staff constantly. A two-minute LinkedIn check tells you whether the purchase manager who asked for your quote is still there. If she has moved on, the record is not dead, but it is not dormant either; it is a fresh cold lead to whoever replaced her, and it should be worked like one. For consumer records, a WhatsApp check tells you whether the number is even live, which brings us to the next question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three: does the channel still reach them?&lt;/strong&gt; Before any calling, send the whole shortlist one honest WhatsApp template message. Not a pitch. Something like: "You had enquired with us about distributor pricing a while back. We're cleaning our records. Still relevant, or should we close the file?" Watch the ticks. Numbers that bounce are dead. Single grey tick for a fortnight, park it. Delivered and read, even with no reply, means the channel is alive and the record earns a call. Anyone who replies "closed" has done you the favour of sorting themselves; thank them and mark it dead. Anyone who replies with a question just moved to the top of tomorrow's call list.&lt;/p&gt;

&lt;p&gt;This one message does more sorting than a week of dialling, because it filters on the cheapest signal available before spending the most expensive resource you have, which is a competent salesperson's morning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Four: what does the first call say?&lt;/strong&gt; Only now does a human call, and only the records that survived the first three questions. The call is not "just following up", which is the phrase that has trained all of India to hang up. The call references the original enquiry specifically: the model number they asked about, the quantity, the site location. That detail is sitting in your old WhatsApp threads and quotation PDFs. Using it is what tells the person this is a company that remembers them, not a call centre with a purchased list.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to automate, and what not to
&lt;/h2&gt;

&lt;p&gt;The tagging, the recurrence dates, the WhatsApp sweep, the sorting of replies into buckets: automate all of it. This is exactly the drudgery software is for, and it is why the sort never happens when it is left to humans with a spreadsheet.&lt;/p&gt;

&lt;p&gt;The reactivation conversation itself: do not automate it. I will say that plainly because every tool vendor is selling you the opposite. There are platforms that will happily run an "AI re-engagement sequence" over your whole database, and what they actually do is send five templated messages to people including the dead ones, which converts your one remaining asset, the memory of a decent past interaction, into an unsubscribe. A dormant lead came back to life because a human said something specific to them. A robot saying "We miss you" has never revived anyone, and you only get one chance to knock on that door before it stops being opened.&lt;/p&gt;

&lt;p&gt;Automate the sorting. Keep the conversation human. That division of labour is the whole system.&lt;/p&gt;

&lt;h2&gt;
  
  
  One thing to do this week
&lt;/h2&gt;

&lt;p&gt;Do not buy software this week. Export your list, every source, including the sales team's personal WhatsApp threads, into one sheet, and run question one on it: tag every record where the reason to buy recurs, and write down the month it recurs. That single column, built in an afternoon, will show you more pipeline than your next month of ad spend, and it will tell you whether the rest of this system is worth building.&lt;/p&gt;

&lt;p&gt;If the column comes back nearly empty, your business genuinely runs on one-time buyers and your money does belong in new acquisition. At least now you will know, instead of guessing in both directions and paying for it twice.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write about automation and the systems I actually run at &lt;a href="https://architmittal.com/blog/your-crm-is-a-graveyard?src=devto" rel="noopener noreferrer"&gt;architmittal.com&lt;/a&gt;. Originally published there.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>crm</category>
      <category>leadrevival</category>
      <category>salesautomation</category>
    </item>
    <item>
      <title>Your Website Chatbot Is Talking to Nobody. The Dashboard Proves It</title>
      <dc:creator>Archit Mittal</dc:creator>
      <pubDate>Sun, 30 Aug 2026 02:45:06 +0000</pubDate>
      <link>https://dev.to/automate-archit/your-website-chatbot-is-talking-to-nobody-the-dashboard-proves-it-33md</link>
      <guid>https://dev.to/automate-archit/your-website-chatbot-is-talking-to-nobody-the-dashboard-proves-it-33md</guid>
      <description>&lt;p&gt;The first automation most Indian businesses buy is a chatbot for their website. It is also, almost always, the automation installed on the surface their customers visit least. Your buyers are on WhatsApp, on Google Maps, on Instagram, on the phone. Your website is where they go once, to confirm you exist, before messaging you somewhere else. The bot sits in the corner of that page like a receptionist in a building nobody enters.&lt;/p&gt;

&lt;p&gt;I am not guessing at this. I have built these systems, and I have opened the dashboards afterwards. The pattern repeats: a business pays for a chatbot subscription for a year, renews it without looking, and at no point does anyone ask the only question that matters. How many real conversations did this thing actually have?&lt;/p&gt;

&lt;p&gt;You can answer that question in under an hour, before your next renewal. This post shows you how.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the website got the bot first
&lt;/h2&gt;

&lt;p&gt;The website gets the bot first because that is what vendors sell first. A website widget is the easiest thing for a chatbot company to install. There is no WhatsApp Business API approval, no template registration, no Meta verification. The vendor drops one script tag on your site, shows you the little chat bubble, and the demo looks impressive because in the demo, someone is talking to it.&lt;/p&gt;

&lt;p&gt;So the decision gets made backwards. Nobody asked "where do our enquiries actually arrive?" The question that got asked was "where is it easiest to put a bot?" Those are different questions with different answers, and the gap between them is where the subscription money goes.&lt;/p&gt;

&lt;p&gt;Think about how your own customers reach you. If you run a distribution business, your retailers WhatsApp their orders. If you run a clinic, patients call or walk in from a Google Maps listing. If you run a services firm, referrals come by phone and email. In every one of these, the website is a brochure. People check the address, maybe the phone number, and leave. A brochure does not need a receptionist.&lt;/p&gt;

&lt;h2&gt;
  
  
  A worked example: the distributor
&lt;/h2&gt;

&lt;p&gt;Take a business every Indian owner recognises. An FMCG distributor covering a few districts. Retailers place orders the same way they have for years: a WhatsApp message to the salesman, often a photo of a handwritten list, sometimes a voice note. Payment reminders go out on WhatsApp. Scheme details go out on WhatsApp. During GST filing week, the accountant is chasing missing invoices, on WhatsApp.&lt;/p&gt;

&lt;p&gt;This distributor gets pitched a chatbot. The vendor puts it on the company website, the one that was built years ago and last updated when the address changed. The bot can answer questions about products, working hours, and coverage area.&lt;/p&gt;

&lt;p&gt;Now walk through who visits that website. Retailers do not. They have the salesman's number saved. New retailers hear about the distributor through the company's sales officer or through other shopkeepers, and their first touchpoint is a phone call. The people who do land on the website are job applicants, the occasional brand manager doing due diligence, and the owner's nephew checking whether the site is still up.&lt;/p&gt;

&lt;p&gt;The bot is fluent, polite, available at three in the morning, and talking to nobody. Meanwhile the actual queue of customer conversations, the WhatsApp inbox where orders arrive and sit unanswered between two and five in the afternoon because the salesman is on the road, has no automation at all. The money went to the empty room.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check the real number before you renew
&lt;/h2&gt;

&lt;p&gt;Do not ask the vendor whether the bot is working. Ask the dashboard. Every chatbot platform keeps a conversation log, because that log is how they justify the renewal. Here is the check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step one: find the conversations report.&lt;/strong&gt; Log into the chatbot dashboard. Look for a section called Conversations, Chats, Sessions, or Inbox. Set the date range to the last three months, not the last week. A short window hides the emptiness; one busy day can make a dead month look alive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step two: separate real conversations from noise.&lt;/strong&gt; The headline number is inflated by design. Most platforms count a "conversation" the moment the widget loads or the bot fires its automatic greeting, even if the visitor never types a word. Filter for conversations where a human actually sent at least one message. Then subtract your own noise: your staff testing it, the vendor's team testing it, you checking it after reading this post. What is left is the true count.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step three: read what people actually asked.&lt;/strong&gt; Open the transcripts of the real conversations. This is the most honest ten minutes you will spend on your marketing stack. You will usually find three kinds of messages: people asking for the phone number so they can leave the website and call you, people asking something the bot could not answer, and job seekers. Count how many transcripts end with the bot successfully resolving something a human would otherwise have handled. That number is the bot's entire contribution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step four: compare it to one day of WhatsApp.&lt;/strong&gt; Open WhatsApp Business on the phone where enquiries land. Count yesterday's incoming customer conversations. Just yesterday. Now put that single day next to three months of website bot transcripts. For most businesses I have looked at, this comparison is not close, and it settles the renewal decision on its own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step five: check who even visits the site.&lt;/strong&gt; If you have Google Analytics or Search Console connected, look at monthly visitors and which pages they see. If most sessions are a few seconds on the homepage, you have confirmed the surface itself is quiet. No bot, however clever, generates conversations on a page nobody opens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sometimes the honest answer is: don't automate this
&lt;/h2&gt;

&lt;p&gt;Here is the part the industry will not tell you, because everyone in it is paid when you automate more, not less.&lt;/p&gt;

&lt;p&gt;If your website gets a trickle of visitors, the correct amount of website automation is none. Not a better bot, not an AI upgrade to the existing bot, not a redesign with the bot moved to the centre of the page. None. Put a clear phone number and a WhatsApp click-to-chat link on the site and let the website do its actual job, which is being a brochure. A link that opens WhatsApp costs nothing, needs no renewal, and delivers the visitor to the channel where you already answer people.&lt;/p&gt;

&lt;p&gt;The same honesty applies on WhatsApp itself. Even where the volume genuinely is, not everything should be handed to a bot. Order confirmations, catalogue requests, working hours, payment reminders: automate those, they are repetitive and low-stakes. But a retailer angry about a short supply, or a new customer with a big first order, should reach a human quickly. A bot that stalls a hot enquiry with menu options does more damage than no bot at all. Automation should absorb the repetitive middle of your inbox, not fence off the conversations that make you money.&lt;/p&gt;

&lt;p&gt;If a vendor never tells you "this channel does not need automation", you are not talking to an adviser. You are talking to a renewal.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do this week
&lt;/h2&gt;

&lt;p&gt;One action. Log into your chatbot dashboard, set the range to the last three months, and count the conversations where a real customer typed a real message and the bot resolved it without a human stepping in. Write that number down next to what the renewal costs you.&lt;/p&gt;

&lt;p&gt;If the number is healthy, keep the bot and ignore me. If the number is close to zero, cancel the renewal, add a WhatsApp click-to-chat link to your website in its place, and spend the money where your customers already are. Either way, you will have made the decision from your own data instead of the vendor's demo, and that habit is worth more than any single subscription.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write about automation and the systems I actually run at &lt;a href="https://architmittal.com/blog/website-chatbot-talks-to-nobody?src=devto" rel="noopener noreferrer"&gt;architmittal.com&lt;/a&gt;. Originally published there.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>chatbots</category>
      <category>automation</category>
      <category>smallbusiness</category>
    </item>
    <item>
      <title>How a Weekend Python Script Saved a CA Firm 209 Hours During ITR Season</title>
      <dc:creator>Archit Mittal</dc:creator>
      <pubDate>Sat, 29 Aug 2026 12:45:06 +0000</pubDate>
      <link>https://dev.to/automate-archit/how-a-weekend-python-script-saved-a-ca-firm-209-hours-during-itr-season-2on0</link>
      <guid>https://dev.to/automate-archit/how-a-weekend-python-script-saved-a-ca-firm-209-hours-during-itr-season-2on0</guid>
      <description>&lt;p&gt;₹3,12,000. That's what Rajesh Sharma's CA firm in Jaipur spent every ITR season hiring three temporary data entry operators for two months. They'd sit in a back room extracting numbers from Form 16 PDFs, cross-checking PAN details against the income tax portal, and pre-filling ITR forms — one cell at a time, one return at a time, 340 returns total.&lt;/p&gt;

&lt;p&gt;Last July, a single Python script replaced that entire process. Built in one weekend. Running cost: ₹0. Time saved across the season: 209 hours.&lt;/p&gt;

&lt;p&gt;This is not a theoretical tutorial. This is what actually happened, with real numbers, real friction, and the parts that almost went wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: ITR Season Is a Two-Month Data Entry Marathon
&lt;/h2&gt;

&lt;p&gt;If you've never worked inside a CA firm during July-September, here's what it looks like. Clients send Form 16 PDFs over WhatsApp — sometimes as photos, sometimes as scanned copies with coffee stains. The firm's staff manually opens each PDF, types salary figures into a master Excel sheet, cross-references PAN numbers on the income tax portal, and fills in the ITR form fields one by one.&lt;/p&gt;

&lt;p&gt;Rajesh's firm handled 340 individual ITRs last year. Each return took an average of 38 minutes of pure data processing before a CA even looked at it for deductions and exemptions. That's 215 hours of someone staring at PDFs and typing numbers into boxes.&lt;/p&gt;

&lt;p&gt;The three temporary hires cost ₹1,04,000 each for the season — ₹3,12,000 total. And they still made errors. Rajesh's senior CA spent another 40+ hours catching and fixing data entry mistakes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Har saal yahi hota tha. Teen log hire karo, train karo, phir unki galtiyan dhundo. It felt like we were paying people to create problems we'd solve ourselves." (Every year the same thing. Hire three people, train them, then find their mistakes.)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Script: What It Actually Does
&lt;/h2&gt;

&lt;p&gt;I didn't build Rajesh an AI platform. I didn't sell him a subscription. I built a Python script that does three things — and does them well.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 1: PDF Text Extraction
&lt;/h3&gt;

&lt;p&gt;The script uses an open-source PDF library to read every Form 16 PDF in a folder. It extracts employer name, PAN, salary breakdowns (basic, HRA, special allowance), TDS deducted, and Section 80C/80D declarations. For scanned or photo PDFs — which made up about 30% of the documents — it falls back to OCR using Tesseract, another free tool.&lt;/p&gt;

&lt;p&gt;This stage alone eliminated about 70% of the manual typing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 2: Validation and Cross-Check
&lt;/h3&gt;

&lt;p&gt;Every extracted PAN is validated against a checksum algorithm (PAN numbers follow a specific format — the script catches typos instantly). Salary totals are cross-checked: does basic + HRA + special allowance + other components equal gross salary? If not, the return gets flagged for manual review instead of silently carrying an error forward.&lt;/p&gt;

&lt;p&gt;This is where the script outperformed humans consistently. The temporary hires caught about 60% of calculation mismatches. The script catches 100% — because it doesn't get tired at 4 PM on a Thursday.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 3: Pre-Filled Output
&lt;/h3&gt;

&lt;p&gt;The script generates a structured spreadsheet with every field mapped to the ITR form layout. Rajesh's CAs open the sheet, review the numbers, apply their professional judgment on deductions and exemptions, and file. The 38-minute-per-return data processing step dropped to about 6 minutes of review.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The Numbers:&lt;/strong&gt;&lt;br&gt;
Before: 38 min/return x 340 returns = 215 hours of data processing&lt;br&gt;
After: 6 min/return x 340 returns = 34 hours of CA review&lt;br&gt;
Net saving: ~209 hours (after accounting for the 28 returns flagged for manual processing)&lt;br&gt;
Cost saving: ₹3,12,000 in seasonal hiring — replaced by a script with ₹0 running cost&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Almost Went Wrong
&lt;/h2&gt;

&lt;p&gt;I'd be lying if I said the script worked perfectly from day one. Two things nearly derailed it.&lt;/p&gt;

&lt;p&gt;The first problem was PDF variety. Indian employers don't follow a standard Form 16 template. Some PDFs had salary components in tables, others in plain text paragraphs, and a few used formats I'd never seen — with Devanagari headers mixed into English content. The first version of the script handled about 75% of PDFs correctly. I spent the second day of that weekend writing fallback extraction logic for edge cases. By the end, it handled 92% automatically — the remaining 8% (28 returns) got flagged for manual processing.&lt;/p&gt;

&lt;p&gt;The second problem was trust. Rajesh didn't trust the script's output initially — and he shouldn't have. We ran a parallel test: his staff processed the first 50 returns manually while the script processed the same 50. We compared outputs line by line. The script matched human output on 47 returns and was actually &lt;em&gt;more accurate&lt;/em&gt; on the other 3 (the staff had made small transcription errors).&lt;/p&gt;

&lt;p&gt;That parallel test took an extra day but bought something no amount of demos could: Rajesh's confidence that the numbers were right.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Jab tak maine apni aankhon se comparison nahi dekha, mujhe yakeen nahi aaya. But the numbers don't lie." (Until I saw the comparison with my own eyes, I didn't believe it. But the numbers don't lie.)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why This Matters Beyond One CA Firm
&lt;/h2&gt;

&lt;p&gt;India has over 3.5 lakh practising Chartered Accountants. The vast majority of small and mid-size firms still process ITRs the way Rajesh used to — manually, with seasonal hires, under deadline pressure that leads to errors and late nights.&lt;/p&gt;

&lt;p&gt;The automation here isn't complicated. It's not an AI model that needs training data or a cloud platform that costs ₹50,000/month. It's a Python script using free libraries, running on the same laptop the CA already owns. The total infrastructure cost is zero.&lt;/p&gt;

&lt;p&gt;What it requires is someone who understands both the accounting workflow and the code — or a CA willing to work with someone who does. That intersection is where the real value lives. Not in the technology itself, but in knowing which 80% of the process is pure data movement and which 20% genuinely needs a Chartered Accountant's brain.&lt;/p&gt;

&lt;p&gt;If you're a CA firm owner reading this: you don't need to learn Python. You need to find someone who can spend a weekend understanding your Form 16 processing workflow and write a script tailored to it. The ROI pays for itself before August.&lt;/p&gt;

&lt;p&gt;And if you're a developer looking for freelance projects: ITR season starts in July. CA firms start panicking in June. That's your window. The ones who've done it manually for 20 years are the ones most ready to try something different — they just need someone to show them it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can a Python script really automate ITR filing for a CA firm?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. A Python script can automate the repetitive data extraction, validation, and pre-filling stages of ITR preparation. The CA still reviews and signs off on every return — the script handles the 80% of work that is pure data movement, not professional judgment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much time can automation save during ITR season?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In this case, a single script saved 209 hours across one ITR season by automating PDF extraction, data validation, and form pre-filling. The exact savings depend on your firm's volume and how manual your current process is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is it legal for CA firms to use automation scripts for ITR filing?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Absolutely. The automation handles data processing — extracting numbers from Form 16s, validating PAN details, and pre-filling fields. The Chartered Accountant still reviews every return, applies professional judgment, and digitally signs the final filing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What does it cost to build an ITR automation script?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The script in this case study was built in a single weekend using free, open-source Python libraries. Total running cost: ₹0. No SaaS subscriptions, no API fees, no licensing.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write about automation and the systems I actually run at &lt;a href="https://architmittal.com/blog/weekend-python-script-ca-firm-209-hours-itr?src=devto" rel="noopener noreferrer"&gt;architmittal.com&lt;/a&gt;. Originally published there.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>automation</category>
      <category>finance</category>
      <category>tax</category>
    </item>
    <item>
      <title>What is MCP Protocol? The USB Port for AI Agents</title>
      <dc:creator>Archit Mittal</dc:creator>
      <pubDate>Sat, 29 Aug 2026 02:45:06 +0000</pubDate>
      <link>https://dev.to/automate-archit/what-is-mcp-protocol-the-usb-port-for-ai-agents-3ib0</link>
      <guid>https://dev.to/automate-archit/what-is-mcp-protocol-the-usb-port-for-ai-agents-3ib0</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; MCP (Model Context Protocol) is a universal standard that lets AI agents connect to external tools, databases, and APIs — like USB for AI. Build one MCP server, connect it to any compatible AI.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Problem MCP Solves
&lt;/h2&gt;

&lt;p&gt;Imagine you have a brilliant assistant who can write code, analyze data, and answer complex questions — but they cannot access the internet, read your files, or check your database. That is what most AI models are like today. They are powerful thinkers trapped in a box with no hands.&lt;/p&gt;

&lt;p&gt;MCP — Model Context Protocol — gives AI models hands.&lt;/p&gt;

&lt;p&gt;It is a standardized way for AI agents to connect to external tools, databases, APIs, and services. Think of it like USB for AI. Before USB, every device needed its own proprietary connector. MCP does for AI what USB did for hardware: one standard protocol that connects everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The USB Analogy
&lt;/h2&gt;

&lt;p&gt;Before USB existed in the 1990s, connecting a printer required a parallel port cable. A mouse needed a PS/2 connector. A modem used a serial port. Every device, every cable, every driver was different. It was chaos.&lt;/p&gt;

&lt;p&gt;USB fixed this by creating one universal port that anything could plug into. Printers, keyboards, phones, cameras — one connector, one protocol, everything works.&lt;/p&gt;

&lt;p&gt;MCP does the same thing for AI agents:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before MCP&lt;/th&gt;
&lt;th&gt;After MCP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Custom code for each AI + tool integration&lt;/td&gt;
&lt;td&gt;One standard protocol&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Every AI vendor builds their own plugin system&lt;/td&gt;
&lt;td&gt;Universal tool interface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tools locked to specific AI platforms&lt;/td&gt;
&lt;td&gt;Tools work with any MCP-compatible AI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Developers rebuild integrations for each AI&lt;/td&gt;
&lt;td&gt;Build once, connect everywhere&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How MCP Actually Works
&lt;/h2&gt;

&lt;p&gt;MCP uses a client-server architecture:&lt;/p&gt;

&lt;h3&gt;
  
  
  MCP Servers (The Tools)
&lt;/h3&gt;

&lt;p&gt;An MCP server is a lightweight program that exposes specific capabilities. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;filesystem MCP server&lt;/strong&gt; lets AI agents read and write files&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;database MCP server&lt;/strong&gt; lets agents run SQL queries&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Slack MCP server&lt;/strong&gt; lets agents send messages&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;GitHub MCP server&lt;/strong&gt; lets agents create issues and pull requests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each server declares what it can do using a standardized format. It tells the AI: "I can read files, write files, and list directories. Here are the parameters I need."&lt;/p&gt;

&lt;h3&gt;
  
  
  MCP Clients (The AI)
&lt;/h3&gt;

&lt;p&gt;The AI agent (like Claude Code) acts as an MCP client. It discovers available servers, understands their capabilities, and calls them when needed. The key insight is that the AI decides which tools to use based on the task at hand.&lt;/p&gt;

&lt;p&gt;When you ask Claude Code to "read the config file and fix the bug," it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Checks its available MCP tools&lt;/li&gt;
&lt;li&gt;Finds the filesystem server&lt;/li&gt;
&lt;li&gt;Calls the "read file" tool with the config path&lt;/li&gt;
&lt;li&gt;Analyzes the content&lt;/li&gt;
&lt;li&gt;Calls the "write file" tool with the fix&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All through MCP. No custom code. No platform-specific integration.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Communication Flow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User: "Check our database for users who signed up last week"
  ↓
AI Agent (Claude): "I need to query a database. Let me use the database MCP server."
  ↓
MCP Protocol: Standardized request to database server
  ↓
Database MCP Server: Runs the query, returns results
  ↓
AI Agent: "Here are the 47 users who signed up last week..."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The beauty is that the AI agent does not know or care about the database implementation details. PostgreSQL, MySQL, MongoDB — it does not matter. The MCP server handles the specifics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why MCP Matters for Developers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Build Tools Once
&lt;/h3&gt;

&lt;p&gt;Before MCP, if you wanted your AI to interact with your company's internal API, you had to build a custom integration for each AI platform. ChatGPT plugins, Claude tools, Gemini extensions — each with their own format, their own requirements, their own deployment.&lt;/p&gt;

&lt;p&gt;With MCP, you build one server. It works with any MCP-compatible AI. Today that means Claude, but the protocol is open and other platforms are adopting it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Composability
&lt;/h3&gt;

&lt;p&gt;MCP servers can be mixed and matched. Need an AI that can read your database, check your Git history, and post to Slack? Connect three MCP servers. No custom glue code.&lt;/p&gt;

&lt;p&gt;This composability is incredibly powerful. I have built setups for clients where a single AI agent connects to 5-6 MCP servers and can handle complex, multi-step tasks that would have required custom development before.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Security and Control
&lt;/h3&gt;

&lt;p&gt;MCP servers run on your infrastructure. Your data does not leave your system — the AI requests specific information through the protocol, and only the results travel back. This is fundamentally more secure than giving an AI broad access to your systems.&lt;/p&gt;

&lt;p&gt;You can also control exactly what capabilities each server exposes. A read-only database server for analysis. A file server limited to specific directories. Fine-grained access control built into the protocol.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World MCP Use Cases
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Automated Code Review
&lt;/h3&gt;

&lt;p&gt;I set up a Claude Code environment with MCP servers for Git, filesystem, and a linting API. The AI agent can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pull the latest changes from a branch&lt;/li&gt;
&lt;li&gt;Read the modified files&lt;/li&gt;
&lt;li&gt;Run the linter&lt;/li&gt;
&lt;li&gt;Analyze the code changes&lt;/li&gt;
&lt;li&gt;Write review comments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All automated, all through MCP. What used to require a custom CI pipeline is now a single AI agent with the right tools connected.&lt;/p&gt;

&lt;h3&gt;
  
  
  Customer Support Intelligence
&lt;/h3&gt;

&lt;p&gt;For a client's support team, I connected MCP servers for their ticketing system (Freshdesk), knowledge base, and CRM. The AI agent can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read incoming support tickets&lt;/li&gt;
&lt;li&gt;Search the knowledge base for relevant articles&lt;/li&gt;
&lt;li&gt;Look up the customer's history in the CRM&lt;/li&gt;
&lt;li&gt;Draft a response incorporating all context&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The support team reviews and sends the response, but the research and drafting — the time-consuming part — is handled by the AI through MCP connections.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-Agent Workflows
&lt;/h3&gt;

&lt;p&gt;MCP enables multi-agent architectures where different AI agents handle different parts of a workflow. One agent researches, another writes, a third reviews — all sharing context through MCP servers.&lt;/p&gt;

&lt;p&gt;I have built content pipelines where:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Agent 1 connects to a research MCP server (web search + document analysis)&lt;/li&gt;
&lt;li&gt;Agent 2 reads Agent 1's findings and writes content via a writing MCP server&lt;/li&gt;
&lt;li&gt;Agent 3 reviews the content against brand guidelines using a custom rules MCP server&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each agent is specialized, and MCP provides the connective tissue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started with MCP
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Using MCP with Claude Code
&lt;/h3&gt;

&lt;p&gt;If you use Claude Code, MCP is already part of your workflow. Claude Code comes with built-in MCP servers for filesystem access, and you can add more through configuration.&lt;/p&gt;

&lt;p&gt;To add a custom MCP server, you add it to your Claude Code configuration:&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&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="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"my-database"&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="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&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="s2"&gt;"path/to/database-server.js"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&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="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"DATABASE_URL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"postgresql://..."&lt;/span&gt;&lt;span class="w"&gt;
      &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="w"&gt;
  &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="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Building Your Own MCP Server
&lt;/h3&gt;

&lt;p&gt;The MCP SDK makes it straightforward to build custom servers. A basic server in TypeScript looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Server&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@modelcontextprotocol/sdk/server/index.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-custom-server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.0.0&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="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setRequestHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tools/list&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get_weather&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Get weather for a city&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;City name&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="na"&gt;required&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;city&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;span class="p"&gt;}))&lt;/span&gt;

&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setRequestHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tools/call&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get_weather&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;city&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;city&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;weather&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchWeather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;city&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;weather&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="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;The protocol handles discovery, capability negotiation, and communication. You focus on the business logic.&lt;/p&gt;

&lt;p&gt;MCP is the backbone of modern AI automation. See it in action in my &lt;a href="https://dev.to/blog/claude-code-honest-developer-review"&gt;Claude Code review&lt;/a&gt; where I use MCP-connected tools daily. For the business case, read how these tools helped me &lt;a href="https://dev.to/blog/how-i-saved-client-85k-on-ai-api-costs"&gt;save a client ₹85K/month&lt;/a&gt;. And for the automation layer, check my &lt;a href="https://dev.to/blog/n8n-vs-zapier-real-cost-comparison"&gt;n8n vs Zapier comparison&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Future of MCP
&lt;/h2&gt;

&lt;p&gt;MCP is still early, but the trajectory is clear. As more AI platforms adopt the protocol, we will see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A marketplace of pre-built MCP servers for every popular service&lt;/li&gt;
&lt;li&gt;Enterprise adoption for secure AI integration with internal systems&lt;/li&gt;
&lt;li&gt;Complex multi-agent workflows becoming accessible to non-developers&lt;/li&gt;
&lt;li&gt;AI agents that can genuinely interact with the real world through standardized tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are building AI-powered products or integrating AI into your business, learning MCP now puts you ahead. It is the infrastructure layer that makes AI agents actually useful in production.&lt;/p&gt;

&lt;p&gt;I build custom MCP servers for clients who need AI agents integrated with their existing systems. If you want to explore how MCP can automate your workflows, &lt;a href="https://dev.to/contact"&gt;book a consultation&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write about automation and the systems I actually run at &lt;a href="https://architmittal.com/blog/what-is-mcp-protocol-usb-for-ai-agents?src=devto" rel="noopener noreferrer"&gt;architmittal.com&lt;/a&gt;. Originally published there.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>aiagents</category>
      <category>claude</category>
      <category>protocol</category>
    </item>
    <item>
      <title>Your customers are on WhatsApp. Your automation is built for Slack.</title>
      <dc:creator>Archit Mittal</dc:creator>
      <pubDate>Fri, 28 Aug 2026 12:45:06 +0000</pubDate>
      <link>https://dev.to/automate-archit/your-customers-are-on-whatsapp-your-automation-is-built-for-slack-4la6</link>
      <guid>https://dev.to/automate-archit/your-customers-are-on-whatsapp-your-automation-is-built-for-slack-4la6</guid>
      <description>&lt;p&gt;Almost every automation tutorial you will read was written by someone whose customers send email. So the pattern is always the same: a form fills a spreadsheet, a spreadsheet posts to Slack, Slack pings a human, the human replies by email. It is a clean loop and it has nothing to do with how an Indian business actually receives work.&lt;/p&gt;

&lt;p&gt;Your enquiries arrive as a WhatsApp message at 9.40pm. Your dealer sends the month's order as a voice note. Your client sends the purchase register as eleven photographs of a ledger. Your delivery boy confirms drop-off by sending a picture of the signed challan to a group with fourteen people in it. If your automation lives in email and Slack, it is automating a channel your business does not use.&lt;/p&gt;

&lt;p&gt;The gap is not technical. The tools exist. The gap is that nobody localises the pattern, so owners either bolt on a chatbot that annoys people or give up and keep three staff members answering the same question all day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with what is actually happening on the number
&lt;/h2&gt;

&lt;p&gt;Before you buy anything, do the tally. Open your business WhatsApp and read the last two weeks of incoming messages. Put every single one into one of three buckets.&lt;/p&gt;

&lt;p&gt;Bucket one: questions with exactly one correct answer. Price list. Are you open. Where is my order. GST number. Do you deliver to Nashik. Share the catalogue.&lt;/p&gt;

&lt;p&gt;Bucket two: things that need a human to decide something. Discounts. Complaints. Anything where the customer is upset. Anything involving a first-time buyer above your usual ticket size.&lt;/p&gt;

&lt;p&gt;Bucket three: information you need to collect from them, repeatedly, in a fixed format. Documents, order quantities, addresses, payment proof.&lt;/p&gt;

&lt;p&gt;Bucket one and bucket three are where automation pays. Bucket two is where automation destroys trust faster than any amount of it saves. Most owners get this backwards because the demos they are shown are all about bucket two, since a bot handling a complaint looks more impressive in a video than a bot sending a catalogue.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually moves, and what it costs you
&lt;/h2&gt;

&lt;p&gt;Moving to the WhatsApp Business Platform is not the same as installing WhatsApp Business on a second phone. It is a different product with different rules, and those rules are where every first attempt breaks.&lt;/p&gt;

&lt;p&gt;The one that catches everyone: you cannot message a customer freely whenever you like. When a customer messages you, a service window opens and you can reply with anything you want for a limited period. Outside that window, you can only send a pre-approved template. Templates are categorised, roughly, as utility, authentication, or marketing, and they are approved by Meta, not by you. Marketing costs more than utility, and Meta decides which category your message falls into regardless of what you named it. A "your order is dispatched" message that ends with "also check out our new range" gets reclassified, and your costs move with it.&lt;/p&gt;

&lt;p&gt;The second one: a phone number can live in the consumer app or on the API, not both. The day you migrate your main business number, the app on your manager's phone stops working with it, and the chat history does not come along. Every business I have seen do this without planning for it spent the first week in quiet panic because the person who knew all the customers could no longer see any of them. Plan the shared inbox before you migrate, not after.&lt;/p&gt;

&lt;p&gt;The third one: quality rating. If people block or report your number, your ability to send messages gets restricted. This is the mechanism that punishes the "let us blast everyone in the database" instinct, and it punishes it at the account level, meaning the marketing blast can take down your order confirmations too.&lt;/p&gt;

&lt;h2&gt;
  
  
  The distributor order desk
&lt;/h2&gt;

&lt;p&gt;Here is the case I see most often, in some version, across building materials, pharma, FMCG, auto parts.&lt;/p&gt;

&lt;p&gt;A distributor takes orders from dealers on WhatsApp. Some send a typed list. Some send a voice note. Some send a photo of a handwritten page. One person at the distributor's office reads all of them, types them into Tally or the ERP, and sends back a confirmation. That person is also the person who knows which dealer is behind on payments and which one always over-orders in the last week of the month.&lt;/p&gt;

&lt;p&gt;The automation instinct is to build a bot that reads the order and pushes it into the ERP. That is the wrong first build, because the failure mode is expensive. A misread quantity ships wrong stock, and the dealer does not blame the system, they blame you.&lt;/p&gt;

&lt;p&gt;The build that works is smaller and duller. The system receives the message, transcribes the voice note or reads the photo, and produces a structured draft order which it sends straight back to the dealer as a formatted message: these items, these quantities, this total, reply YES to confirm or send corrections. The dealer confirms. Only a confirmed order goes to the ERP, and it goes with the dealer's own confirmation attached to it.&lt;/p&gt;

&lt;p&gt;You have not removed the human judgement. You have removed the typing, and you have moved the accuracy check to the only person who actually knows what they meant. When it goes wrong, and it will, the dealer catches it before stock moves. That is the difference between an automation you can leave running and one you have to babysit.&lt;/p&gt;

&lt;p&gt;The same shape works for a CA practice during ITR and GST season. The chase for documents is the single most draining part of the month, and it is bucket three: a fixed list, collected from many people, in a repeating cycle. A system that sends each client their outstanding list, accepts photos and PDFs back on WhatsApp, files them into the right client folder, and stops chasing the moment the list is complete will save more hours than anything clever you build for advisory work. Nobody needs an AI to interpret the documents. They need the chase to happen without a human remembering to do it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The DLT and consent bit nobody mentions
&lt;/h2&gt;

&lt;p&gt;Two mistakes, in opposite directions.&lt;/p&gt;

&lt;p&gt;The first is assuming DLT covers you. DLT registration, with its headers and approved content templates, exists under TRAI's rules for commercial communication sent over the telecom network. That is SMS and voice. WhatsApp does not travel over that pipe, so your DLT registration does not authorise anything on it, and its absence does not block you either. What governs your WhatsApp messaging is Meta's own business messaging policy and template approval process. If you have an SMS fallback for OTPs or delivery alerts, that leg still needs DLT. Two channels, two regimes, and vendors selling you one will happily let you assume it covers both.&lt;/p&gt;

&lt;p&gt;The second mistake is assuming that because DLT does not apply, nothing does. India's data protection law is on the books and its obligations are being phased in, and it does not care which channel you used. If you are holding customer phone numbers and messaging them, you need to be able to show why you have the number, what you told the person when you took it, and how they can tell you to stop. "It was in an old Excel sheet from an exhibition" is not consent, and a database like that is exactly what tanks your quality rating in week one.&lt;/p&gt;

&lt;p&gt;Practically, this means one boring thing you should build before the fun parts: a consent field on every contact record that stores when the number was captured, through which form or conversation, and the exact wording they saw. Then honour opt-outs at the system level, so that a person who says stop is stopped everywhere and not just in the campaign they replied to. This takes an afternoon. Retrofitting it after you have sent a hundred thousand messages does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the answer is don't
&lt;/h2&gt;

&lt;p&gt;Do not automate first contact for anything expensive or emotional. If someone is enquiring about a large order, a service failure, or money they have already paid, a templated reply reads as contempt. Route those to a person and let the automation do nothing more than make sure a person sees it fast.&lt;/p&gt;

&lt;p&gt;Do not automate collections messaging on WhatsApp. It is the fastest route to being blocked and reported by exactly the people you most need to stay in touch with, and being blocked by a debtor is worse than being ignored by one.&lt;/p&gt;

&lt;p&gt;Do not put an open-ended AI agent on your main number and let it answer whatever comes in. The variance is the problem. On the day it invents a delivery date or a discount, you own that commitment in front of a customer holding a screenshot.&lt;/p&gt;

&lt;p&gt;And do not automate a process your staff are still arguing about. If two people in your office would answer the same enquiry differently, you do not have an automation problem, you have an undecided policy. Automating it just makes one of them permanently wrong at scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do this one thing this week
&lt;/h2&gt;

&lt;p&gt;Do the tally described above, but write it down properly. Take the last two weeks of incoming WhatsApp messages on your main business number, and for every message, note the bucket and the answer that was given. Then find the single question that appeared most often in bucket one and count how many separate people typed a reply to it.&lt;/p&gt;

&lt;p&gt;That question, with that one answer, is your first automation. Not a chatbot. One template, one trigger, one message. Ship it, watch it for a fortnight, and only then look at the second one.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write about automation and the systems I actually run at &lt;a href="https://architmittal.com/blog/whatsapp-gap?src=devto" rel="noopener noreferrer"&gt;architmittal.com&lt;/a&gt;. Originally published there.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>whatsapp</category>
      <category>automation</category>
      <category>compliance</category>
    </item>
    <item>
      <title>Zapier vs Make vs n8n: your run volume decides, not the feature table</title>
      <dc:creator>Archit Mittal</dc:creator>
      <pubDate>Fri, 28 Aug 2026 02:45:07 +0000</pubDate>
      <link>https://dev.to/automate-archit/zapier-vs-make-vs-n8n-your-run-volume-decides-not-the-feature-table-ko7</link>
      <guid>https://dev.to/automate-archit/zapier-vs-make-vs-n8n-your-run-volume-decides-not-the-feature-table-ko7</guid>
      <description>&lt;p&gt;The comparison table you have been reading is the wrong document. Zapier, Make and n8n all connect the same apps, all handle webhooks, all do branching and retries. Where they differ, and where the money actually goes, is in what each one counts as a billable unit. Get that wrong and you will pay three times more than you needed to for the identical workflow, or worse, you will build something that works beautifully in testing and then eats a month's quota in a single busy weekend.&lt;/p&gt;

&lt;p&gt;So the only comparison worth making is rupee per 10,000 runs. Not per month, not per seat. Per 10,000 times the thing actually happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three tools bill for three different things
&lt;/h2&gt;

&lt;p&gt;This is the whole article, so read it slowly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zapier bills per task.&lt;/strong&gt; A task is an action step that executes. The trigger is not charged. A five-step Zap that fires once consumes roughly one task per action step that runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make bills per operation.&lt;/strong&gt; An operation is more or less any module call, and the trigger counts. So does each pass of an iterator or aggregator. A scenario that loops over a list of items bills once per item, not once per run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;n8n Cloud bills per workflow execution.&lt;/strong&gt; One run of the workflow is one execution, whether it contains four nodes or forty. Self-hosted n8n on your own server has no per-run meter at all. You pay for the machine and for whoever keeps it alive.&lt;/p&gt;

&lt;p&gt;Three meters, three completely different shapes. Which is why the per-unit price on the pricing page tells you almost nothing on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  The multiplier is the whole game
&lt;/h2&gt;

&lt;p&gt;Write it as arithmetic:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost per 10,000 runs = 10,000 × (billable units per run) × (price per unit)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every vendor advertises the third term. Nobody tells you the second, because the second depends entirely on how you built the workflow. And the second term varies far more between platforms than the third does. A job that is one execution on n8n can be five tasks on Zapier and twenty operations on Make. A cheaper unit price loses badly to a twenty-times multiplier.&lt;/p&gt;

&lt;p&gt;So before you compare plans, you have to count. Take one real workflow, draw it out, and count what each platform would charge for a single run of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A worked example: the distributor's WhatsApp enquiry
&lt;/h2&gt;

&lt;p&gt;Take a business that sells through dealers and takes enquiries on WhatsApp. The workflow everyone builds first:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Message arrives on the business number&lt;/li&gt;
&lt;li&gt;Check whether this number already exists in the customer sheet&lt;/li&gt;
&lt;li&gt;Create or update the row&lt;/li&gt;
&lt;li&gt;Push the lead into the CRM&lt;/li&gt;
&lt;li&gt;Ping the assigned salesperson&lt;/li&gt;
&lt;li&gt;Wait two days, and if nothing has moved, send one follow-up&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On a per-task meter, that is a handful of billed actions per enquiry. On a per-operation meter, the trigger is billed too, the lookup is billed, and if step two searches and returns several matching rows, the modules after it bill once per row. That last detail is where people get hurt. A search that returns four matches quietly quadruples the back half of the scenario. On a per-execution meter, all six steps are one unit.&lt;/p&gt;

&lt;p&gt;Now multiply by your actual enquiry volume, not your good-month volume. A distributor doing steady business through the year has one number. The same distributor during a festive push has another number entirely, and it is the second one your bill is calculated on.&lt;/p&gt;

&lt;p&gt;Here is the part that decides the platform: if you add a step, the per-task and per-operation bills go up and the per-execution bill does not. Automations do not stay at six steps. They grow. Somebody wants the enquiry logged in a second sheet, somebody wants a Telugu template for southern leads, somebody wants the follow-up to skip Sundays. Two years in, that workflow is not six steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  The spike problem, and why annual plans mislead
&lt;/h2&gt;

&lt;p&gt;Indian SMB volumes are not flat. They are lumpy in ways that are entirely predictable and almost never modelled.&lt;/p&gt;

&lt;p&gt;GST returns cluster around filing dates. Anything you automate around invoice reconciliation or vendor mismatch does most of its work in a few days each month, and then sits idle. ITR season does the same thing to anyone serving CAs or doing payroll support. Festive periods do it to retail and distribution. Quarter close does it to anyone selling B2B.&lt;/p&gt;

&lt;p&gt;Per-run billing does not care about your averages. If your reconciliation workflow processes a month of invoices in one overnight batch, that batch is thousands of billable units in a few hours. I have watched a perfectly sensible scenario chew through an entire monthly allowance over a weekend nobody was watching, and the failure mode is the ugly one: the automation stops mid-month, silently, and the team keeps assuming it is running.&lt;/p&gt;

&lt;p&gt;If your volume is spiky, look hard at what happens when you exceed the plan. Some platforms throttle, some auto-upgrade you, some just stop. Find out which before you build, not after.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where n8n stops being free
&lt;/h2&gt;

&lt;p&gt;Self-hosted n8n removes the per-run meter, and people hear that as "free". It is not free, it is a different bill.&lt;/p&gt;

&lt;p&gt;You need a server. A small VPS at typical Indian market rates is cheap enough that it disappears into your monthly costs. What does not disappear is the maintenance: version upgrades, queue mode once you outgrow a single process, database backups, SSL renewal, monitoring so you find out about failures from an alert rather than from a customer. Someone has to own that. If that someone is you, and you are the owner of a company doing crores in revenue, your hourly cost makes this the most expensive option on the list by a wide margin.&lt;/p&gt;

&lt;p&gt;The honest break-even test: self-hosting wins when the subscription you would otherwise pay clearly exceeds the server cost plus the hours the maintenance will actually take, and when there is a named person other than the founder who owns those hours. If you cannot name that person, use the cloud version and stop calculating.&lt;/p&gt;

&lt;p&gt;Also worth knowing before you build a business on it: n8n's source-available terms permit internal use freely, but restrict reselling it as a hosted service. If your plan is to run automations for your own company, this is irrelevant. If your plan is to run them for clients, read the terms properly first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the answer is don't automate this
&lt;/h2&gt;

&lt;p&gt;Nobody selling automation will tell you this, so here it is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not automate anything that runs fewer than a few times a week and takes under ten minutes.&lt;/strong&gt; The build takes hours, the maintenance never ends, and the payback is measured in decades. Write an SOP instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not automate a process nobody has agreed on.&lt;/strong&gt; If two people describe the lead follow-up process differently, you do not have an automation problem, you have a process problem, and encoding the disagreement into a workflow makes it permanent and invisible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not automate the final GST filing decision.&lt;/strong&gt; Automate the fetching, the matching, the flagging of mismatches, the reminder to the accountant. The submission itself should be a human clicking a button, because the cost of a wrong filing is not a wrong filing, it is a notice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not automate anything that sends money out.&lt;/strong&gt; Payment approvals, vendor payouts, refunds above a threshold. Automate the preparation, keep the approval human.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not automate around a broken data source.&lt;/strong&gt; If the customer sheet has duplicate rows, three spellings of every city, and phone numbers with and without country codes, an automation will run enthusiastically on all of it and give you a mess at machine speed. Clean the data first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The verdict
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Low volume, few steps, and you want it to work without you.&lt;/strong&gt; Zapier. You are buying the connector library and not thinking about it again. It is the expensive option and that is fine at low volume.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Medium volume, and someone in the business enjoys building.&lt;/strong&gt; Make. The per-unit price rewards you, but only if you stay disciplined about module count and never let an iterator run unbounded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High volume, or heavy looping, or workflows that keep growing steps.&lt;/strong&gt; n8n. Per-execution billing means complexity stops being taxed. Cloud first, self-hosted only when you have someone to own the server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your workflows loop over lists, the per-operation meter will punish you and the choice is close to made for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do this one thing this week
&lt;/h2&gt;

&lt;p&gt;Take your single highest-volume automation, the one that fires most often. Count two things: how many times it ran last month, and how many billable units one run consumes on the platform you are on. Multiply. Then price the same run count on the other two, using each vendor's current published rate rather than any figure you read in an article, including this one, because all three change their pricing and a stale number is worse than none.&lt;/p&gt;

&lt;p&gt;One workflow, one spreadsheet, half an hour. That number will tell you more than every feature comparison you have read this year.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write about automation and the systems I actually run at &lt;a href="https://architmittal.com/blog/zapier-make-n8n?src=devto" rel="noopener noreferrer"&gt;architmittal.com&lt;/a&gt;. Originally published there.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>tooling</category>
      <category>indiansmb</category>
    </item>
    <item>
      <title>The ₹0 Automation Stack: Enterprise-Grade Workflows Without Paying for SaaS</title>
      <dc:creator>Archit Mittal</dc:creator>
      <pubDate>Thu, 27 Aug 2026 18:27:58 +0000</pubDate>
      <link>https://dev.to/automate-archit/the-0-automation-stack-enterprise-grade-workflows-without-paying-for-saas-2645</link>
      <guid>https://dev.to/automate-archit/the-0-automation-stack-enterprise-grade-workflows-without-paying-for-saas-2645</guid>
      <description>&lt;p&gt;₹37,500 per month. That was the SaaS bill a Jaipur-based textile exporter was paying for automating invoices, GST reconciliation, and shipping notifications across three platforms. Three dashboards, three logins, three support tickets every time something broke.&lt;/p&gt;

&lt;p&gt;I replaced all of it with Python scripts running on a ₹500/month VPS. The recurring cost dropped to effectively zero — and the workflows actually became more reliable.&lt;/p&gt;

&lt;p&gt;This isn't a hypothetical framework. This is the exact stack I've deployed across 11 Indian businesses over the past 18 months, from CA firms filing ITR returns to stock traders screening 150+ equities before market open. Every tool in this stack is free. Every workflow runs in production today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Indian Businesses Overpay for Automation
&lt;/h2&gt;

&lt;p&gt;Most business owners discover automation through SaaS marketing. The pitch is compelling: drag-and-drop workflows, no coding required, instant results. What the pricing page doesn't tell you is that the "Starter" plan handles 100 tasks per month, and your GST reconciliation alone burns through that in three days.&lt;/p&gt;

&lt;p&gt;The real cost isn't the subscription — it's the upgrade treadmill. You start at ₹2,000/month, hit the task limit by week two, upgrade to ₹8,000/month, then discover that the webhook integration you need is locked behind the "Business" tier at ₹25,000/month.&lt;/p&gt;

&lt;p&gt;For businesses processing under 10,000 transactions monthly — which includes the vast majority of Indian SMBs, freelancers, and professional firms — a Python-based stack isn't just cheaper. It's more flexible, more transparent, and entirely under your control.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack: Seven Layers, Zero Recurring Cost
&lt;/h2&gt;

&lt;p&gt;Here's every component of the automation stack I deploy for clients. Each layer is free, battle-tested, and replaceable without rebuilding the entire system.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Layer 1 — Logic &amp;amp; Scripting: Python 3.11+&lt;/strong&gt;&lt;br&gt;
The backbone of every automation. Handles API calls, data transformation, conditional logic, and error handling. Free forever, runs anywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2 — Data Processing: Pandas + OpenPyXL&lt;/strong&gt;&lt;br&gt;
Reads Excel sheets, CSVs, and bank statements. Cleans, matches, and transforms data. This single library replaces most of what businesses pay ₹10,000+/month for in data integration tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 3 — Database: SQLite (small) or PostgreSQL (scale)&lt;/strong&gt;&lt;br&gt;
SQLite ships with Python — zero setup. For anything above 50,000 records or multi-user access, PostgreSQL (free, open-source) handles it. Supabase offers free-tier hosted PostgreSQL if you don't want to manage a database server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 4 — Scheduling: cron + Python schedule library&lt;/strong&gt;&lt;br&gt;
cron handles time-based triggers on Linux. The Python &lt;code&gt;schedule&lt;/code&gt; library handles interval-based logic within scripts. Together they replace paid workflow schedulers entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 5 — Notifications: Telegram Bot API + Gmail SMTP&lt;/strong&gt;&lt;br&gt;
Telegram bots are free, instant, and support rich formatting (tables, buttons, file attachments). Gmail SMTP handles email alerts up to 500/day. WhatsApp Business API is free under 1,000 messages/month.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 6 — Web Scraping &amp;amp; Browser Automation: Selenium + BeautifulSoup&lt;/strong&gt;&lt;br&gt;
For workflows that need to interact with websites that don't offer APIs — government portals, legacy ERP dashboards, bank statement downloads. Free, mature, well-documented.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 7 — Hosting: VPS (₹500/month) or Home Server (₹0)&lt;/strong&gt;&lt;br&gt;
A basic VPS from providers like Hetzner or DigitalOcean runs the entire stack. For truly zero cost, an old laptop running Ubuntu works — I have a client whose stock screener runs on a repurposed 2018 ThinkPad.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Real Numbers: SaaS vs. ₹0 Stack
&lt;/h2&gt;

&lt;p&gt;Here's the cost comparison from three actual client deployments. These aren't projections — these are real invoices I've seen versus the cost of what replaced them.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workflow&lt;/th&gt;
&lt;th&gt;SaaS Cost (Monthly)&lt;/th&gt;
&lt;th&gt;₹0 Stack Cost (Monthly)&lt;/th&gt;
&lt;th&gt;Annual Savings&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Invoice processing + GST filing&lt;/td&gt;
&lt;td&gt;₹18,000&lt;/td&gt;
&lt;td&gt;₹0&lt;/td&gt;
&lt;td&gt;₹2,16,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bank reconciliation (3 accounts)&lt;/td&gt;
&lt;td&gt;₹12,000&lt;/td&gt;
&lt;td&gt;₹0&lt;/td&gt;
&lt;td&gt;₹1,44,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stock screening + WhatsApp alerts&lt;/td&gt;
&lt;td&gt;₹47,000 (advisory fee)&lt;/td&gt;
&lt;td&gt;₹0&lt;/td&gt;
&lt;td&gt;₹5,64,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daily MIS report generation&lt;/td&gt;
&lt;td&gt;₹8,500&lt;/td&gt;
&lt;td&gt;₹0&lt;/td&gt;
&lt;td&gt;₹1,02,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Total annual savings across these four workflows: ₹10,26,000.&lt;/strong&gt; The one-time setup cost for all four was ₹1,45,000. Payback period: under 2 months.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Trade-Off Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;I'd be dishonest if I said this stack has no downsides. It does. The primary trade-off is setup time and maintenance ownership.&lt;/p&gt;

&lt;p&gt;A SaaS tool gives you a working automation in 20 minutes. A Python script takes 2-8 hours to build, test, and deploy. If something breaks at 2 AM, there's no support chat — you debug it yourself, or your automation consultant does.&lt;/p&gt;

&lt;p&gt;This matters. For a solopreneur who needs a quick Slack-to-email bridge, paying ₹2,000/month for a SaaS tool makes perfect sense. The ₹0 stack wins when you have workflows that are core to your business operations, when the SaaS bill has crossed ₹10,000/month, or when you need customisation that no drag-and-drop builder can handle.&lt;/p&gt;

&lt;p&gt;I wrote about knowing when &lt;em&gt;not&lt;/em&gt; to automate in my piece on &lt;a href="https://dev.to/blog/weekend-python-script-ca-firm-209-hours-itr"&gt;how a CA firm automated ITR processing&lt;/a&gt; — the lesson applies here too. Not every workflow deserves a custom script. But the ones that do? They deserve better than a ₹25,000/month subscription with a 500-task ceiling.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Start: The First Workflow to Automate
&lt;/h2&gt;

&lt;p&gt;If you've never built a ₹0 automation before, start with the workflow that causes the most daily friction. For most Indian businesses, that's one of these three: bank statement download and matching, invoice data extraction from PDFs, or daily/weekly report generation from spreadsheets.&lt;/p&gt;

&lt;p&gt;Pick one. Build a Python script that handles the happy path — the 80% case where everything works normally. Add error handling and a Telegram notification for failures. Run it manually for a week to build trust. Then schedule it with cron and forget about it.&lt;/p&gt;

&lt;p&gt;That single workflow will teach you more about automation than any SaaS trial. And once it's running, it runs forever — no subscription renewal, no "your plan has been downgraded," no surprise pricing changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Reliability Question
&lt;/h3&gt;

&lt;p&gt;Skeptics ask: "Is a free stack reliable enough for production?" Fair question. My answer: a CA firm has been running Python-based ITR automation through two consecutive filing seasons — over 1,200 returns processed, zero missed deadlines. The scripts have error handling, retry logic, and instant Telegram alerts if anything fails. That's more observability than most SaaS dashboards provide.&lt;/p&gt;

&lt;p&gt;Reliability isn't about how much you pay. It's about how well you handle failures. A ₹0 script with proper logging and alerting beats a ₹20,000/month SaaS tool where failures disappear into a "contact support" black hole.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;In upcoming articles, I'll walk through specific implementations: automated bank reconciliation with Python and Pandas, stock screening bots that replace expensive advisory services, and GST reconciliation pipelines that file-ready returns without manual intervention.&lt;/p&gt;

&lt;p&gt;Every implementation will follow the same philosophy: zero recurring cost, production-grade reliability, and real rupee numbers from real Indian businesses.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write about automation and the systems I actually run at &lt;a href="https://architmittal.com/blog/zero-rupee-automation-stack-enterprise-workflows?src=devto" rel="noopener noreferrer"&gt;architmittal.com&lt;/a&gt;. Originally published there.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>python</category>
      <category>opensource</category>
      <category>costoptimization</category>
    </item>
    <item>
      <title>How I Cut a Client's AI API Bill from Rs 85,000 to Rs 12,000 a Month</title>
      <dc:creator>Archit Mittal</dc:creator>
      <pubDate>Thu, 27 Aug 2026 18:15:08 +0000</pubDate>
      <link>https://dev.to/automate-archit/how-i-cut-a-clients-ai-api-bill-from-rs-85000-to-rs-12000-a-month-36k8</link>
      <guid>https://dev.to/automate-archit/how-i-cut-a-clients-ai-api-bill-from-rs-85000-to-rs-12000-a-month-36k8</guid>
      <description>&lt;p&gt;₹85,000 per month. That was the AI API bill sitting in my client's inbox when they called me in a mild panic last quarter. They run a mid-sized e-commerce operation in Pune — about 4,000 orders a day — and had integrated AI into customer support, product descriptions, and internal reporting. The AI was working beautifully. The invoice was not.&lt;/p&gt;

&lt;p&gt;Three weeks later, their monthly bill was ₹12,400. Same tasks. Same quality. No corners cut. Here's exactly what changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real problem: every task was using the most expensive model
&lt;/h2&gt;

&lt;p&gt;When I audited their setup, the issue was obvious within five minutes. Every single API call — whether it was classifying a customer complaint into one of 8 categories or generating a 2,000-word product description — was hitting the same premium model. It's the most common mistake I see with businesses adopting AI: they pick one model during the proof-of-concept phase and never revisit that decision as they scale.&lt;/p&gt;

&lt;p&gt;You wouldn't hire a senior chartered accountant to do data entry. But that's essentially what was happening — a top-tier reasoning model answering "Is this complaint about shipping or billing?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 1: Model routing — the single biggest cost lever
&lt;/h2&gt;

&lt;p&gt;Model routing means sending each task to the cheapest model that can handle it at acceptable quality. I categorised their ~47 distinct API call types into three tiers. 68% of calls moved to the lightweight tier, 20% to mid-tier, only 12% stayed on premium. That single change dropped the bill from ₹85K to roughly ₹38K — no quality loss, verified with two weeks of A/B testing on customer satisfaction scores before switching fully.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 2: Prompt caching — stop paying for the same context twice
&lt;/h2&gt;

&lt;p&gt;Their support bot sent the same 1,200-token system prompt with every call — policies, tone, catalogue context, all identical across thousands of daily calls. Caching processes it once and references it cheaply on subsequent calls within the window. At ~6,000 support interactions a day, this alone saved ₹8,000-10,000 monthly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 3: Batching non-urgent requests
&lt;/h2&gt;

&lt;p&gt;Not everything needs a real-time response. Their internal reporting made individual calls as each data point arrived — sixty to eighty calls that could be batched into three or four. We collect through the day and process in windows at 6 AM, 2 PM, 10 PM. Batch pricing is typically half of real-time, and a few hours of delay is fine for internal reports.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 4: Output token discipline
&lt;/h2&gt;

&lt;p&gt;Their product prompts asked for "a detailed, comprehensive description" and got 800-1,000 tokens when 200-300 was the real need. We rewrote them with explicit length and structure: "Write it in exactly 3 sentences. What it is. Key benefit. Who it's for." Output tokens cost more than input; cutting length 60% across thousands of daily calls compounds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The final numbers
&lt;/h2&gt;

&lt;p&gt;An 85% reduction, ₹85,000 to ₹12,400, same work. Customer satisfaction actually rose 3% — the lighter models respond faster, and people prefer a quick reply to a marginally more eloquent one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What most people get wrong about AI costs
&lt;/h2&gt;

&lt;p&gt;The instinct is to shop for a cheaper provider. The real leverage is architectural. I've seen businesses switch providers three times and still overpay because the pattern — one model for everything, no caching, verbose outputs — never changes.&lt;/p&gt;

&lt;p&gt;If your bill is higher than you'd like, ask: how many calls actually need a premium model? Are you sending the same context repeatedly? Can any calls be batched? Are your prompts asking for more output than you use? The answers usually reveal that 60-80% of the bill is waste hiding in plain sight. You don't need to spend less on AI. You need to spend smarter.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write about automation and the systems I actually run at architmittal.com. Originally published there.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>costoptimization</category>
      <category>llm</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Build an Offline GSTIN Validator in 62 Lines of Python</title>
      <dc:creator>Archit Mittal</dc:creator>
      <pubDate>Mon, 17 Aug 2026 12:59:29 +0000</pubDate>
      <link>https://dev.to/automate-archit/build-an-offline-gstin-validator-in-62-lines-of-python-7df</link>
      <guid>https://dev.to/automate-archit/build-an-offline-gstin-validator-in-62-lines-of-python-7df</guid>
      <description>&lt;p&gt;Every vendor master I have ever been handed has bad GSTINs in it. Not malicious — just typos. A digit swapped during copy-paste, a trailing space, an &lt;code&gt;O&lt;/code&gt; where a &lt;code&gt;0&lt;/code&gt; should be, a number carried over from an old registration that was surrendered two years ago.&lt;/p&gt;

&lt;p&gt;You find out at the worst possible time: GSTR-1 filing day, when the portal rejects the upload and you are staring at 400 rows with no idea which ones are broken.&lt;/p&gt;

&lt;p&gt;Here is the thing most people do not know: &lt;strong&gt;a GSTIN carries its own checksum.&lt;/strong&gt; You do not need an API, a subscription, or an internet connection to catch the majority of bad ones. The 15th character is a mathematical function of the first 14. If it does not match, the number is wrong — full stop.&lt;/p&gt;

&lt;p&gt;This post builds a validator that checks that checksum and decodes the rest of the number, in 62 lines of Python. No dependencies outside the standard library.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a GSTIN actually encodes
&lt;/h2&gt;

&lt;p&gt;A GSTIN is 15 characters, and every slice means something:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2 7 A A P F U 0 9 3 9 F 1 Z V
└─┘ └───────────────────┘ │ │ │
 │           │            │ │ └─ checksum
 │           │            │ └─── always Z (reserved)
 │           │            └───── registration count for this PAN in this state
 │           └────────────────── the entity's 10-character PAN
 └────────────────────────────── state code (27 = Maharashtra)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two useful consequences fall out of this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The PAN is sitting right there in characters 3–12. You can cross-check it against your TDS records without asking the vendor for anything.&lt;/li&gt;
&lt;li&gt;The 4th character of that PAN tells you the entity type — &lt;code&gt;C&lt;/code&gt; for company, &lt;code&gt;P&lt;/code&gt; for proprietor, &lt;code&gt;F&lt;/code&gt; for partnership or LLP, &lt;code&gt;H&lt;/code&gt; for HUF, &lt;code&gt;T&lt;/code&gt; for trust. Handy when you are deciding whether a vendor should have been deducted TDS at all.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The checksum algorithm
&lt;/h2&gt;

&lt;p&gt;This is the part worth understanding, because it is the bit that catches typos.&lt;/p&gt;

&lt;p&gt;Map every character to a value: &lt;code&gt;0-9&lt;/code&gt; → 0–9, &lt;code&gt;A-Z&lt;/code&gt; → 10–35. That is a base-36 alphabet. Then, for each of the first 14 characters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiply its value by 1 if it is in an odd position, 2 if it is in an even position.&lt;/li&gt;
&lt;li&gt;Add the quotient and remainder of that product divided by 36.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sum all 14 contributions. The checksum character is &lt;code&gt;(36 − sum mod 36) mod 36&lt;/code&gt;, mapped back through the same alphabet.&lt;/p&gt;

&lt;p&gt;If that sounds like the Luhn algorithm on your credit card, it is the same family of idea — a weighted modular sum that catches single-character errors and most transpositions.&lt;/p&gt;

&lt;p&gt;Nine lines of Python:&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;CHARSET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ&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;checksum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first14&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first14&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;prod&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CHARSET&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ch&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="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;else&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;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;36&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;36&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;CHARSET&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sanity check it against a GSTIN you know is real before you trust it on 400 rows. &lt;code&gt;27AAPFU0939F1ZV&lt;/code&gt; should return &lt;code&gt;V&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full tool
&lt;/h2&gt;

&lt;p&gt;Reads a CSV with a &lt;code&gt;gstin&lt;/code&gt; column, writes the same CSV back with &lt;code&gt;valid&lt;/code&gt;, &lt;code&gt;error&lt;/code&gt;, &lt;code&gt;state&lt;/code&gt;, &lt;code&gt;pan&lt;/code&gt;, &lt;code&gt;entity_type&lt;/code&gt;, and &lt;code&gt;registration_no&lt;/code&gt; columns appended.&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="n"&gt;CHARSET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;GSTIN_RE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][0-9A-Z][Z][0-9A-Z]$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;STATES&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;01&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;Jammu &amp;amp; Kashmir&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;02&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;Himachal Pradesh&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;03&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;Punjab&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;04&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;Chandigarh&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;05&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;Uttarakhand&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;06&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;Haryana&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;07&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;Delhi&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;08&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;Rajasthan&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;09&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;Uttar Pradesh&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;10&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;Bihar&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;11&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;Sikkim&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;12&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;Arunachal Pradesh&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;13&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;Nagaland&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;14&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;Manipur&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;15&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;Mizoram&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;16&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;Tripura&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;17&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;Meghalaya&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;18&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;Assam&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;19&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;West Bengal&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;20&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;Jharkhand&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;21&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;Odisha&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;22&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;Chhattisgarh&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;23&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;Madhya Pradesh&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;24&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;Gujarat&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;26&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;Dadra &amp;amp; Nagar Haveli and Daman &amp;amp; Diu&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;27&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;Maharashtra&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;29&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;Karnataka&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;30&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;Goa&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;31&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;Lakshadweep&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;32&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;Kerala&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;33&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;Tamil Nadu&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;34&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;Puducherry&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;35&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;Andaman &amp;amp; Nicobar&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;36&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;Telangana&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;37&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;Andhra Pradesh&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;38&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;Ladakh&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;PAN_ENTITY&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;C&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;Company&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;P&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;Individual / Proprietor&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;H&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;HUF&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;F&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;Partnership Firm / LLP&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;A&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;AOP&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;T&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;Trust&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;B&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;Body of Individuals&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;L&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;Local Authority&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;J&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;Artificial Juridical Person&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;G&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;Government&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;checksum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first14&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first14&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;prod&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CHARSET&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ch&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="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;else&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;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;36&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;36&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;CHARSET&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &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="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gstin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;length &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, expected 15&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="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;GSTIN_RE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gstin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&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;structure does not match GSTIN pattern&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;g&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;STATES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gstin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown state code &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&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="nf"&gt;checksum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gstin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;checksum mismatch (expected &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;checksum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&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="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gstin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valid&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&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="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;STATES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;entity_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PAN_ENTITY&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;g&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unknown&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;registration_no&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outp&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DictReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&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;gstin&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="p"&gt;))})&lt;/span&gt;
    &lt;span class="n"&gt;fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromkeys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DictWriter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fieldnames&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeheader&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writerows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;bad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; checked | &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; invalid | wrote &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;outp&lt;/span&gt;&lt;span class="si"&gt;}&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;__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;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&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;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Running it
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;vendors.csv&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="k"&gt;vendor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;gstin&lt;/span&gt;
&lt;span class="k"&gt;Acme&lt;/span&gt; &lt;span class="k"&gt;Traders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;27&lt;/span&gt;&lt;span class="k"&gt;AAPFU&lt;/span&gt;&lt;span class="mf"&gt;0939&lt;/span&gt;&lt;span class="k"&gt;F&lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="k"&gt;ZV&lt;/span&gt;
&lt;span class="k"&gt;Bharat&lt;/span&gt; &lt;span class="k"&gt;Supplies&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;29&lt;/span&gt;&lt;span class="k"&gt;AAGCB&lt;/span&gt;&lt;span class="mf"&gt;7383&lt;/span&gt;&lt;span class="k"&gt;J&lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="k"&gt;Z&lt;/span&gt;&lt;span class="mf"&gt;4&lt;/span&gt;
&lt;span class="k"&gt;Typo&lt;/span&gt; &lt;span class="k"&gt;Enterprises&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;27&lt;/span&gt;&lt;span class="k"&gt;AAPFU&lt;/span&gt;&lt;span class="mf"&gt;0939&lt;/span&gt;&lt;span class="k"&gt;F&lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="k"&gt;ZX&lt;/span&gt;
&lt;span class="k"&gt;Short&lt;/span&gt; &lt;span class="k"&gt;Code&lt;/span&gt; &lt;span class="k"&gt;Ltd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;27&lt;/span&gt;&lt;span class="k"&gt;AAPFU&lt;/span&gt;&lt;span class="mf"&gt;0939&lt;/span&gt;&lt;span class="k"&gt;F&lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="k"&gt;Z&lt;/span&gt;
&lt;span class="k"&gt;Bad&lt;/span&gt; &lt;span class="k"&gt;State&lt;/span&gt; &lt;span class="k"&gt;Co&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;99&lt;/span&gt;&lt;span class="k"&gt;AAPFU&lt;/span&gt;&lt;span class="mf"&gt;0939&lt;/span&gt;&lt;span class="k"&gt;F&lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="k"&gt;ZV&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python gstin_validator.py vendors.csv checked.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5 checked | 3 invalid | wrote checked.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;checked.csv&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="k"&gt;vendor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;gstin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;pan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;entity&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;registration&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;no&lt;/span&gt;
&lt;span class="k"&gt;Acme&lt;/span&gt; &lt;span class="k"&gt;Traders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;27&lt;/span&gt;&lt;span class="k"&gt;AAPFU&lt;/span&gt;&lt;span class="mf"&gt;0939&lt;/span&gt;&lt;span class="k"&gt;F&lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="k"&gt;ZV&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,,&lt;/span&gt;&lt;span class="k"&gt;Maharashtra&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;AAPFU&lt;/span&gt;&lt;span class="mf"&gt;0939&lt;/span&gt;&lt;span class="k"&gt;F&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;Partnership&lt;/span&gt; &lt;span class="k"&gt;Firm&lt;/span&gt; &lt;span class="err"&gt;/&lt;/span&gt; &lt;span class="k"&gt;LLP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;Bharat&lt;/span&gt; &lt;span class="k"&gt;Supplies&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;29&lt;/span&gt;&lt;span class="k"&gt;AAGCB&lt;/span&gt;&lt;span class="mf"&gt;7383&lt;/span&gt;&lt;span class="k"&gt;J&lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="k"&gt;Z&lt;/span&gt;&lt;span class="mf"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,,&lt;/span&gt;&lt;span class="k"&gt;Karnataka&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;AAGCB&lt;/span&gt;&lt;span class="mf"&gt;7383&lt;/span&gt;&lt;span class="k"&gt;J&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;Company&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;Typo&lt;/span&gt; &lt;span class="k"&gt;Enterprises&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;27&lt;/span&gt;&lt;span class="k"&gt;AAPFU&lt;/span&gt;&lt;span class="mf"&gt;0939&lt;/span&gt;&lt;span class="k"&gt;F&lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="k"&gt;ZX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;checksum&lt;/span&gt; &lt;span class="k"&gt;mismatch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;expected&lt;/span&gt; &lt;span class="k"&gt;V&lt;/span&gt;&lt;span class="p"&gt;),,,,&lt;/span&gt;
&lt;span class="k"&gt;Short&lt;/span&gt; &lt;span class="k"&gt;Code&lt;/span&gt; &lt;span class="k"&gt;Ltd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;27&lt;/span&gt;&lt;span class="k"&gt;AAPFU&lt;/span&gt;&lt;span class="mf"&gt;0939&lt;/span&gt;&lt;span class="k"&gt;F&lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="k"&gt;Z&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"length 14, expected 15"&lt;/span&gt;&lt;span class="p"&gt;,,,,&lt;/span&gt;
&lt;span class="k"&gt;Bad&lt;/span&gt; &lt;span class="k"&gt;State&lt;/span&gt; &lt;span class="k"&gt;Co&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;99&lt;/span&gt;&lt;span class="k"&gt;AAPFU&lt;/span&gt;&lt;span class="mf"&gt;0939&lt;/span&gt;&lt;span class="k"&gt;F&lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="k"&gt;ZV&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;unknown&lt;/span&gt; &lt;span class="k"&gt;state&lt;/span&gt; &lt;span class="k"&gt;code&lt;/span&gt; &lt;span class="mf"&gt;99&lt;/span&gt;&lt;span class="p"&gt;,,,,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the third row. &lt;code&gt;...1ZX&lt;/code&gt; instead of &lt;code&gt;...1ZV&lt;/code&gt; — one character off, structurally perfect, and a regex-only validator would have waved it straight through. The checksum caught it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not do
&lt;/h2&gt;

&lt;p&gt;Be honest with yourself about the limits, because this is where people get burned:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It cannot tell you if a GSTIN is registered or active.&lt;/strong&gt; A number can be arithmetically perfect and belong to nobody, or belong to a registration that was cancelled last March. Only the GST portal knows that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It cannot tell you the number belongs to the vendor who gave it to you.&lt;/strong&gt; Cross-checking the embedded PAN against your own records helps here, and costs you nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The state list needs occasional maintenance.&lt;/strong&gt; Codes 25 and 26 shifted when Dadra &amp;amp; Nagar Haveli merged with Daman &amp;amp; Diu; 38 (Ladakh) is recent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So treat this as a filter, not a verdict. Run it across the whole master, fix the arithmetically-broken rows first — those are free wins, no external calls needed — and then spend your portal lookups on the ones that survive. On a 400-row vendor master that usually means you are checking 15 numbers online instead of 400.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to put it
&lt;/h2&gt;

&lt;p&gt;The two places it earns its keep:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;At the point of entry.&lt;/strong&gt; Wire &lt;code&gt;validate()&lt;/code&gt; into whatever form or sheet captures a new vendor. Rejecting a bad GSTIN at creation time is worth ten times catching it at filing time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;As a pre-flight check.&lt;/strong&gt; Run it over the return data before you generate the upload file. Thirty seconds now, versus a rejected JSON and a re-run later.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The whole thing is standard library, so it drops into a Lambda, a cron job, or a Google Sheets Apps Script port without a &lt;code&gt;requirements.txt&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;Follow me on Twitter &lt;a href="https://twitter.com/automate_archit" rel="noopener noreferrer"&gt;@automate_archit&lt;/a&gt; for daily AI automation tips.&lt;/p&gt;

</description>
      <category>python</category>
      <category>automation</category>
      <category>tutorial</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
