<?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: Alessandro Binda</title>
    <description>The latest articles on DEV Community by Alessandro Binda (@alessandrobinda114).</description>
    <link>https://dev.to/alessandrobinda114</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%2F3935544%2F9c668a3a-ffd5-44f2-b1a0-c80b97ee480b.png</url>
      <title>DEV Community: Alessandro Binda</title>
      <link>https://dev.to/alessandrobinda114</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alessandrobinda114"/>
    <language>en</language>
    <item>
      <title>How to Stop an AI Assistant From Inventing Numbers in Business Reports</title>
      <dc:creator>Alessandro Binda</dc:creator>
      <pubDate>Wed, 28 Oct 2026 20:32:12 +0000</pubDate>
      <link>https://dev.to/alessandrobinda114/how-to-stop-an-ai-assistant-from-inventing-numbers-in-business-reports-24oh</link>
      <guid>https://dev.to/alessandrobinda114/how-to-stop-an-ai-assistant-from-inventing-numbers-in-business-reports-24oh</guid>
      <description>&lt;p&gt;&lt;em&gt;A method, not a guarantee: no setup removes errors completely. We give no accuracy percentages because we have not published a measurement.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does an AI assistant invent numbers?
&lt;/h2&gt;

&lt;p&gt;A language model writes text that sounds right. If you ask for a figure that is not in front of it, it can produce a plausible one. The risk is highest when the model is asked to remember, estimate or sum figures from a long document. The fix is not a better prompt alone; it is a different division of labour.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the basic rule?
&lt;/h2&gt;

&lt;p&gt;Numbers come from the data, calculations come from code, and the model writes the explanation. The assistant should never be the source of a figure. If you keep that rule, most invented numbers disappear because the model has nothing to invent: it is handed the figures it may quote.&lt;/p&gt;

&lt;h2&gt;
  
  
  Control 1: how do I show where each number comes from?
&lt;/h2&gt;

&lt;p&gt;Attach to every figure its origin: the table, the cell or the query, and the date. In a report this can be a footnote or a hover. A number without an origin is treated as unverified and should not be published. This also lets a reader check a figure in seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Control 2: why should totals be calculated outside the model?
&lt;/h2&gt;

&lt;p&gt;Sums, averages, percentages and year-on-year changes are deterministic. A spreadsheet formula or a database query gets them right every time; a language model sometimes does not. Compute them first, then pass the results to the model as given facts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Control 3: what should the assistant do when a figure is missing?
&lt;/h2&gt;

&lt;p&gt;Write "data not available" and say which source was empty. Tell the assistant explicitly that this is the correct answer when data is absent. Without that instruction, models tend to fill the gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Control 4: how do I catch figures that look wrong?
&lt;/h2&gt;

&lt;p&gt;Compare each figure to the previous period and to a plausible range. A cost line that moves by an order of magnitude, or a margin above 100%, is flagged automatically. These checks are cheap and catch both data errors and model errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Control 5: who reviews, and what do they review?
&lt;/h2&gt;

&lt;p&gt;A person reviews only the figures that were flagged or that changed the conclusion, not the whole report. Keep the reviewer's name and date on the report. If no figure is flagged, say so in the report.&lt;/p&gt;

&lt;h2&gt;
  
  
  What can I measure to know the setup works?
&lt;/h2&gt;

&lt;p&gt;Count the errors found by the controls and by readers over a period, compared with the number of figures checked. Publish the method with the number, or do not publish the number. We do not publish an accuracy figure for S.C.A.L.A. today.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should I ask any AI vendor, including S.C.A.L.A.?
&lt;/h2&gt;

&lt;p&gt;Ask to see the origin of any number you plan to use, and ask what the assistant does when data is missing. If the answer to either is "the model estimates it", do not use the figure in a report.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://get-scala.com/en/blog/how-to-stop-ai-inventing-numbers-in-business-reports" rel="noopener noreferrer"&gt;https://get-scala.com/en/blog/how-to-stop-ai-inventing-numbers-in-business-reports&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>data</category>
      <category>productivity</category>
    </item>
    <item>
      <title>SPF, DKIM and DMARC: A Setup Checklist for Business Email Senders</title>
      <dc:creator>Alessandro Binda</dc:creator>
      <pubDate>Wed, 28 Oct 2026 20:32:11 +0000</pubDate>
      <link>https://dev.to/alessandrobinda114/spf-dkim-and-dmarc-a-setup-checklist-for-business-email-senders-5gb0</link>
      <guid>https://dev.to/alessandrobinda114/spf-dkim-and-dmarc-a-setup-checklist-for-business-email-senders-5gb0</guid>
      <description>&lt;p&gt;&lt;em&gt;Google requirements read on 28 October 2026. Your email provider's own instructions are the reference for the exact DNS records.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What are SPF, DKIM and DMARC?
&lt;/h2&gt;

&lt;p&gt;SPF lists the servers allowed to send email for your domain. DKIM adds a cryptographic signature that receivers can check. DMARC tells receivers what to do when SPF or DKIM checks fail and sends you reports. Together they let a receiving server see that a message really comes from your domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do I need all three?
&lt;/h2&gt;

&lt;p&gt;For senders of more than 5,000 messages per day to Gmail, Google requires SPF and DKIM and a DMARC policy (the policy can be "none"), plus valid forward and reverse DNS records and TLS (&lt;a href="https://support.google.com/a/answer/81126" rel="noopener noreferrer"&gt;Google&lt;/a&gt;). Below that volume it is still the baseline expected by most receivers.&lt;/p&gt;

&lt;h2&gt;
  
  
  In what order should I set them up?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Make an inventory of every service that sends mail as your domain (company mail, newsletter tool, CRM, support desk, outreach tool).&lt;/li&gt;
&lt;li&gt;Publish one SPF record that includes all of them.&lt;/li&gt;
&lt;li&gt;Enable DKIM signing in each service and publish the keys it gives you.&lt;/li&gt;
&lt;li&gt;Publish a DMARC record with policy "none" and an address for reports.&lt;/li&gt;
&lt;li&gt;Read the reports for a few weeks, fix the senders that fail, then consider a stricter policy.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What is the most common SPF mistake?
&lt;/h2&gt;

&lt;p&gt;Publishing two SPF records for the same domain, or listing so many services that the record exceeds the lookup limit. Keep one record, and remove services you no longer use.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does "alignment" mean?
&lt;/h2&gt;

&lt;p&gt;For DMARC to pass, the domain in the From header must match the domain checked by SPF or DKIM. Google lists this as a requirement for direct email. A tool that sends "from your domain" using its own signing domain can fail DMARC even when SPF and DKIM pass individually.&lt;/p&gt;

&lt;h2&gt;
  
  
  What else does Google require?
&lt;/h2&gt;

&lt;p&gt;Keep the spam rate in Postmaster Tools below 0.30% (Google recommends below 0.10%), format messages according to RFC 5322, do not impersonate Gmail From headers, and support one-click unsubscribe with a clearly visible link in marketing messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I test the setup?
&lt;/h2&gt;

&lt;p&gt;Send a test message to a mailbox that shows the original headers and read the SPF, DKIM and DMARC results. Repeat for each sending service. Check again after any DNS or tool change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where does this fit in cold outreach?
&lt;/h2&gt;

&lt;p&gt;Authentication is necessary but not sufficient: list quality, volumes and legal basis matter just as much. See &lt;a href="https://get-scala.com/en/blog/b2b-cold-email-europe-without-spam" rel="noopener noreferrer"&gt;B2B Cold Email in Europe Without Landing in Spam&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://get-scala.com/en/blog/spf-dkim-dmarc-setup-checklist" rel="noopener noreferrer"&gt;https://get-scala.com/en/blog/spf-dkim-dmarc-setup-checklist&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>email</category>
      <category>devops</category>
      <category>security</category>
      <category>sysadmin</category>
    </item>
    <item>
      <title>What 100 fast-growing UK SMEs have in common (data from Companies House)</title>
      <dc:creator>Alessandro Binda</dc:creator>
      <pubDate>Sat, 24 Oct 2026 23:06:43 +0000</pubDate>
      <link>https://dev.to/alessandrobinda114/what-100-fast-growing-uk-smes-have-in-common-data-from-companies-house-3mfp</link>
      <guid>https://dev.to/alessandrobinda114/what-100-fast-growing-uk-smes-have-in-common-data-from-companies-house-3mfp</guid>
      <description>&lt;p&gt;The UK has about five million active companies, but "fast-growing" lists usually mean venture-backed start-ups. We looked at the other end: &lt;strong&gt;independent private companies that grew turnover quickly using only the accounts they filed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The result is the &lt;em&gt;SCALA Growth 100 UK (2022-2024)&lt;/em&gt;: the 100 independent UK SMEs with the fastest turnover growth between FY2022 and FY2024. It is computed from Companies House filings, it is not an official Companies House list, and no company paid or applied to be on it. (How we parsed 7.35 million filings is in &lt;a href="https://dev.to/alessandrobinda114/how-we-found-the-uks-100-fastest-growing-smes-in-7-million-companies-house-filings-and-why-4-in-3pj"&gt;the technical write-up&lt;/a&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What the 100 look like
&lt;/h2&gt;

&lt;p&gt;All figures below come from the published CSV (turnover as filed, in pounds):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Growth:&lt;/strong&gt; the median company grew turnover by &lt;strong&gt;31.4% a year&lt;/strong&gt; (range 22.5% to 93.8%), or &lt;strong&gt;73% over the two years&lt;/strong&gt; at the median.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Size:&lt;/strong&gt; the median company went from &lt;strong&gt;£13.1M in 2022 to £24.8M in 2024&lt;/strong&gt;. 63 of the 100 had at least £20M turnover in 2024; 12 started below £5M.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leader:&lt;/strong&gt; Travellanda Ltd (Borehamwood) grew from £8.8M to £33.0M (+275.6%).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Largest in 2024:&lt;/strong&gt; MM Freight Ltd, Prestwick Aircraft Maintenance Ltd and New Century Foods Ltd, each around £41M.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where:&lt;/strong&gt; 11 are in London, 3 in Glasgow; the other 86 are spread across 78 other places.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What they do:&lt;/strong&gt; business support services (6), online and mail-order retail (4), other manufacturing (3), motor vehicle repair (3), property development (3), food services (3). No single sector dominates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So these are not tiny start-ups: most are established companies in ordinary sectors that doubled their turnover in two years.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the list was built (short version)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Only accounts filed at Companies House, FY2022 to FY2024.&lt;/li&gt;
&lt;li&gt;Independent companies only: no foreign or group parent, no company controlled by another company.&lt;/li&gt;
&lt;li&gt;Holding, property, finance and investment vehicles excluded.&lt;/li&gt;
&lt;li&gt;Each company reviewed individually (group membership, mergers, insolvency, consistency with public sources). When in doubt, it is left out.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Use the data
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Ranking, method and badges: &lt;a href="https://score.get-scala.com/rankings/uk/growth-100/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=g100-uk-2026" rel="noopener noreferrer"&gt;score.get-scala.com/rankings/uk/growth-100&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CSV and JSON (open data): &lt;a href="https://github.com/Alessandro114/uk-fastest-growing-companies/tree/main/data" rel="noopener noreferrer"&gt;github.com/Alessandro114/uk-fastest-growing-companies&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Look up any UK company's financials for free: &lt;a href="https://score.get-scala.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=g100-uk-2026" rel="noopener noreferrer"&gt;S.C.A.L.A. Score&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your company is on the list, or you think a figure or a group link is wrong, tell us: corrections and removal requests are always honoured.&lt;/p&gt;

</description>
      <category>data</category>
      <category>uk</category>
      <category>startup</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How we found the UK's 100 fastest-growing SMEs in 7 million Companies House filings (and why 4 in 10 candidates were wrong)</title>
      <dc:creator>Alessandro Binda</dc:creator>
      <pubDate>Wed, 14 Oct 2026 14:00:28 +0000</pubDate>
      <link>https://dev.to/alessandrobinda114/how-we-found-the-uks-100-fastest-growing-smes-in-7-million-companies-house-filings-and-why-4-in-3pj</link>
      <guid>https://dev.to/alessandrobinda114/how-we-found-the-uks-100-fastest-growing-smes-in-7-million-companies-house-filings-and-why-4-in-3pj</guid>
      <description>&lt;p&gt;Ask an AI assistant "which UK SMEs grew fastest in the last two years?" and you'll get either famous scale-ups from press releases or "I don't have reliable data on that".&lt;/p&gt;

&lt;p&gt;The data exists. Every UK company files accounts at Companies House, and a large share of them are machine-readable iXBRL. So we built the answer from scratch: a ranking of the 100 independent UK SMEs whose turnover grew fastest from FY2022 to FY2024, published as a CSV, a zero-dependency CLI and a skill that AI agents can use.&lt;/p&gt;

&lt;p&gt;This post is about how we built it, and about the part nobody tells you: most of the "fastest-growing" companies you find in raw filings aren't.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Reading 7.35 million filings without filling the disk
&lt;/h2&gt;

&lt;p&gt;Companies House publishes a monthly zip of every electronically filed set of accounts. We processed 24 months (January 2024 to December 2025): &lt;strong&gt;7,352,908 filings&lt;/strong&gt;, between 2 and 4.4 GB per archive.&lt;/p&gt;

&lt;p&gt;One archive at a time, read straight from the zip, deleted after parsing. Each file is an XHTML document with inline XBRL facts like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ix:nonFraction&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"core:TurnoverRevenue"&lt;/span&gt; &lt;span class="na"&gt;contextRef=&lt;/span&gt;&lt;span class="s"&gt;"c2"&lt;/span&gt; &lt;span class="na"&gt;unitRef=&lt;/span&gt;&lt;span class="s"&gt;"GBP"&lt;/span&gt;
                &lt;span class="na"&gt;decimals=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;scale=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;format=&lt;/span&gt;&lt;span class="s"&gt;"ixt:num-dot-decimal"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;21,770,081&lt;span class="nt"&gt;&amp;lt;/ix:nonFraction&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parser looks for a short list of turnover tags (whatever the namespace prefix), honours &lt;code&gt;scale&lt;/code&gt;, &lt;code&gt;sign&lt;/code&gt; and the number format, keeps only GBP units, and skips any context with dimensions, which is how consolidated (group) figures are tagged:&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;TAGS&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;TurnoverRevenue&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;Turnover&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;TurnoverGrossOperatingRevenue&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;TurnoverNetOperatingRevenue&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;Revenue&lt;/span&gt;&lt;span class="sh"&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;parse_num&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;txt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;strip_tags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;txt&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="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;zerodash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&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;scale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sign&lt;/span&gt; &lt;span class="o"&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="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every set of accounts carries two periods: the current year and the prior year as comparative. Take the period with the latest end date as "current" and the one ending within 10 days of its start as "prior", and &lt;strong&gt;one filing gives you two years&lt;/strong&gt;. Two years of archives give you three years of turnover.&lt;/p&gt;

&lt;p&gt;Only &lt;strong&gt;162,700 filings (2.2%)&lt;/strong&gt; report turnover at all. Small and micro companies are allowed to file without a profit and loss account, so this is a ranking of medium-sized and larger SMEs by construction.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Three years per company, in one query
&lt;/h2&gt;

&lt;p&gt;Facts go to Parquet, one file per month. DuckDB then builds one row per company and financial year, preferring a figure reported as "current" over the same period reported as a comparative a year later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row_number&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;cn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pe&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;pri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;archive&lt;/span&gt; &lt;span class="k"&gt;DESC&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;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;company_number&lt;/span&gt; &lt;span class="n"&gt;cn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;period_start&lt;/span&gt; &lt;span class="n"&gt;ps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;period_end&lt;/span&gt; &lt;span class="n"&gt;pe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;turnover&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="n"&gt;pri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;archive&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;
        &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;company_number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prior_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prior_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prior_turnover&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;archive&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;prior&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;12-month financial years only (350 to 380 days), same year-end month in 2022 and 2024&lt;/li&gt;
&lt;li&gt;turnover rising every year, FY2022 → FY2023 → FY2024&lt;/li&gt;
&lt;li&gt;at least £1.7m in 2022 and between £4.3m and £43m in 2024 (SME range)&lt;/li&gt;
&lt;li&gt;no single-year jump above 2.5×, which is almost always a merger or a hive-up rather than sales&lt;/li&gt;
&lt;li&gt;if the 2025 accounts are already filed and turnover fell by more than 30%, the "growth" was a one-off peak: out&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That left 888 candidates. That's where the real work started.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The false positives
&lt;/h2&gt;

&lt;p&gt;We checked candidates by hand: the Companies House page of each one (persons with significant control, filing history, officers), plus the company's own website and press. &lt;strong&gt;260 checked, 97 thrown out.&lt;/strong&gt; In a parallel ranking for France built from INPI filings, 332 checked, 126 thrown out. Almost 4 in 10, in both countries.&lt;/p&gt;

&lt;p&gt;What they were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Part of a bigger group (42).&lt;/strong&gt; A subsidiary of a foreign parent or a larger UK group, or owned by private equity, that looks exactly like an independent SME in its own accounts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Doubts we couldn't resolve (36).&lt;/strong&gt; We left them out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Insolvency or strike-off (10).&lt;/strong&gt; Growing turnover, then administration, liquidation or a proposal to strike off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Something else (8).&lt;/strong&gt; Including a cluster of companies recently renamed after a single person, with micro-entity accounts that didn't match the turnover they reported.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Growth bought, not earned (1).&lt;/strong&gt; An acquisition explained the jump.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The useful lesson: most of this can be caught automatically once you know to look. Three rules from the free Companies House company data product (status exactly "Active", account category not micro, dormant or audit-exempt subsidiary, and no name change since 2023) caught &lt;strong&gt;all 10&lt;/strong&gt; of the insolvency cases we had found by hand, without losing a single company we had confirmed.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Independence, from the PSC register
&lt;/h2&gt;

&lt;p&gt;The persons with significant control (PSC) snapshot is free too. We kept only corporate controllers (no personal data stored) and excluded a company when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a corporate PSC is registered outside the UK, or&lt;/li&gt;
&lt;li&gt;a UK corporate PSC is itself controlled by another company, or turns over more than £43m.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A founder's personal holding company passes. A group's trading subsidiary doesn't. The manual review then caught what the register can't see.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Making it usable by AI agents
&lt;/h2&gt;

&lt;p&gt;A ranking on a web page answers humans. To answer agents, we shipped the data as a CLI that prints JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; github:Alessandro114/uk-fastest-growing-companies filter &lt;span class="nt"&gt;--region&lt;/span&gt; Scotland &lt;span class="nt"&gt;--table&lt;/span&gt;
&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;#2   Diona Construction Ltd     Glasgow    £9.6m -&amp;gt; £28.1m   +71.3%/yr
#3   I3works Ltd                Glasgow    £9.8m -&amp;gt; £27.3m   +67.3%/yr
#14  Pacson Limited             Dundee     £6.3m -&amp;gt; £13.8m   +48.3%/yr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why a CLI rather than an API: an agent calls it with one short line, gets compact JSON, and if a command fails it just tries another. No API key, no long request bodies, no wasted context. A &lt;code&gt;SKILL.md&lt;/code&gt; tells Claude (or any agent with a shell) when to use it and how to quote the figures: in GBP, with the year, and with the Companies House link for each company.&lt;/p&gt;

&lt;p&gt;In Claude Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/plugin marketplace add Alessandro114/uk-fastest-growing-companies
/plugin install uk-fastest-growing-companies@uk-fastest-growing-companies
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then ask "which construction firms in Scotland grew fastest?" and you get filed numbers instead of a guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  What came out
&lt;/h2&gt;

&lt;p&gt;The median company on the list grew 31% a year. Together, the 100 went from £1.3bn to £2.35bn in turnover. 81 of them are outside Greater London, and the largest group is construction (14 companies), not software.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repo (data, CLI, skill): &lt;a href="https://github.com/Alessandro114/uk-fastest-growing-companies" rel="noopener noreferrer"&gt;https://github.com/Alessandro114/uk-fastest-growing-companies&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Full ranking and method: &lt;a href="https://score.get-scala.com/rankings/uk/growth-100/" rel="noopener noreferrer"&gt;https://score.get-scala.com/rankings/uk/growth-100/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this saves you a weekend of parsing iXBRL, a star on the repo helps others find it. And if you spot a company that shouldn't be there, open an issue: every correction makes the list better.&lt;/p&gt;

</description>
      <category>opendata</category>
      <category>python</category>
      <category>datascience</category>
      <category>ai</category>
    </item>
    <item>
      <title>I Replaced whatsapp-web.js With a REST API and My Bot Stopped Crashing</title>
      <dc:creator>Alessandro Binda</dc:creator>
      <pubDate>Sun, 30 Aug 2026 19:49:30 +0000</pubDate>
      <link>https://dev.to/alessandrobinda114/i-replaced-whatsapp-webjs-with-a-rest-api-and-my-bot-stopped-crashing-357j</link>
      <guid>https://dev.to/alessandrobinda114/i-replaced-whatsapp-webjs-with-a-rest-api-and-my-bot-stopped-crashing-357j</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;If you've built a WhatsApp bot with &lt;code&gt;whatsapp-web.js&lt;/code&gt;, you know the pain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Random disconnects every 2-6 hours&lt;/li&gt;
&lt;li&gt;QR re-authentication breaking at 3am&lt;/li&gt;
&lt;li&gt;Memory leaks from the embedded Chromium instance&lt;/li&gt;
&lt;li&gt;Multi-instance? Forget it — each instance eats 500MB+ RAM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I ran this stack in production for 8 months. It worked... until it didn't. The breaking point was when I needed multi-tenant support: one server handling WhatsApp sessions for multiple businesses.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Migration
&lt;/h2&gt;

&lt;p&gt;I moved from &lt;code&gt;whatsapp-web.js&lt;/code&gt; (browser automation) to &lt;strong&gt;WAHA&lt;/strong&gt; (a proper REST API that wraps WhatsApp Web internally). The difference:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;whatsapp-web.js&lt;/th&gt;
&lt;th&gt;WAHA&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Architecture&lt;/td&gt;
&lt;td&gt;Your code spawns Chromium&lt;/td&gt;
&lt;td&gt;Standalone Docker container&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory per session&lt;/td&gt;
&lt;td&gt;~500MB&lt;/td&gt;
&lt;td&gt;~80MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-session&lt;/td&gt;
&lt;td&gt;Manual, fragile&lt;/td&gt;
&lt;td&gt;Native REST endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session persistence&lt;/td&gt;
&lt;td&gt;File-based, breaks&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reconnection&lt;/td&gt;
&lt;td&gt;You build it&lt;/td&gt;
&lt;td&gt;Automatic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API style&lt;/td&gt;
&lt;td&gt;Event-driven JS&lt;/td&gt;
&lt;td&gt;REST + webhooks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What Changed in the Code
&lt;/h2&gt;

&lt;p&gt;The biggest architectural shift: instead of your bot being the WhatsApp process, your bot becomes a &lt;strong&gt;webhook consumer&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Before (whatsapp-web.js):&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&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;Client&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;message&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;msg&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="c1"&gt;// Your bot IS the WhatsApp process&lt;/span&gt;
  &lt;span class="c1"&gt;// If this crashes, WhatsApp disconnects&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&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;generateAIResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Spawns Chromium&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After (WAHA webhook):&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="c1"&gt;// WAHA runs separately as a Docker container&lt;/span&gt;
&lt;span class="c1"&gt;// Your bot receives webhooks&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/webhook/message&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&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;response&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;generateAIResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Send via REST API&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;WAHA_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/sendText`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&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;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;chatId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;from&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;response&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&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 separation means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bot crashes don't kill WhatsApp sessions&lt;/li&gt;
&lt;li&gt;You can restart your bot without re-authenticating&lt;/li&gt;
&lt;li&gt;Multiple bots can share the same WAHA instance&lt;/li&gt;
&lt;li&gt;Scaling is just adding more webhook consumers&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The AI Agent Layer
&lt;/h2&gt;

&lt;p&gt;Once the WhatsApp connection was stable, I built the actual agent runtime on top: &lt;strong&gt;SARA&lt;/strong&gt; (&lt;a href="https://github.com/Alessandro114/sara" rel="noopener noreferrer"&gt;github.com/Alessandro114/sara&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;SARA handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tool-calling loop&lt;/strong&gt; on Groq/Cerebras/Mistral (free tiers)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;20 industry-specific knowledge packs&lt;/strong&gt; (restaurants, clinics, retail...)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session memory&lt;/strong&gt; that persists across conversations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-tenant isolation&lt;/strong&gt; — each business gets its own agent persona&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The entire stack runs self-hosted via Docker Compose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;waha&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;devlikeapro/waha&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3000:3000"&lt;/span&gt;
  &lt;span class="na"&gt;sara&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;WAHA_URL=http://waha:3000&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GROQ_API_KEY=${GROQ_API_KEY}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Results After Migration
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Uptime&lt;/strong&gt;: 99.7% over 3 months (vs ~92% with whatsapp-web.js)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory&lt;/strong&gt;: 80MB per session (vs 500MB)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy time&lt;/strong&gt;: restart bot in 2s without losing sessions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-tenant&lt;/strong&gt;: 5 businesses on one $10/mo VPS&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;SARA is open source (Apache-2.0): &lt;a href="https://github.com/Alessandro114/sara" rel="noopener noreferrer"&gt;github.com/Alessandro114/sara&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're stuck on whatsapp-web.js and hitting the same problems, the WAHA migration is worth the effort. The codebase is ~3K lines of TypeScript.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm the author of SARA. Questions about the migration or architecture welcome in the comments or on GitHub.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>whatsapp</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>We Built 41 AI Agents That Run an Entire Business — Here's What We Learned</title>
      <dc:creator>Alessandro Binda</dc:creator>
      <pubDate>Sun, 16 Aug 2026 19:43:11 +0000</pubDate>
      <link>https://dev.to/alessandrobinda114/we-built-41-ai-agents-that-run-an-entire-business-heres-what-we-learned-3725</link>
      <guid>https://dev.to/alessandrobinda114/we-built-41-ai-agents-that-run-an-entire-business-heres-what-we-learned-3725</guid>
      <description>&lt;h2&gt;
  
  
  The Problem Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;Every business runs on 7+ disconnected tools. Zendesk for support. PagerDuty for monitoring. Monday.com for projects. BambooHR for people. QuickBooks for invoices. A WhatsApp group for everything else.&lt;/p&gt;

&lt;p&gt;Total cost: $1,200-1,800/month. Total integration: zero.&lt;/p&gt;

&lt;p&gt;We asked: what if one AI platform handled all of it?&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;41 autonomous AI agents&lt;/strong&gt; organized in 9 packs, all accessible via WhatsApp:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HR &amp;amp; People&lt;/strong&gt; — An employee sends "How many vacation days do I have left?" on WhatsApp. SARA (our AI) checks the balance, finds 12 days, and submits the leave request to their manager. No app. No portal. No training.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Finance&lt;/strong&gt; — "What's the status of invoice 1234?" SARA finds it, sees it's 15 days overdue, and asks if you want to send a payment reminder. One message: done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IT Support&lt;/strong&gt; — "I need to reset my password for the CRM." SARA creates the IT ticket, sets priority, and confirms: "New credentials will arrive within 15 minutes."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Infrastructure&lt;/strong&gt; — At 2 AM, the database container crashes. Our guardian agent detects it in 60 seconds, recreates the container on the correct network, fixes the nginx proxy, and verifies health. You wake up to a "Recovered" notification. Total downtime: under 2 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 9 Agent Packs
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pack&lt;/th&gt;
&lt;th&gt;Agents&lt;/th&gt;
&lt;th&gt;What It Does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Support&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Ticketing, status, escalation via WhatsApp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HR &amp;amp; People&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Leave, payslips, onboarding checklists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Finance &amp;amp; Admin&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Invoicing, expenses, payment reminders&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compliance &amp;amp; Legal&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;GDPR requests, deadlines, contract renewals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sales &amp;amp; Marketing&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Lead scoring, pipeline, campaigns, follow-ups&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Project Management&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Tasks, assignments, progress, timelines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IT Support&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Password resets, access provisioning, system status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inventory&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Stock levels, reorder alerts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Proactive Customer&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Appointment reminders, feedback, churn detection&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Plus 7 infrastructure ops agents that run 24/7: auto-healing, DB performance monitoring, security audits, backup verification, and log anomaly detection.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;Every agent follows the same pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;SARA receives a message&lt;/strong&gt; (WhatsApp, Telegram, or web)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intent detection&lt;/strong&gt; determines which agent to invoke&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool dispatcher&lt;/strong&gt; calls the right handler with context (tenant, phone, sector)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handler&lt;/strong&gt; queries the database, performs the action, returns structured data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SARA&lt;/strong&gt; formats the response in natural language&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The handlers degrade gracefully — if a table doesn't exist yet (customer hasn't set up that module), they return a helpful message instead of crashing.&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="nf"&gt;registerHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;check_leave_balance&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;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&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;try&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s2"&gt;`SELECT leave_type, (total_days - used_days) AS remaining
             FROM hr_leave_balances WHERE user_id = $1 AND employee_phone = $2`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;phone&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&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;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;No leave data found.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;balances&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;does not exist&lt;/span&gt;&lt;span class="dl"&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="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HR module not configured yet.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&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;h2&gt;
  
  
  Why WhatsApp?
&lt;/h2&gt;

&lt;p&gt;Everyone has it. No app to install. No login to remember. No training needed.&lt;/p&gt;

&lt;p&gt;A CEO can manage their entire business from the same app they use to text their family. That's not a feature — that's a paradigm shift.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;41&lt;/strong&gt; AI agent tools across 9 business functions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;20&lt;/strong&gt; industry verticals (restaurant, hotel, legal, healthcare, retail...)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&amp;lt; 60 seconds&lt;/strong&gt; to auto-heal infrastructure failures&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;€2,000/month&lt;/strong&gt; for the entire suite (vs $1,800+ for fragmented tools)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;€0&lt;/strong&gt; to try — 14-day free trial, no credit card&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Free Tool: AI Business Health Check
&lt;/h2&gt;

&lt;p&gt;We also built a free tool that analyzes any business website and generates an AI-readiness score across 5 dimensions. Try it: &lt;a href="https://get-scala.com/en/health-check" rel="noopener noreferrer"&gt;get-scala.com/en/health-check&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;We're adding Meeting Notes via WhatsApp — send a voice memo of your meeting, get a structured summary with action items in 30 seconds. Free. Because the best marketing is a product people can't stop talking about.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;SCALA AI OS is an open platform. SARA, the core AI assistant, is &lt;a href="https://github.com/Alessandro114/sara" rel="noopener noreferrer"&gt;open source on GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Try SARA on WhatsApp: +39 351 787 4003&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>saas</category>
      <category>startup</category>
      <category>automation</category>
    </item>
    <item>
      <title>Every WhatsApp chatbot framework is broken. Here's what I built instead.</title>
      <dc:creator>Alessandro Binda</dc:creator>
      <pubDate>Sat, 15 Aug 2026 00:56:03 +0000</pubDate>
      <link>https://dev.to/alessandrobinda114/every-whatsapp-chatbot-framework-is-broken-heres-what-i-built-instead-3d2</link>
      <guid>https://dev.to/alessandrobinda114/every-whatsapp-chatbot-framework-is-broken-heres-what-i-built-instead-3d2</guid>
      <description>&lt;p&gt;I've evaluated every open-source WhatsApp bot framework on GitHub. They all share the same fatal flaw.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;Most WhatsApp bot frameworks are glorified API wrappers. They handle message transport — receiving a text, routing it somewhere, sending a reply — and that's it. The "intelligence" layer is left entirely to you. You get a pipe. You get a webhook. You get some session management. And then you're on your own.&lt;/p&gt;

&lt;p&gt;The frameworks that &lt;em&gt;do&lt;/em&gt; add AI make a different mistake: they duct-tape GPT onto the messaging pipe and call it "AI-powered." The pattern is always the same: receive message → append to conversation history → call &lt;code&gt;openai.chat.completions.create()&lt;/code&gt; → send reply. It's generic. It's stateless in any meaningful business sense. It doesn't know what industry it's serving, what data it has access to, or what actions it's actually allowed to take.&lt;/p&gt;

&lt;p&gt;Here's the part that breaks me: none of these frameworks understand that &lt;strong&gt;a restaurant needs different tools than a law firm&lt;/strong&gt;. A restaurant needs to check table availability, query allergens, create reservations, and handle cancellations. A law firm needs to schedule consultations, check document status, route inquiries by practice area. These are not the same problem. Treating them as "just chat" is the core architectural failure of every framework I've seen.&lt;/p&gt;

&lt;p&gt;And then there's the "enterprise" tier: Twilio Flex, Intercom, Freshchat. These charge $500–$2,000/month for what is fundamentally a prompt and a webhook wrapped in a dashboard. They're selling you infrastructure and calling it intelligence. The underlying model doesn't know your business. It can't execute actions in your systems. It's an expensive illusion.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Actually Needed
&lt;/h2&gt;

&lt;p&gt;The shift that matters isn't from "no AI" to "has AI." It's from &lt;strong&gt;generic chat&lt;/strong&gt; to &lt;strong&gt;domain-specific function calling&lt;/strong&gt;. This is not a subtle distinction.&lt;/p&gt;

&lt;p&gt;Here's what a properly architected tool dispatcher looks like versus what everyone else ships:&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="c1"&gt;// Each vertical gets domain-specific tools&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DINEOS_TOOLS&lt;/span&gt; &lt;span class="o"&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;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;check_availability&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;checkTableAvailability&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;check_allergens&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;checkMenuAllergens&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;book_table&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;createReservation&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;cancel_reservation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cancelWithPolicy&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_menu&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fetchLiveMenu&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;LEGALOS_TOOLS&lt;/span&gt; &lt;span class="o"&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;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;schedule_consultation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bookLawyerSlot&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;check_case_status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;queryCaseDB&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;route_inquiry&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;classifyAndRoute&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// vs the generic approach everyone else uses:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GENERIC_APPROACH&lt;/span&gt; &lt;span class="o"&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;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;chat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;askGPT&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// useless&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the model has access to real, domain-specific tools, it stops being a chatbot and starts being an agent. It can actually &lt;em&gt;do&lt;/em&gt; things: query live inventory, write to your reservations database, check against your allergen tables, trigger workflows in your backend. That's the difference between a wrapper and a platform.&lt;/p&gt;

&lt;p&gt;Two other things that almost no framework handles correctly: &lt;strong&gt;PII protection&lt;/strong&gt; and &lt;strong&gt;multi-provider failover&lt;/strong&gt;. You are routing customer names, phone numbers, order histories, and medical information through third-party LLM APIs. That's a GDPR liability waiting to happen. And when OpenAI has an outage — which they do, routinely — your entire customer-facing AI goes dark. These aren't edge cases. They're production requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I got tired of the same conversation and built &lt;a href="https://github.com/Alessandro114/sara" rel="noopener noreferrer"&gt;SARA&lt;/a&gt; — an open-source WhatsApp AI agent platform with 20 vertical-specific agent profiles, each with its own tool set, knowledge base, and autonomy configuration.&lt;/p&gt;

&lt;p&gt;The provider chain runs &lt;strong&gt;Groq → Cerebras → SambaNova → Mistral&lt;/strong&gt; in sequence. If Groq is down or rate-limited, the system fails over to Cerebras automatically. All four providers have generous free tiers, which means the inference cost for most deployments is literally zero. No OpenAI dependency. No single point of failure.&lt;/p&gt;

&lt;p&gt;Each tenant gets their own RAG instance — a pgvector knowledge base populated with their menus, policies, product catalogs, or legal documents. The model isn't hallucinating from its training data; it's retrieving from the business's actual content. Every query is scoped to that tenant's data.&lt;/p&gt;

&lt;p&gt;The piece I'm most proud of is the autonomy gate:&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="c1"&gt;// Autonomy levels — not every action should be automatic&lt;/span&gt;
&lt;span class="kr"&gt;enum&lt;/span&gt; &lt;span class="nx"&gt;AutonomyLevel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// AI suggests, human decides&lt;/span&gt;
  &lt;span class="nx"&gt;OBSERVE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// AI drafts, human approves  &lt;/span&gt;
  &lt;span class="nx"&gt;SEMI_AUTO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// AI acts on low-risk, asks on high-risk&lt;/span&gt;
  &lt;span class="nx"&gt;FULL_AUTO&lt;/span&gt;   &lt;span class="c1"&gt;// AI handles everything&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Risk classification before every action&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;classifyRisk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ToolArgs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;RiskLevel&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;tool&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;book_table&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;RiskLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LOW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// reversible&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;tool&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;process_refund&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;RiskLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HIGH&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// money moved&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;tool&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cancel_reservation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;RiskLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MEDIUM&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;RiskLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LOW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Gate checks: if action risk &amp;gt; autonomy level → ask human&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;autonomyGate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ToolArgs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AutonomyLevel&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;risk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;classifyRisk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&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;risk&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;RiskLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HIGH&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;level&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;AutonomyLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FULL_AUTO&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;blocked&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;requires_approval&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;blocked&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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;Before every LLM call, PII is anonymized — names replaced with tokens, phone numbers stripped, emails masked. The model never sees raw customer data. After the response is generated, the PII is re-injected for the actual reply. This is not optional if you're handling real customer data at scale.&lt;/p&gt;

&lt;p&gt;The result: 20 vertical agents (DineOS for restaurants, LegalOS for law firms, ClinicOS for healthcare, RetailOS for e-commerce, and 16 more), each with 3–6 domain-specific tools, running on a zero-cost inference stack, with PII protection baked in at the transport layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stop Wrapping APIs and Calling It AI
&lt;/h2&gt;

&lt;p&gt;The entire WhatsApp bot ecosystem has optimized for the wrong thing: ease of connection, not quality of intelligence. Getting a message in and a message out is a solved problem. The unsolved problem is making the agent actually useful for a specific business context.&lt;/p&gt;

&lt;p&gt;Stop wrapping APIs and calling it AI. Build agents that actually understand the domain.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;SARA is open source under AGPL-3.0. Code, architecture docs, and agent definitions are at &lt;a href="https://github.com/Alessandro114/sara" rel="noopener noreferrer"&gt;github.com/Alessandro114/sara&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>typescript</category>
      <category>whatsapp</category>
    </item>
    <item>
      <title>How I Built 20 AI Agents for WhatsApp — and Open-Sourced Everything</title>
      <dc:creator>Alessandro Binda</dc:creator>
      <pubDate>Wed, 12 Aug 2026 20:59:00 +0000</pubDate>
      <link>https://dev.to/alessandrobinda114/how-i-built-20-ai-agents-for-whatsapp-and-open-sourced-everything-2ipa</link>
      <guid>https://dev.to/alessandrobinda114/how-i-built-20-ai-agents-for-whatsapp-and-open-sourced-everything-2ipa</guid>
      <description>&lt;p&gt;Building a conversational AI platform that serves SMBs across 20 industries sounds straightforward until you hit the real constraints: latency, cost, privacy, and reliability — all at once. This is the story of how we architected &lt;strong&gt;SARA&lt;/strong&gt; (our AI agent layer for WhatsApp), the decisions we made, and why we open-sourced the entire thing.&lt;/p&gt;

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




&lt;h2&gt;
  
  
  The core problem: 20 verticals, one conversation interface
&lt;/h2&gt;

&lt;p&gt;SARA powers AI agents for restaurants, hotels, law firms, clinics, retail, logistics, and 14 other verticals — all through WhatsApp. Each vertical needs domain-specific tool access (a restaurant agent books tables; a clinic agent checks appointment slots). But the conversation interface is identical: a WhatsApp message in, a useful response out.&lt;/p&gt;

&lt;p&gt;The naive approach is one giant prompt per vertical. That breaks fast: context windows fill up, prompts drift, and debugging is a nightmare.&lt;/p&gt;

&lt;p&gt;Our approach: &lt;strong&gt;one core agent loop, 20 sets of tool definitions&lt;/strong&gt;. Function calling is the vertical differentiation layer, not the prompt.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture decision 1: Multi-provider LLM failover chain
&lt;/h2&gt;

&lt;p&gt;We chain free-tier LLM providers in priority order: &lt;strong&gt;Groq → Cerebras → SambaNova → Mistral&lt;/strong&gt;. Each is tried in sequence; on timeout or error, the next kicks in. The caller never knows which backend responded.&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="c1"&gt;// ai-providers.ts — simplified provider failover chain&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PROVIDER_CHAIN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AIProvider&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="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;groq&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Groq&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GROQ_API_KEY&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;llama-3.1-70b-versatile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;maxTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8192&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="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;cerebras&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Cerebras&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CEREBRAS_API_KEY&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;llama3.1-70b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;maxTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8192&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="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;sambanova&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SambaNova&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SAMBANOVA_API_KEY&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Meta-Llama-3.1-70B-Instruct&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;maxTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4096&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="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;mistral&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MistralClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MISTRAL_API_KEY&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mistral-large-latest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;maxTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8192&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getAIResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ChatMessage&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;ToolDefinition&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AIResponse&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;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;PROVIDER_CHAIN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;tool_choice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxTokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[AI] Provider &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; failed, trying next...`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;All AI providers exhausted&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key insight: the tool definitions are passed identically to every provider. OpenAI-compatible function calling is now a de-facto standard — Groq, Cerebras, SambaNova, and Mistral all support it, so the failover is truly transparent.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture decision 2: Tool dispatcher pattern
&lt;/h2&gt;

&lt;p&gt;Function calling gives you the LLM's &lt;em&gt;intent&lt;/em&gt; (&lt;code&gt;{ name: 'book_table', arguments: {...} }&lt;/code&gt;). You still need something to actually execute it. We built a centralized dispatcher with 30+ handlers — one per tool — all registered in a single map.&lt;/p&gt;

&lt;p&gt;The key design principle: &lt;strong&gt;tool handlers are pure functions&lt;/strong&gt;. They take validated arguments, hit the DB or an external API, and return a structured result. The agent loop feeds results back as &lt;code&gt;role: 'tool'&lt;/code&gt; messages until the LLM stops requesting tool calls.&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="c1"&gt;// tool-dispatcher.ts — dispatcher core + agent loop&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ToolHandler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AgentContext&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ToolResult&lt;/span&gt;&lt;span class="o"&gt;&amp;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;TOOL_REGISTRY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ToolHandler&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;book_table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="nx"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bookTable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;check_availability&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checkAvailability&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;get_menu&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="nx"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getMenu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;create_ticket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="nx"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createTicket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;get_appointment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="nx"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getAppointment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// ... 25+ more across 20 verticals&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;dispatchTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;toolCall&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ToolCall&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AgentContext&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ToolResult&lt;/span&gt;&lt;span class="o"&gt;&amp;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;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;TOOL_REGISTRY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;toolCall&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;handler&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;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Unknown tool: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;toolCall&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="o"&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;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toolCall&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;function&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;validated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validateToolArgs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toolCall&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;validated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&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;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;validated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;validated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Agent loop: keep calling tools until the LLM stops asking for them&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;chatChainWithTools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ChatMessage&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ToolDefinition&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AgentContext&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;response&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;getAIResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toolResults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tc&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;dispatchTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;assistant&lt;/span&gt;&lt;span class="dl"&gt;'&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="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tool_calls&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;toolResults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&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;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tool&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;tool_call_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tool_calls&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;id&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="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;r&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;response&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;getAIResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why function calling over prompt-only approaches?&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Structured output by default&lt;/strong&gt; — no regex parsing of free-text responses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Composable&lt;/strong&gt; — the LLM can chain multiple tools in a single turn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auditable&lt;/strong&gt; — every tool invocation is logged with exact arguments, giving a full per-conversation audit trail.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Architecture decision 3: PII anonymization before the LLM sees anything
&lt;/h2&gt;

&lt;p&gt;WhatsApp conversations contain real names, phone numbers, fiscal codes, addresses. Sending that raw to third-party LLM APIs is a GDPR problem and a trust problem.&lt;/p&gt;

&lt;p&gt;We anonymize PII before the message reaches any LLM, then deanonymize in the dispatcher layer — only when a tool actually needs the real value to write to the DB.&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="c1"&gt;// pii-anonymizer.ts&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PIIMap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// '[PHONE_1]' -&amp;gt; '+39 333 1234567'&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;anonymizeMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;anonymized&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;map&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PIIMap&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="na"&gt;map&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PIIMap&lt;/span&gt; &lt;span class="o"&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;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;PERSON&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;PHONE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;EMAIL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;anonymized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&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="nx"&gt;FISCAL_CODE_REGEX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`[ID_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="nx"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;]`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PHONE_REGEX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`[PHONE_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="nx"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PHONE&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;]`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;EMAIL_REGEX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`[EMAIL_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;EMAIL&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;]`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;anonymized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Restore real values in tool args before execution&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;deanonymizeToolArgs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PIIMap&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&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;return&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;parse&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;args&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="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\[(?:&lt;/span&gt;&lt;span class="sr"&gt;PERSON|PHONE|EMAIL|ID&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;_&lt;/span&gt;&lt;span class="se"&gt;\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ph&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ph&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;ph&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 LLM sees &lt;code&gt;[PHONE_1]&lt;/code&gt; and reasons about it correctly. The &lt;code&gt;book_table&lt;/code&gt; handler gets the real phone number only at execution time. The LLM API provider never sees actual PII.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture decision 4: Autonomy gate — risk-tiered execution
&lt;/h2&gt;

&lt;p&gt;Not all tool actions are equal. Reading a menu is safe. Cancelling a reservation or triggering a refund is not. An autonomy gate sits between the dispatcher and execution, classifying each tool call by risk level.&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="c1"&gt;// autonomy-gate.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TOOL_RISK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;low&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;get_menu&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;low&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;check_availability&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;low&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;book_table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;cancel_reservation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;process_refund&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;send_notification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;gateToolExecution&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AgentContext&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;risk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;TOOL_RISK&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;medium&lt;/span&gt;&lt;span class="dl"&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;risk&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;low&lt;/span&gt;&lt;span class="dl"&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="na"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;risk&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userConfirmed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Action "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" requires explicit user confirmation.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;risk&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;autonomyLevel&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Action "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" requires autonomy level &amp;gt;= 2.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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 autonomy level is set per-tenant at onboarding. A conservative restaurant owner sets level 1 (read-only AI, all writes need confirmation). A high-trust logistics operator sets level 3 (full autonomous execution). Same agent code, different gate behaviour.&lt;/p&gt;




&lt;h2&gt;
  
  
  What ships in the open-source repo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/Alessandro114/sara" rel="noopener noreferrer"&gt;github.com/Alessandro114/sara&lt;/a&gt; includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multi-tenant WhatsApp session management (WAHA-based)&lt;/li&gt;
&lt;li&gt;Full provider failover chain (Groq / Cerebras / SambaNova / Mistral)&lt;/li&gt;
&lt;li&gt;Tool dispatcher with 30+ handlers across 20 verticals&lt;/li&gt;
&lt;li&gt;PII anonymization + deanonymization pipeline&lt;/li&gt;
&lt;li&gt;Autonomy gate with configurable per-tenant risk levels&lt;/li&gt;
&lt;li&gt;RAG layer (pgvector + Jina embeddings v3) for per-tenant knowledge bases&lt;/li&gt;
&lt;li&gt;Dream cycle: proactive agent behaviours that fire outside conversation threads&lt;/li&gt;
&lt;li&gt;White-label support: each tenant gets their own agent identity and persona&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Licensed &lt;strong&gt;AGPL-3.0&lt;/strong&gt; — free to self-host, cloud hosting available for teams that prefer managed infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why open-source this?
&lt;/h2&gt;

&lt;p&gt;Most WhatsApp AI demos are either toy FAQ bots or closed-source commercial black boxes. We wanted to show what a production-grade, multi-vertical agent architecture actually looks like — the provider failover, the PII layer, the autonomy controls, the tool dispatch loop.&lt;/p&gt;

&lt;p&gt;The growing platform already serves SMBs across 20 industries. Open-sourcing the core means the community can audit, extend, and build on what we learned.&lt;/p&gt;

&lt;p&gt;If this architecture helps you ship faster or think differently about agent design, that is the win.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If this is useful, please star the repo: &lt;a href="https://github.com/Alessandro114/sara" rel="noopener noreferrer"&gt;https://github.com/Alessandro114/sara&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Open an issue to discuss architecture decisions, request a vertical definition, or contribute a new tool handler — PRs welcome.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>typescript</category>
      <category>whatsapp</category>
    </item>
    <item>
      <title>Why Vertical AI Agents Beat Horizontal Ones (And How to Build Them)</title>
      <dc:creator>Alessandro Binda</dc:creator>
      <pubDate>Sun, 09 Aug 2026 21:09:34 +0000</pubDate>
      <link>https://dev.to/alessandrobinda114/why-vertical-ai-agents-beat-horizontal-ones-and-how-to-build-them-4735</link>
      <guid>https://dev.to/alessandrobinda114/why-vertical-ai-agents-beat-horizontal-ones-and-how-to-build-them-4735</guid>
      <description>&lt;p&gt;ChatGPT can write a poem. It cannot manage your restaurant's Friday night rush.&lt;/p&gt;

&lt;p&gt;The gap between "general AI" and "AI that does a specific job" is where the real value lives. Here's why we bet everything on vertical AI agents — and the architecture behind 20 industry-specific agents running in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Horizontal Trap
&lt;/h2&gt;

&lt;p&gt;Horizontal AI tools (Zapier AI, generic chatbots, "AI for everything" platforms) share a fatal flaw: they know a little about everything and a lot about nothing.&lt;/p&gt;

&lt;p&gt;When a hotel manager asks "optimize my pricing for next weekend", a horizontal AI will give you a generic answer about dynamic pricing. A vertical HotelOS agent will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pull your occupancy data from the last 3 weekends&lt;/li&gt;
&lt;li&gt;Check local events (conference, festival, sports)&lt;/li&gt;
&lt;li&gt;Compare competitor rates on Booking.com&lt;/li&gt;
&lt;li&gt;Factor in your cost base and margin targets&lt;/li&gt;
&lt;li&gt;Suggest specific prices per room category with reasoning&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's not prompt engineering. That's &lt;strong&gt;domain-encoded business logic&lt;/strong&gt; exposed as tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture: One Framework, 20 Brains
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Shared Infrastructure:
├── AI Provider Chain (Groq/Cerebras/SambaNova/Mistral)
├── Tool Dispatcher (30+ handlers)
├── Autonomy Gate (risk classification)
├── OpenAPI Connector (universal SaaS integration)
└── Communication Layer (WhatsApp, Web, API)

Per-Vertical Agent Definition:
├── System prompt (industry-specific personality + knowledge)
├── Tool whitelist (which tools this agent can use)
├── Business rules (what requires human approval)
├── Data schema (tables, relationships, KPIs)
└── Proactive behaviors (when to reach out unprompted)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Agent Definition Example: DineOS (Restaurant)
&lt;/h3&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dineos"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DineOS Agent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"vertical"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"restaurant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tools"&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="s2"&gt;"create_reservation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"check_availability"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"menu_analysis"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"staff_schedule"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"food_cost_calculator"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"daily_revenue_report"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"supplier_order"&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;"proactive_behaviors"&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="nl"&gt;"trigger"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"reservation_count &amp;gt; capacity * 0.9"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"alert_owner"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Tonight is 90%+ booked. Consider opening the patio."&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="nl"&gt;"trigger"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ingredient_stock &amp;lt; reorder_point"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"draft_supplier_order"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"requires_approval"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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="nl"&gt;"autonomy_rules"&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;"low_risk"&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;"check_availability"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"menu_analysis"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"daily_revenue_report"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"medium_risk"&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;"create_reservation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"staff_schedule"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"high_risk"&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;"supplier_order"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pricing_change"&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;
  
  
  Why This Beats Fine-Tuning
&lt;/h3&gt;

&lt;p&gt;You don't need a fine-tuned model per vertical. You need:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The right tools&lt;/strong&gt; — a restaurant agent with &lt;code&gt;food_cost_calculator&lt;/code&gt; is more useful than a model that memorized 10,000 recipes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The right guardrails&lt;/strong&gt; — different verticals have different risk profiles&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The right data&lt;/strong&gt; — inject business-specific context (menu, pricing, inventory) at runtime, not training time&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This approach means we can launch a new vertical in days, not months:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Define the agent (system prompt + tool whitelist + rules) → 1 day&lt;/li&gt;
&lt;li&gt;Create the data schema (tables + migrations) → 1 day&lt;/li&gt;
&lt;li&gt;Wire up proactive behaviors → 1 day&lt;/li&gt;
&lt;li&gt;Test with real scenarios → 2 days&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Cross-Vertical Intelligence
&lt;/h2&gt;

&lt;p&gt;The real magic: agents that talk to each other.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Event: Large group booking (20 pax) at DineOS restaurant
  → DineOS notifies TravelOS: "20 guests arriving Saturday"
  → TravelOS checks hotel availability nearby
  → TravelOS offers group rate to the booking contact
  → AgencyOS logs the cross-sell opportunity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is implemented via an event bus:&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="nx"&gt;eventBus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;large_booking&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;vertical&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dineos&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;guest_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-08-15&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;contact&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;[REDACTED]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[REDACTED]&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="c1"&gt;// TravelOS listener&lt;/span&gt;
&lt;span class="nx"&gt;eventBus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;large_booking&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;event&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;guest_count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&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;availability&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;checkHotelAvailability&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&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;availability&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rooms&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;guest_count&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;suggestGroupRate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&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;h2&gt;
  
  
  The 20 Verticals
&lt;/h2&gt;

&lt;p&gt;We currently run agents for: Restaurant (DineOS), Hotel (HotelOS), Property (PropertyOS), Retail (RetailOS), Travel Agency (TravelOS), Facility Management (FacilityOS), Studio/Gym (StudioOS), Legal (LegalOS), Healthcare (HealthOS), Education (EduOS), Automotive (AutoOS), Real Estate (RealEstateOS), Construction (BuildOS), Logistics (LogisticsOS), Agriculture (AgroOS), Beauty/Spa (BeautyOS), Events (EventOS), Finance (FinanceOS), HR (HROS), and a General agent.&lt;/p&gt;

&lt;p&gt;All 21 agent definitions are open source: &lt;a href="https://github.com/Alessandro114/scala-agent-definitions" rel="noopener noreferrer"&gt;scala-agent-definitions&lt;/a&gt; (Apache-2.0).&lt;/p&gt;

&lt;h2&gt;
  
  
  Metrics That Matter
&lt;/h2&gt;

&lt;p&gt;For vertical AI agents, the metrics are different from chatbots:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Chatbot&lt;/th&gt;
&lt;th&gt;Vertical Agent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Success&lt;/td&gt;
&lt;td&gt;Response quality&lt;/td&gt;
&lt;td&gt;Task completion rate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Value&lt;/td&gt;
&lt;td&gt;Conversations&lt;/td&gt;
&lt;td&gt;Revenue generated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retention&lt;/td&gt;
&lt;td&gt;DAU&lt;/td&gt;
&lt;td&gt;Operational dependency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pricing&lt;/td&gt;
&lt;td&gt;Per-message&lt;/td&gt;
&lt;td&gt;Per-seat (flat monthly)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When a restaurant can't run Friday night without your agent, churn is near zero. That's the moat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Platform&lt;/strong&gt;: &lt;a href="https://get-scala.com" rel="noopener noreferrer"&gt;get-scala.com&lt;/a&gt; (Growth $97/mo, Scale $197/mo)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WhatsApp Agent (open source)&lt;/strong&gt;: &lt;a href="https://github.com/Alessandro114/sara" rel="noopener noreferrer"&gt;SARA on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent definitions (open source)&lt;/strong&gt;: &lt;a href="https://github.com/Alessandro114/scala-agent-definitions" rel="noopener noreferrer"&gt;scala-agent-definitions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RE feasibility agent (open source)&lt;/strong&gt;: &lt;a href="https://github.com/Alessandro114/landiq" rel="noopener noreferrer"&gt;LandIQ on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;The future of AI isn't one model that does everything. It's specialized agents that do one thing exceptionally well. Follow for more on building vertical AI.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>saas</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Building a Production WhatsApp AI Agent: Architecture That Actually Works</title>
      <dc:creator>Alessandro Binda</dc:creator>
      <pubDate>Sun, 09 Aug 2026 21:09:19 +0000</pubDate>
      <link>https://dev.to/alessandrobinda114/building-a-production-whatsapp-ai-agent-architecture-that-actually-works-2gfd</link>
      <guid>https://dev.to/alessandrobinda114/building-a-production-whatsapp-ai-agent-architecture-that-actually-works-2gfd</guid>
      <description>&lt;p&gt;Everyone demos a WhatsApp chatbot. Few run one in production with real customers sending real messages 24/7.&lt;/p&gt;

&lt;p&gt;After 18 months of running SARA — an open-source WhatsApp AI agent serving businesses across 20 industries — here's what we learned about architecture that survives contact with reality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why WhatsApp?
&lt;/h2&gt;

&lt;p&gt;The numbers are simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;2B+ monthly active users&lt;/li&gt;
&lt;li&gt;60% of SMB customers prefer messaging over calling&lt;/li&gt;
&lt;li&gt;98% open rate (vs 20% for email)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But WhatsApp is NOT just another chat channel. It has unique constraints that break naive implementations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WhatsApp (WAHA) → Bridge (:3008) → SARA API (:3006) → AI Provider Chain → Tool Dispatcher
                                                              ↓
                                                    Groq → Cerebras → SambaNova → Mistral
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Provider Fallback Chain
&lt;/h3&gt;

&lt;p&gt;Single-provider AI is a production risk. We use a 4-provider chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Primary: Groq (fastest, free tier)
    ↓ fail
Fallback 1: Cerebras
    ↓ fail
Fallback 2: SambaNova
    ↓ fail
Fallback 3: Mistral (paid, always works)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each provider gets 2 retries with exponential backoff before failover. Result: &lt;strong&gt;99.7% uptime&lt;/strong&gt; over 6 months with $0 inference cost (free tiers).&lt;/p&gt;

&lt;h3&gt;
  
  
  Tool Calling: Not Just Chat
&lt;/h3&gt;

&lt;p&gt;SARA doesn't just answer questions. She executes actions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;create_reservation&lt;/code&gt; — books a table with date normalization ("domani alle 8" → 2026-08-10T20:00)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;check_inventory&lt;/code&gt; — queries stock levels&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;generate_invoice&lt;/code&gt; — creates a PDF from database records&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;schedule_appointment&lt;/code&gt; — manages calendar slots&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The dispatcher maps 30+ tools to handlers with an autonomy gate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User message → Intent classification → Risk assessment → Tool execution
                                              ↓
                                    Low risk: execute immediately
                                    Medium: execute + notify owner
                                    High: ask for confirmation first
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You do NOT want your AI agent booking a catering order for 500 people without human approval.&lt;/p&gt;

&lt;h3&gt;
  
  
  PII Handling
&lt;/h3&gt;

&lt;p&gt;Messages contain names, phone numbers, addresses. Our pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Anonymize&lt;/strong&gt; before sending to LLM (replace "Mario Rossi" → "[PERSON_1]")&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Process&lt;/strong&gt; with anonymized data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;De-anonymize&lt;/strong&gt; tool calls only (the reservation needs the real name)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never log&lt;/strong&gt; PII in plain text&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Session Management
&lt;/h3&gt;

&lt;p&gt;WhatsApp doesn't have "sessions" — it's just a stream of messages. We manage context with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sliding window&lt;/strong&gt;: last 20 messages as context&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business context injection&lt;/strong&gt;: CRM data, menu, pricing injected per-tenant&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-conversation memory&lt;/strong&gt;: the agent remembers "last time you ordered the risotto"&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Self-Hosting vs Cloud
&lt;/h2&gt;

&lt;p&gt;SARA runs on a single VPS (4 vCPU, 8GB RAM):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Resource&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WAHA (WhatsApp Web)&lt;/td&gt;
&lt;td&gt;~500MB RAM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bridge service&lt;/td&gt;
&lt;td&gt;~50MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SARA API&lt;/td&gt;
&lt;td&gt;~200MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL + pgvector&lt;/td&gt;
&lt;td&gt;~2GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total&lt;/td&gt;
&lt;td&gt;~3GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No GPU needed — inference is offloaded to cloud providers (Groq, etc.).&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hardest Bugs
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WhatsApp session contention&lt;/strong&gt; — running two instances with the same number = instant logout for both. We learned this the hard way.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Date parsing across languages&lt;/strong&gt; — "dopodomani" (Italian for "day after tomorrow") + timezone handling + business hours awareness. This alone took weeks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Message ordering&lt;/strong&gt; — WhatsApp doesn't guarantee delivery order. Our bridge queues and re-orders by timestamp.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;SARA is AGPL-3.0 on GitHub: &lt;a href="https://github.com/Alessandro114/sara" rel="noopener noreferrer"&gt;github.com/Alessandro114/sara&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Self-host it, extend it, build your own vertical agent on top. Cloud-only features (multi-tenant, white-label, analytics) stay in the commercial version.&lt;/p&gt;

&lt;p&gt;The 20 industry-specific agent definitions are also open source: &lt;a href="https://github.com/Alessandro114/scala-agent-definitions" rel="noopener noreferrer"&gt;scala-agent-definitions&lt;/a&gt; (Apache-2.0).&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Proactive agents&lt;/strong&gt; — don't wait for messages, reach out when something needs attention ("Your reservation for tonight has a conflict")&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-agent events&lt;/strong&gt; — when DineOS agent sees a large booking, TravelOS agent checks nearby hotel availability&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Voice&lt;/strong&gt; — WhatsApp voice messages → STT → agent → TTS → voice reply&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Running AI in production is 10% model quality and 90% engineering. Follow for more war stories.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>whatsapp</category>
      <category>ai</category>
      <category>opensource</category>
      <category>agents</category>
    </item>
    <item>
      <title>How We Built a Universal SaaS Connector With OpenAPI + AI Agents</title>
      <dc:creator>Alessandro Binda</dc:creator>
      <pubDate>Sun, 09 Aug 2026 21:09:19 +0000</pubDate>
      <link>https://dev.to/alessandrobinda114/how-we-built-a-universal-saas-connector-with-openapi-ai-agents-4ap3</link>
      <guid>https://dev.to/alessandrobinda114/how-we-built-a-universal-saas-connector-with-openapi-ai-agents-4ap3</guid>
      <description>&lt;p&gt;Most SaaS integrations are built one at a time. Slack connector, Stripe connector, HubSpot connector — each one is weeks of custom code, OAuth flows, and webhook handling.&lt;/p&gt;

&lt;p&gt;We took a different approach: a single OpenAPI connector that lets our AI agents talk to &lt;strong&gt;any&lt;/strong&gt; SaaS with a spec file.&lt;/p&gt;

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

&lt;p&gt;Our platform (S.C.A.L.A.) serves 20 different industries — restaurants, hotels, property management, retail, and more. Each vertical needs different integrations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Restaurants need POS + reservation systems&lt;/li&gt;
&lt;li&gt;Hotels need PMS + channel managers&lt;/li&gt;
&lt;li&gt;Retail needs inventory + e-commerce platforms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Building 50+ custom integrations was not an option for a bootstrapped team.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User prompt → AI Agent → Tool Dispatcher → OpenAPI Connector → External SaaS
                                    ↓
                            spec_url or spec_json
                            auth_type (api_key | bearer | basic | oauth2)
                            encrypted credentials (AES-256)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 1: Import any OpenAPI spec
&lt;/h3&gt;

&lt;p&gt;Users paste a spec URL or upload JSON. We validate it, extract endpoints, and store the schema:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createConnectorSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&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="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&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="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;spec_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2048&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;spec_json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;auth_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api_key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bearer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;basic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;oauth2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
  &lt;span class="na"&gt;auth_config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;api_key_header&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;bearer_token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="c1"&gt;// ... other auth types&lt;/span&gt;
  &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;optional&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;h3&gt;
  
  
  Step 2: AI agent discovers available actions
&lt;/h3&gt;

&lt;p&gt;When a user says "check my inventory levels", the agent:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Looks at all connected OpenAPI specs&lt;/li&gt;
&lt;li&gt;Matches the intent to available endpoints (semantic search)&lt;/li&gt;
&lt;li&gt;Builds the API call with correct parameters&lt;/li&gt;
&lt;li&gt;Executes via SSRF-protected fetch&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 3: Credentials never leave the vault
&lt;/h3&gt;

&lt;p&gt;All secrets are encrypted at rest with AES-256. The connector decrypts only at call time, in-memory, never logged:&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;encryptSecret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;decryptSecret&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;../lib/encryption.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Store encrypted&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;encrypted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;encryptSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawApiKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Decrypt only when calling&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decryptSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;encrypted&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Security: SSRF Protection
&lt;/h2&gt;

&lt;p&gt;The biggest risk with a universal connector is SSRF — a malicious spec could point to internal services. We validate every URL before fetching:&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;validateUrlSSRF&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;safeFetch&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;../lib/ssrf.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Blocks: localhost, 169.254.x.x, 10.x.x.x, internal DNS&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;validated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validateUrlSSRF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetUrl&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;response&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;safeFetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;validated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Rate Limiting
&lt;/h2&gt;

&lt;p&gt;Each connector gets its own rate limit bucket. We use per-key in-process limiting (1000 req/min) on top of the global IP-based limiter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;API Key → SHA-256 hash → lookup in api_keys table → scope check → rate limit bucket → execute
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What This Enables
&lt;/h2&gt;

&lt;p&gt;With this pattern, adding a new SaaS integration takes &lt;strong&gt;minutes&lt;/strong&gt;, not weeks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User pastes the OpenAPI spec URL&lt;/li&gt;
&lt;li&gt;Sets auth type + credentials&lt;/li&gt;
&lt;li&gt;The AI agent immediately knows how to use every endpoint&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We've seen users connect everything from Shopify to custom ERPs — all through the same universal connector.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;OpenAPI specs are inconsistent&lt;/strong&gt; — many have wrong types, missing required fields, or incomplete auth docs. Build defensive parsing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AI needs curated endpoints&lt;/strong&gt; — showing the agent ALL 200 endpoints of a large API is noise. Let users pin the 5-10 they actually need.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Timeout aggressively&lt;/strong&gt; — external APIs are slow. 15-second hard timeout prevents one bad call from blocking the agent loop.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Log everything except secrets&lt;/strong&gt; — when debugging "why didn't the agent call Shopify correctly?", you need the full request chain minus credentials.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;S.C.A.L.A. is an agentic AI platform with 20 industry-specific agents. The OpenAPI connector is available on the Scale plan.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Website: &lt;a href="https://get-scala.com" rel="noopener noreferrer"&gt;get-scala.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Open-source WhatsApp agent: &lt;a href="https://github.com/Alessandro114/sara" rel="noopener noreferrer"&gt;SARA on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Open-source RE feasibility agent: &lt;a href="https://github.com/Alessandro114/landiq" rel="noopener noreferrer"&gt;LandIQ on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;API docs and sandbox available at &lt;a href="https://get-scala.com/en/api-docs" rel="noopener noreferrer"&gt;get-scala.com/api-docs&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Building AI agents for real businesses, not demos. Follow for more from the trenches.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>openapi</category>
      <category>ai</category>
      <category>saas</category>
      <category>typescript</category>
    </item>
    <item>
      <title>How I Built 100 Industry-Specific Website Templates and Why I'm Giving Them Away (Next.js + TypeScript)</title>
      <dc:creator>Alessandro Binda</dc:creator>
      <pubDate>Sat, 08 Aug 2026 00:28:42 +0000</pubDate>
      <link>https://dev.to/alessandrobinda114/how-i-built-100-industry-specific-website-templates-and-why-im-giving-them-away-nextjs--423i</link>
      <guid>https://dev.to/alessandrobinda114/how-i-built-100-industry-specific-website-templates-and-why-im-giving-them-away-nextjs--423i</guid>
      <description>&lt;p&gt;Last week I open-sourced a project I've been building for several months: 100 website components across 12 industry verticals, organized as a pnpm monorepo, MIT licensed, free to deploy.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/Alessandro114/scala-sites" rel="noopener noreferrer"&gt;https://github.com/Alessandro114/scala-sites&lt;/a&gt;&lt;br&gt;
Live demo: &lt;a href="https://scala-sites.vercel.app" rel="noopener noreferrer"&gt;https://scala-sites.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This post covers three things: the technical architecture decisions, the business reasoning behind giving it away, and a cost analysis that motivated the whole thing.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Problem with Generic Templates
&lt;/h3&gt;

&lt;p&gt;Every website template marketplace sells you the same thing: a generic hero component, a features grid, a testimonials section, a contact form. The assumption is that good design transfers across industries.&lt;/p&gt;

&lt;p&gt;It doesn't.&lt;/p&gt;

&lt;p&gt;A restaurant's website needs to answer: what's on the menu today, can I book a table right now, what do real customers say. A law firm's website needs to answer: do you handle my specific type of case, how do I get a consultation, what's your track record. A gym's website needs to answer: what classes run when, can I sign up directly, what membership costs.&lt;/p&gt;

&lt;p&gt;These are structurally different problems. A generic &lt;code&gt;&amp;lt;Hero&amp;gt;&lt;/code&gt; component with a title and a button doesn't solve any of them particularly well.&lt;/p&gt;

&lt;p&gt;So I built per-vertical hero components, per-vertical JSON-LD schemas, and per-vertical component sets.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Theming System
&lt;/h3&gt;

&lt;p&gt;Rather than a global design token system, I went with three base themes and a &lt;code&gt;createCustomTheme()&lt;/code&gt; override function:&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;createCustomTheme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;themeToStyleObject&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;@scala-sites/themes&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;restaurantTheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createCustomTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;classic&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;primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#1e3a5f&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#c9a84c&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#fafaf8&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="c1"&gt;// Use in your layout:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;themeToStyleObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;restaurantTheme&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// Returns: { '--color-primary': '#1e3a5f', '--color-accent': '#c9a84c', ... }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The three base themes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Minimal&lt;/strong&gt; — white background, Inter font, subtle borders. Works for luxury brands, professional services.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bold&lt;/strong&gt; — dark background, Bebas Neue headings, sharp edges. Works for gyms, nightlife, automotive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Classic&lt;/strong&gt; — warm tones, Playfair Display serif, soft radius. Works for traditional restaurants, heritage brands, legal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No CSS-in-JS runtime. No Tailwind config rebuild when you change a color. Pure CSS variables injected at the root. This means theme switching is a single property change, and SSR works without hydration mismatches.&lt;/p&gt;

&lt;h3&gt;
  
  
  JSON-LD by Vertical
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;@scala-sites/core&lt;/code&gt; SEO package handles structured data automatically based on which vertical you're in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;dineos&lt;/code&gt; (restaurant) emits &lt;code&gt;FoodEstablishment&lt;/code&gt; schema with &lt;code&gt;servesCuisine&lt;/code&gt;, &lt;code&gt;hasMenu&lt;/code&gt;, &lt;code&gt;priceRange&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;legalos&lt;/code&gt; (law firm) emits &lt;code&gt;LegalService&lt;/code&gt; with &lt;code&gt;areaServed&lt;/code&gt;, &lt;code&gt;knowsAbout&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;clinicos&lt;/code&gt; (medical) emits &lt;code&gt;MedicalBusiness&lt;/code&gt; with &lt;code&gt;medicalSpecialty&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gymos&lt;/code&gt; emits &lt;code&gt;SportsActivityLocation&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;FAQAccordion&lt;/code&gt; component — shared across all verticals — always emits &lt;code&gt;FAQPage&lt;/code&gt; schema, which Google uses to populate rich results directly in search.&lt;/p&gt;

&lt;p&gt;The goal is that a developer using these templates doesn't need to think about structured data. It's correct by default.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hero Architecture Decision
&lt;/h3&gt;

&lt;p&gt;The most debated decision in the repo was whether to have a single parameterized &lt;code&gt;&amp;lt;Hero&amp;gt;&lt;/code&gt; component or per-vertical hero implementations.&lt;/p&gt;

&lt;p&gt;I went per-vertical. Here's the argument:&lt;/p&gt;

&lt;p&gt;A restaurant hero needs: full-bleed video or image, overlay gradient, reservation CTA as primary action, menu CTA as secondary, possibly an urgency element ("booking fast this weekend").&lt;/p&gt;

&lt;p&gt;A gym hero needs: class schedule preview above the fold, membership CTA, possibly a live class counter or next-class timer.&lt;/p&gt;

&lt;p&gt;A law firm hero needs: practice area selector, consultation CTA, social proof counter (cases handled, years experience) — but restrained, because aggressive sales tactics read wrong in legal contexts.&lt;/p&gt;

&lt;p&gt;These aren't just different slot content. They're different interaction models. Forcing them into one component either makes the component so generic it adds no value, or makes it so complex with conditional logic that it becomes harder to maintain than separate components.&lt;/p&gt;

&lt;p&gt;Separate components means more code. But each component is smaller, more readable, and can evolve independently. A restaurant owner updating their hero doesn't need to understand the gym hero's class-schedule logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Cost Analysis
&lt;/h3&gt;

&lt;p&gt;The second thing I built is &lt;a href="https://analyze.get-scala.com" rel="noopener noreferrer"&gt;https://analyze.get-scala.com&lt;/a&gt; — a tool that shows what a typical SMB pays for its digital stack.&lt;/p&gt;

&lt;p&gt;I spent a week on pricing research across every major tool category. The result for a typical 3-location restaurant, using annual billing rates:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Monthly&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Website builder&lt;/td&gt;
&lt;td&gt;WordPress.com Business&lt;/td&gt;
&lt;td&gt;$40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email (5 users)&lt;/td&gt;
&lt;td&gt;Google Workspace&lt;/td&gt;
&lt;td&gt;$35&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CRM&lt;/td&gt;
&lt;td&gt;HubSpot Starter&lt;/td&gt;
&lt;td&gt;$40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Booking&lt;/td&gt;
&lt;td&gt;Calendly (2 seats)&lt;/td&gt;
&lt;td&gt;$20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Live chat + AI&lt;/td&gt;
&lt;td&gt;Tidio + Lyro&lt;/td&gt;
&lt;td&gt;$68&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WhatsApp API&lt;/td&gt;
&lt;td&gt;2K marketing msgs&lt;/td&gt;
&lt;td&gt;$40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SEO&lt;/td&gt;
&lt;td&gt;SEMrush Pro&lt;/td&gt;
&lt;td&gt;$140&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reviews&lt;/td&gt;
&lt;td&gt;Trustpilot Starter&lt;/td&gt;
&lt;td&gt;$99&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email marketing&lt;/td&gt;
&lt;td&gt;Mailchimp (2K contacts)&lt;/td&gt;
&lt;td&gt;$27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Forms&lt;/td&gt;
&lt;td&gt;Typeform Basic&lt;/td&gt;
&lt;td&gt;$29&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Social scheduling&lt;/td&gt;
&lt;td&gt;Buffer (3 channels)&lt;/td&gt;
&lt;td&gt;$15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analytics&lt;/td&gt;
&lt;td&gt;GA4&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;14 vendors&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$555/mo&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's $6,660/year. And this is the conservative version — with Intercom instead of Tidio you hit $1,500/month.&lt;/p&gt;

&lt;p&gt;Three things I found notable in the research:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mailchimp's free tier went from 2,000 contacts (2022) to 500 (2023) to 250 (2026). They degrade it deliberately to force upgrades.&lt;/li&gt;
&lt;li&gt;Ahrefs raised Lite from $99 to $129 in March 2026 with no feature changes.&lt;/li&gt;
&lt;li&gt;SiteGround intro pricing is as low as $2.99/month; renewal rates hit $17.99/month — a 6x jump that small business owners routinely don't notice until it hits.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full pricing data (with sources) is in &lt;code&gt;COST-COMPARISON-DATA.md&lt;/code&gt; in the repo.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Give It Away
&lt;/h3&gt;

&lt;p&gt;My company sells an AI operating system for SMBs — real-time booking, WhatsApp automations, multi-location CRM. The website is the entry point, not the product.&lt;/p&gt;

&lt;p&gt;The templates are free because reaching business owners early — before they've committed to a fragmented stack — is worth more than charging for templates. Give them something genuinely useful. Be present before anyone asks to see a pitch deck.&lt;/p&gt;

&lt;p&gt;It also creates a forcing function: MIT licensed code has to be good enough to fork. That pressure is useful.&lt;/p&gt;

&lt;h3&gt;
  
  
  Performance
&lt;/h3&gt;

&lt;p&gt;The demo app (Next.js 14, App Router, SSG) scores 97-100 on Lighthouse across all vertical pages. Static generation means zero TTFB on cached routes. The CSS variable theming adds zero runtime overhead — it's compiled at build time.&lt;/p&gt;

&lt;p&gt;No JavaScript is required to render any template above the fold. Booking widgets and interactive components lazy-load only when scrolled into view.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try It
&lt;/h3&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/Alessandro114/scala-sites" rel="noopener noreferrer"&gt;https://github.com/Alessandro114/scala-sites&lt;/a&gt;&lt;br&gt;
Cost analyzer: &lt;a href="https://analyze.get-scala.com" rel="noopener noreferrer"&gt;https://analyze.get-scala.com&lt;/a&gt;&lt;br&gt;
Live demo: &lt;a href="https://scala-sites.vercel.app" rel="noopener noreferrer"&gt;https://scala-sites.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Star the repo if it's useful. Open an issue if you want a vertical that's not there yet. PRs welcome — especially for industries outside the current 12.&lt;/p&gt;




</description>
      <category>nextjs</category>
      <category>typescript</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
