<?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: buildbyalex</title>
    <description>The latest articles on DEV Community by buildbyalex (@shurinbergo_5451b6dad08f8).</description>
    <link>https://dev.to/shurinbergo_5451b6dad08f8</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%2F4155479%2F9bd2899a-3287-4b58-9806-92cf4eab3c65.png</url>
      <title>DEV Community: buildbyalex</title>
      <link>https://dev.to/shurinbergo_5451b6dad08f8</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shurinbergo_5451b6dad08f8"/>
    <language>en</language>
    <item>
      <title>A 12,400-item B2B parts catalog on Next.js with quote requests straight to the inbox</title>
      <dc:creator>buildbyalex</dc:creator>
      <pubDate>Sun, 04 Oct 2026 12:07:35 +0000</pubDate>
      <link>https://dev.to/shurinbergo_5451b6dad08f8/a-12400-item-b2b-parts-catalog-on-nextjs-with-quote-requests-straight-to-the-inbox-4jla</link>
      <guid>https://dev.to/shurinbergo_5451b6dad08f8/a-12400-item-b2b-parts-catalog-on-nextjs-with-quote-requests-straight-to-the-inbox-4jla</guid>
      <description>&lt;p&gt;Baltic Dockyard supplies certified parts for marine engines, pumps and offshore rigs. Offline the business worked. Online it barely existed: requests came through scattered channels, there was no email on the company domain, and procurement managers and ship engineers could not find the supplier in search.&lt;/p&gt;

&lt;p&gt;We built the whole thing end to end on Next.js. A few notes on what a B2B parts catalog actually needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  A catalog of 12,400+ items in two languages
&lt;/h2&gt;

&lt;p&gt;The storefront is bilingual, Polish and English, with more than 12,400 items across six departments, plus delivery and company pages. Professional buyers do not browse like shoppers. They arrive with a part number or a very specific query, so the catalog has to be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fast on any device, including a phone in an engine room;&lt;/li&gt;
&lt;li&gt;indexable item by item, with structured data;&lt;/li&gt;
&lt;li&gt;correctly linked between languages with hreflang, so Google shows the Polish page to a buyer in Gdańsk and the English one to a buyer in Rotterdam.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  No cart, a quote request instead
&lt;/h2&gt;

&lt;p&gt;Marine parts are rarely bought with a card. Buyers need a price for their quantity, delivery terms and sometimes a certificate. So instead of a checkout every item leads to a quote request, and the form sends everything straight to the company inbox, including files and specs the buyer attaches.&lt;/p&gt;

&lt;p&gt;A simplified version of the route handler with Resend:&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;// app/api/quote/route.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Resend&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="s2"&gt;resend&lt;/span&gt;&lt;span class="dl"&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;resend&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;Resend&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;RESEND_API_KEY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&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;POST&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;Request&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;form&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;formData&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;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;files&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;File&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;attachments&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;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&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="mi"&gt;5&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="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&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;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;f&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="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Buffer&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&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="kd"&gt;const&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="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;resend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;emails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Quote form &amp;lt;noreply@example.com&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sales@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;replyTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Quote request: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;part&lt;/span&gt;&lt;span class="dl"&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="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;`Part: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;part&lt;/span&gt;&lt;span class="dl"&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="s2"&gt;`Quantity: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;qty&lt;/span&gt;&lt;span class="dl"&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="s2"&gt;`Company: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;company&lt;/span&gt;&lt;span class="dl"&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="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="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="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;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;attachments&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="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&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="mi"&gt;502&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;Setting &lt;code&gt;replyTo&lt;/code&gt; to the buyer's address sounds trivial, but it means the sales team answers with one click from their normal inbox, without copying addresses around.&lt;/p&gt;

&lt;h2&gt;
  
  
  Domain email
&lt;/h2&gt;

&lt;p&gt;The company also got corporate email on its own domain. For B2B this is not cosmetic: a quote from a free mailbox looks like a reseller, and some procurement departments may not take it seriously at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Content for professional queries
&lt;/h2&gt;

&lt;p&gt;On top of the catalog there is technical and on-page SEO (structured data, hreflang, speed) and a Knowledge Base with expert articles. Engineers search for answers to technical questions long before they search for a supplier, and those articles are how the company appears at that stage.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we took from it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Match the buying process.&lt;/strong&gt; In B2B a good quote form beats a cart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every item is a landing page.&lt;/strong&gt; With thousands of SKUs, structured data and hreflang do most of the SEO work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make replying effortless.&lt;/strong&gt; Requests in the normal inbox with attachments and a correct reply-to get answered faster.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full case: &lt;a href="https://buildbyalex.com/en/work/baltic-dockyard" rel="noopener noreferrer"&gt;Baltic Dockyard case study&lt;/a&gt;. If you need a catalog or company site like this, see &lt;a href="https://buildbyalex.com/en/services/websites" rel="noopener noreferrer"&gt;website development&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>seo</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Building an AI workout coach in SwiftUI: one-tap logging during the set, GPT-4o analysis after</title>
      <dc:creator>buildbyalex</dc:creator>
      <pubDate>Sun, 04 Oct 2026 08:57:41 +0000</pubDate>
      <link>https://dev.to/shurinbergo_5451b6dad08f8/building-an-ai-workout-coach-in-swiftui-one-tap-logging-during-the-set-gpt-4o-analysis-after-3126</link>
      <guid>https://dev.to/shurinbergo_5451b6dad08f8/building-an-ai-workout-coach-in-swiftui-one-tap-logging-during-the-set-gpt-4o-analysis-after-3126</guid>
      <description>&lt;p&gt;Most people who track their training in a notes app lose the one thing that matters: set-by-set context. The note does not remember what you lifted last week, and generic gym trackers bury the screen in charts without ever telling you what to do next.&lt;/p&gt;

&lt;p&gt;The brief for Body Forge was a coach in your pocket: clean tracking during the set, real analysis after it, and programmes that adapt as you progress. Here is how the native iOS app is put together.&lt;/p&gt;

&lt;h2&gt;
  
  
  During the set: show almost nothing
&lt;/h2&gt;

&lt;p&gt;The workout screen is built for one hand and a sweaty thumb. It shows only:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;last week's sets for this exercise;&lt;/li&gt;
&lt;li&gt;a rest timer;&lt;/li&gt;
&lt;li&gt;weight and reps;&lt;/li&gt;
&lt;li&gt;one tap to log the set.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything else waits until the session is over. Between sets nobody wants to read a graph.&lt;/p&gt;

&lt;h2&gt;
  
  
  After the session: let the model read the whole workout
&lt;/h2&gt;

&lt;p&gt;When the workout ends, the app sends a compact summary to GPT-4o: volume, intensity and progression compared with the last three weeks. The model writes a short, personal analysis with concrete changes for next time, for example: stalled on incline press three weeks in a row, deload to 80% next week.&lt;/p&gt;

&lt;p&gt;The trick is what you send. Raw logs are noisy, so the app builds a small structured summary first. Simplified:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;ExerciseSummary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Codable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;sets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;SetEntry&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;          &lt;span class="c1"&gt;// weight, reps, rest&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;topSetLast3Weeks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;Double&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;volumeChange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Double&lt;/span&gt;      &lt;span class="c1"&gt;// vs. the previous session&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;WorkoutSummary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Codable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Date&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;programme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;         &lt;span class="c1"&gt;// e.g. "Upper/Lower, week 4"&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;exercises&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;ExerciseSummary&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;avgHeartRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;        &lt;span class="c1"&gt;// from HealthKit, if available&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model gets numbers it can reason about, not a wall of text, and the answer comes back in about 10 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rest of the stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;SwiftUI on iOS 17 and later;&lt;/li&gt;
&lt;li&gt;Core Data locally with CloudKit sync, so history survives a new phone;&lt;/li&gt;
&lt;li&gt;HealthKit for heart rate during the session;&lt;/li&gt;
&lt;li&gt;a programme library: Upper/Lower split, full-body for beginners, custom blocks;&lt;/li&gt;
&lt;li&gt;more than 200 exercises with video form demos;&lt;/li&gt;
&lt;li&gt;Russian and English localisation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The landing page
&lt;/h2&gt;

&lt;p&gt;An App Store link does not explain why one tracker is better than a hundred others, and it does not rank in search. The app got its own bilingual site, bodyforges.com, in the same dark style with one goal per screen: go to the App Store. The first screen paints in about 0.7 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we took from it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Design the in-workout screen for the worst moment.&lt;/strong&gt; Tired, one hand, ten seconds of attention.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Summarise before you prompt.&lt;/strong&gt; A structured summary gives better analysis than raw logs, and it is cheaper.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make the advice concrete.&lt;/strong&gt; "Deload to 80% next week" is useful, "keep pushing" is not.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Body Forge is live in the App Store. Full case: &lt;a href="https://buildbyalex.com/en/work/body-forge" rel="noopener noreferrer"&gt;Body Forge case study&lt;/a&gt;. We build apps like this for small teams and founders, more on &lt;a href="https://buildbyalex.com/en/services/mobile-apps" rel="noopener noreferrer"&gt;mobile app development&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>swift</category>
      <category>ios</category>
      <category>ai</category>
      <category>mobile</category>
    </item>
    <item>
      <title>A RAG consultant for an immigration law firm: what it answers and what it hands to the lawyer</title>
      <dc:creator>buildbyalex</dc:creator>
      <pubDate>Sun, 04 Oct 2026 08:56:37 +0000</pubDate>
      <link>https://dev.to/shurinbergo_5451b6dad08f8/a-rag-consultant-for-an-immigration-law-firm-what-it-answers-and-what-it-hands-to-the-lawyer-1cdk</link>
      <guid>https://dev.to/shurinbergo_5451b6dad08f8/a-rag-consultant-for-an-immigration-law-firm-what-it-answers-and-what-it-hands-to-the-lawyer-1cdk</guid>
      <description>&lt;p&gt;An immigration lawyer in Warsaw was spending hours a day on the same questions. Which documents do I need for a karta pobytu? How long does it take? Her site was a brochure: it brought no leads from Google, and every first message was answered by hand.&lt;/p&gt;

&lt;p&gt;We rebuilt the site and put a consultant inside it that answers from the firm's own material. Here is the shape of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The site first
&lt;/h2&gt;

&lt;p&gt;The assistant only helps if people reach the site, so the groundwork came before any AI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js 15 with proper localized routing in Russian, Polish and Ukrainian, because most clients do not read Polish comfortably;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LegalService&lt;/code&gt; structured data;&lt;/li&gt;
&lt;li&gt;separate pages for commercial queries such as "karta pobytu Warszawa" or "pobyt stały Polska", instead of one page that tries to cover everything.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The consultant: retrieval before generation
&lt;/h2&gt;

&lt;p&gt;The assistant runs on GPT-4o with retrieval. The knowledge base is built from more than 200 real cases the firm has handled and from official documents published by the offices that issue the decisions. Chunks are stored as embeddings in Postgres with pgvector.&lt;/p&gt;

&lt;p&gt;The flow for every question is short:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;embed the question;&lt;/li&gt;
&lt;li&gt;pull the closest chunks from pgvector;&lt;/li&gt;
&lt;li&gt;answer only from those chunks, in the language the client wrote in;&lt;/li&gt;
&lt;li&gt;if the chunks do not cover the question, say so and offer a consultation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A simplified version of the retrieval query:&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;select&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;source&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;kb_chunks&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;locale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;2&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;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;limit&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the core of the instruction the model gets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Answer using only the context below. If the context does not contain the answer,
say that this needs an individual consultation and ask for the client's contact.
Never guess deadlines, fees or document lists.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last line matters most. In immigration law a confident wrong answer about a deadline costs a client far more than "please ask the lawyer".&lt;/p&gt;

&lt;h2&gt;
  
  
  Handing over the lead
&lt;/h2&gt;

&lt;p&gt;The assistant does not try to replace the consultation. Once it has answered the standard part, it asks for contact details, country and the gist of the case, and sends a ready request to the lawyer's Telegram. She opens one message and already knows whether this is a residence card refusal, citizenship or family reunification.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The site ranks in Google's top 10 for more than 15 commercial queries in Poland.&lt;/li&gt;
&lt;li&gt;The assistant handles about 80% of first contacts on its own.&lt;/li&gt;
&lt;li&gt;A client gets an answer in seconds instead of waiting hours.&lt;/li&gt;
&lt;li&gt;The site shows the firm's real rating: 4.9 stars from 312 clients.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An unexpected bonus: ChatGPT now names the site when people ask it about immigration help in Warsaw. Clear service pages and structured data seem to help there as well as in Google.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we would tell anyone building this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The knowledge base is the product.&lt;/strong&gt; Real cases and official documents beat any clever prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Teach the bot to say "I don't know".&lt;/strong&gt; For legal, medical or financial topics, a refusal with a handover is a feature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send the lawyer a summary, not a transcript.&lt;/strong&gt; Contact, country, topic and the question in two lines.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full case: &lt;a href="https://buildbyalex.com/en/work/legalwin" rel="noopener noreferrer"&gt;LegalWin case study&lt;/a&gt;. More on how we build assistants like this: &lt;a href="https://buildbyalex.com/en/services/ai-chatbot" rel="noopener noreferrer"&gt;AI chatbot development&lt;/a&gt; and our notes on &lt;a href="https://buildbyalex.com/en/blog/ai-chatbot-with-knowledge-base" rel="noopener noreferrer"&gt;chatbots that answer from your own data&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>postgres</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Lead forms that reach the phone in under 2 seconds: Next.js + Telegram for a small business</title>
      <dc:creator>buildbyalex</dc:creator>
      <pubDate>Sat, 03 Oct 2026 09:12:23 +0000</pubDate>
      <link>https://dev.to/shurinbergo_5451b6dad08f8/lead-forms-that-reach-the-phone-in-under-2-seconds-nextjs-telegram-for-a-small-business-5eap</link>
      <guid>https://dev.to/shurinbergo_5451b6dad08f8/lead-forms-that-reach-the-phone-in-under-2-seconds-nextjs-telegram-for-a-small-business-5eap</guid>
      <description>&lt;p&gt;A drone operator in Warsaw had a steady flow of enquiries, but they went cold. By the time he replied, the client had already written to someone else. His old site took around 4 seconds to load and did not rank for any of his seven services.&lt;/p&gt;

&lt;p&gt;We rebuilt it on Next.js with two goals: get found for each service separately, and get every request onto his phone before the client opens another tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  One page per service
&lt;/h2&gt;

&lt;p&gt;He shoots weddings, real estate, infrastructure inspections, advertising, events, sports and documentation. Each of those got its own page with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;its own schema markup;&lt;/li&gt;
&lt;li&gt;real photos from that type of shoot;&lt;/li&gt;
&lt;li&gt;a starting price;&lt;/li&gt;
&lt;li&gt;an FAQ written from the questions clients actually ask.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;People search for "drone wedding video Warsaw" or "roof inspection drone", not for "drone services". A single generic page cannot rank for seven different intents, seven focused pages can. Starting prices also filter out people who would never book, so the requests that do come in are warmer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The form writes straight to Telegram
&lt;/h2&gt;

&lt;p&gt;No CRM, no shared inbox. The contact form posts to a Next.js route handler, which checks a rate limit in Upstash Redis and sends the request to the operator's Telegram through the Bot API. A simplified version:&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;// app/api/lead/route.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Ratelimit&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="s2"&gt;@upstash/ratelimit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Redis&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="s2"&gt;@upstash/redis&lt;/span&gt;&lt;span class="dl"&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;ratelimit&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;Ratelimit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEnv&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;limiter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Ratelimit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slidingWindow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;10 m&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;export&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;POST&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;Request&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;ip&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;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-forwarded-for&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&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="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;success&lt;/span&gt; &lt;span class="p"&gt;}&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;ratelimit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`lead:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ip&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="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;success&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="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&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;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;429&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="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;service&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="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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;contact&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&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="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2000&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="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&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;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&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;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s2"&gt;`New request: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;service&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="s2"&gt;`Name: &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="s2"&gt;`Contact: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;contact&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="dl"&gt;""&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="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&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;res&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`https://api.telegram.org/bot&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;TG_TOKEN&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/sendMessage`&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;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&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="s2"&gt;application/json&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="na"&gt;chat_id&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;TG_CHAT_ID&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="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="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&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;ok&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&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;ok&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="mi"&gt;502&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two small details that save trouble. Send plain text without &lt;code&gt;parse_mode&lt;/code&gt;, otherwise one underscore in a client's email breaks Markdown parsing and the message never arrives. And return an error to the form when Telegram fails, so the visitor sees a fallback contact instead of a fake "thank you".&lt;/p&gt;

&lt;h2&gt;
  
  
  Indexing on every deploy
&lt;/h2&gt;

&lt;p&gt;New service pages should not wait a week for a crawler. On each deploy the build sends changed URLs to IndexNow, which Bing and other participating search engines pick up:&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;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.indexnow.org/indexnow&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;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;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&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="s2"&gt;application/json&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="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;key&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;INDEXNOW_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;urlList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;changedUrls&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;
  
  
  Results
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;LCP under 1 second on all seven service pages.&lt;/li&gt;
&lt;li&gt;A request reaches the operator's Telegram in under 2 seconds.&lt;/li&gt;
&lt;li&gt;New pages get indexed in hours rather than days.&lt;/li&gt;
&lt;li&gt;Visitor-to-lead conversion went from 0.8% on the old site to 4.2%.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What we took from it
&lt;/h2&gt;

&lt;p&gt;For a small service business the fastest reply usually wins the job, and a form that lands in a messenger the owner already checks all day beats any CRM he will not open. Separate pages with real prices bring both search traffic and better-qualified requests.&lt;/p&gt;

&lt;p&gt;The full case is on our site: &lt;a href="https://buildbyalex.com/en/work/visionair" rel="noopener noreferrer"&gt;VisionAir case study&lt;/a&gt;. If you need a site like this for your own business, see &lt;a href="https://buildbyalex.com/en/services/websites" rel="noopener noreferrer"&gt;website development&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>typescript</category>
      <category>seo</category>
    </item>
    <item>
      <title>An LLM agent that qualifies B2B leads from AmoCRM: notes from a production build</title>
      <dc:creator>buildbyalex</dc:creator>
      <pubDate>Sat, 03 Oct 2026 09:10:58 +0000</pubDate>
      <link>https://dev.to/shurinbergo_5451b6dad08f8/an-llm-agent-that-qualifies-b2b-leads-from-amocrm-notes-from-a-production-build-57kf</link>
      <guid>https://dev.to/shurinbergo_5451b6dad08f8/an-llm-agent-that-qualifies-b2b-leads-from-amocrm-notes-from-a-production-build-57kf</guid>
      <description>&lt;p&gt;A B2B client came to us with 50 to 80 inbound leads a day and one first-touch sales manager. About six hours of that person's day went to the same questions: what are your prices, can you send the deck, do you work with our volume. Filtering was uneven, and some leads never got a reply at all.&lt;/p&gt;

&lt;p&gt;We replaced that first touch with an LLM agent. Here is how it is put together and what we would keep if we built it again tomorrow.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the agent does
&lt;/h2&gt;

&lt;p&gt;Every morning it pulls new leads from AmoCRM through API v4. For each lead it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;classifies the request type, for example a price question or a request for materials;&lt;/li&gt;
&lt;li&gt;opens a conversation in the channel the lead came from: Telegram, or WhatsApp through Wappi;&lt;/li&gt;
&lt;li&gt;qualifies the lead on six criteria, among them volume, budget, timing and fit with the ideal customer profile;&lt;/li&gt;
&lt;li&gt;moves the deal to the right pipeline stage and tags it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;An admin panel shows every dialog, and a manager can step into any conversation at any moment. That part matters more than it sounds. The sales team trusted the bot because they knew they could take the wheel whenever they wanted.&lt;/p&gt;

&lt;p&gt;The stack is boring on purpose: TypeScript, Node.js with Express, Gemini 2.0 Flash as the model, Firestore for conversation state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Function calling instead of free text
&lt;/h2&gt;

&lt;p&gt;The model never writes to the CRM in prose. It can only call a short list of functions, and the backend validates every argument before anything touches AmoCRM. A simplified version of the qualification call looks like this:&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;qualifyLead&lt;/span&gt; &lt;span class="o"&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="s2"&gt;qualify_lead&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Save qualification answers once the lead has given them&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;leadId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;monthlyVolume&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;small&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="s2"&gt;medium&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="s2"&gt;large&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="s2"&gt;unknown&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;budgetConfirmed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;boolean&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;timing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;now&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="s2"&gt;this_quarter&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="s2"&gt;later&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="s2"&gt;unknown&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;icpFit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;boolean&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;leadId&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="s2"&gt;monthlyVolume&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="s2"&gt;budgetConfirmed&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="s2"&gt;timing&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="s2"&gt;icpFit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enums do a lot of the work here. When the model has to pick &lt;code&gt;unknown&lt;/code&gt; instead of making up a number, the CRM stays clean, and the sales team can filter on those fields without reading transcripts.&lt;/p&gt;

&lt;p&gt;The rest of the toolbox follows the same idea: move a deal between stages, add a tag, send the deck or price list, hand the conversation to a human. Anything outside that list the agent simply cannot do. We wrote a longer piece on &lt;a href="https://buildbyalex.com/en/blog/ai-agent-permissions-in-crm-and-erp" rel="noopener noreferrer"&gt;where to put approval gates when an agent has CRM or ERP access&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed after launch
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Qualifying a lead takes about 8 minutes instead of 40.&lt;/li&gt;
&lt;li&gt;Every inbound lead gets an answer, including the ones that arrive at night or on weekends.&lt;/li&gt;
&lt;li&gt;The share of leads that reach a sales conversation went up 31%. That came from consistency alone: the same questions, asked every time, with nothing forgotten.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The manager who used to do first touch now works only with leads that are already qualified.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you are building something similar
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Start from the CRM fields, not the prompt.&lt;/strong&gt; Decide what a qualified lead looks like as data, then make the model fill exactly those fields.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the tool list short.&lt;/strong&gt; Every extra function is one more way for the agent to do damage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Give people a visible override.&lt;/strong&gt; The admin panel was one of the cheapest parts of the project, and it is the reason the team accepted the bot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure business numbers.&lt;/strong&gt; Time to first reply and the share of leads that reach a sales call tell you more than any accuracy score.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full case with screenshots is here: &lt;a href="https://buildbyalex.com/en/work/crm-bot" rel="noopener noreferrer"&gt;CRM Bot case study&lt;/a&gt;. We build agents like this for small and mid-size companies in Poland and the EU, more on the &lt;a href="https://buildbyalex.com/en/services/ai-agents" rel="noopener noreferrer"&gt;AI agents page&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>typescript</category>
      <category>automation</category>
    </item>
    <item>
      <title>AI agent permissions in CRM and ERP: where to put the approval gate</title>
      <dc:creator>buildbyalex</dc:creator>
      <pubDate>Fri, 02 Oct 2026 14:30:58 +0000</pubDate>
      <link>https://dev.to/shurinbergo_5451b6dad08f8/ai-agent-permissions-in-crm-and-erp-where-to-put-the-approval-gate-308e</link>
      <guid>https://dev.to/shurinbergo_5451b6dad08f8/ai-agent-permissions-in-crm-and-erp-where-to-put-the-approval-gate-308e</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://buildbyalex.com/en/blog/ai-agent-permissions-in-crm-and-erp" rel="noopener noreferrer"&gt;buildbyalex.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The hardest question in an AI agent project is not "which model" or "how much". It is &lt;strong&gt;what this agent may do in our CRM without asking anyone&lt;/strong&gt;. While the agent only reads and answers, the risk is small. The day it gets write access, everything changes at once: the price of the build, the scope of testing, and the list of things that can go wrong quietly.&lt;/p&gt;

&lt;p&gt;Below is what I put into projects: a permission matrix, rules for the approval gate, the audit trail, and a rollback plan. No governance theory, just fields and numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The moment the agent stops reading and starts writing
&lt;/h2&gt;

&lt;p&gt;Published Polish price bands show this threshold better than any explanation. In vendor price lists, an agent answering from a knowledge base and an agent writing into a system are two different products.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Agent scope&lt;/th&gt;
&lt;th&gt;Bands published by Polish vendors&lt;/th&gt;
&lt;th&gt;Running cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Simple assistant on an off-the-shelf platform&lt;/td&gt;
&lt;td&gt;€700-3,500 (3,000-15,000 zł)&lt;/td&gt;
&lt;td&gt;€120-580/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Answers from a knowledge base, only reads your systems&lt;/td&gt;
&lt;td&gt;€4,600-14,000 (20,000-60,000 zł)&lt;/td&gt;
&lt;td&gt;€470-1,900/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reads and writes data in CRM, ERP or a logistics system&lt;/td&gt;
&lt;td&gt;€18,500-58,000 (80,000-250,000 zł)&lt;/td&gt;
&lt;td&gt;€1,900-9,300/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The jump is not the model. Write access forces someone to design permissions, handle failures, plan how a change gets reverted and collect logs that survive a conversation with an auditor.&lt;/p&gt;

&lt;p&gt;If your case is "I want customers to get answers from our documentation", stay in the top rows, which is &lt;a href="https://buildbyalex.com/en/blog/ai-chatbot-with-knowledge-base" rel="noopener noreferrer"&gt;chatbot with a knowledge base&lt;/a&gt; territory. Write access earns its keep only when someone retypes the same data by hand dozens of times a week.&lt;/p&gt;

&lt;h2&gt;
  
  
  The permission matrix: one table the owner signs
&lt;/h2&gt;

&lt;p&gt;Every &lt;a href="https://buildbyalex.com/en/services/ai-agents" rel="noopener noreferrer"&gt;AI agent&lt;/a&gt; build that touches a live system starts with this document. One page, five columns, signed off before the first line of code.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action in CRM or ERP&lt;/th&gt;
&lt;th&gt;Risk&lt;/th&gt;
&lt;th&gt;Agent permission&lt;/th&gt;
&lt;th&gt;Approval gate&lt;/th&gt;
&lt;th&gt;What goes to the log&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Read a customer record and its history&lt;/td&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;td&gt;read&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;record id, query, timestamp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add a note to a case&lt;/td&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;td&gt;write&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;note text, author "agent"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create a lead from an email or form&lt;/td&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;td&gt;write&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;source, fields filled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Move a deal to another pipeline stage&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;write&lt;/td&gt;
&lt;td&gt;yes, above €4,600 (20,000 zł) deal value&lt;/td&gt;
&lt;td&gt;old stage, new stage, reason&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email a customer from a company address&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;write and send&lt;/td&gt;
&lt;td&gt;yes, on first contact&lt;/td&gt;
&lt;td&gt;full body, recipient, attachments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Draft a quote or proposal&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;draft only&lt;/td&gt;
&lt;td&gt;always&lt;/td&gt;
&lt;td&gt;version, prices, discount, margin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change price, discount or payment terms&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;no write, proposal only&lt;/td&gt;
&lt;td&gt;always, manual confirmation&lt;/td&gt;
&lt;td&gt;proposed value, reasoning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Issue an invoice and file it with the tax system&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;no write&lt;/td&gt;
&lt;td&gt;always, a human clicks&lt;/td&gt;
&lt;td&gt;full document payload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change stock levels or reserve goods&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;write within a quantity cap&lt;/td&gt;
&lt;td&gt;above the cap&lt;/td&gt;
&lt;td&gt;SKU, delta, warehouse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delete a record, merge counterparties&lt;/td&gt;
&lt;td&gt;critical&lt;/td&gt;
&lt;td&gt;forbidden&lt;/td&gt;
&lt;td&gt;not applicable&lt;/td&gt;
&lt;td&gt;the attempt and its rejection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Export a customer list or personal data&lt;/td&gt;
&lt;td&gt;critical&lt;/td&gt;
&lt;td&gt;forbidden&lt;/td&gt;
&lt;td&gt;not applicable&lt;/td&gt;
&lt;td&gt;the attempt and its rejection&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three rules keep this table honest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Default zero.&lt;/strong&gt; No permission exists until a row is added. Never the other way round.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One row is one action, not a module.&lt;/strong&gt; "Access to the sales module" means nothing. "Move a deal to another stage" means something.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The critical tier stays empty.&lt;/strong&gt; Deletion, merging and personal-data export are out of scope. A request for an exception usually means the process needs fixing instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A service account, not an employee login
&lt;/h2&gt;

&lt;p&gt;The most common pilot shortcut: the agent gets a salesperson's login because it is faster. Three problems at once. The agent inherits every permission that person has, including forgotten ones. Record history shows the employee's name, so two months later nobody can tell their edits from the model's. And when that person leaves and the account is disabled, the agent stops mid-day.&lt;/p&gt;

&lt;p&gt;The correct version is a separate service account with its own display name, API key, role set and rate limit. In HubSpot, Pipedrive, Zoho or a Comarch-class ERP that is an hour of work. Every record then shows the agent as the author, and revoking access touches nobody on the team.&lt;/p&gt;

&lt;h2&gt;
  
  
  The approval gate: what reaches a human, and how fast
&lt;/h2&gt;

&lt;p&gt;An approval gate is not an "are you sure" dialog. It is a process step with a named recipient and a response time. Without those two it turns into a queue nobody reads.&lt;/p&gt;

&lt;p&gt;Here is how I build it. The agent prepares the full action, saves it as a draft and sends one message to Slack or email: what it wants to do, on which record, on what basis, plus two buttons. If nobody answers in the agreed window, the action expires and the case moves to the manual list. It never fires on a default yes.&lt;/p&gt;

&lt;p&gt;Windows that work in a small company: quotes within 4 working hours, first email to a new customer within 1 hour, above-threshold stage changes by end of day. For an &lt;a href="https://buildbyalex.com/en/blog/ai-agent-for-booking" rel="noopener noreferrer"&gt;agent that books appointments&lt;/a&gt; the line sits elsewhere: a free slot in normal hours it takes on its own, moving someone else's booking or an out-of-hours slot goes to a human.&lt;/p&gt;

&lt;h2&gt;
  
  
  How many cases really go to review
&lt;/h2&gt;

&lt;p&gt;This question decides the budget, not the safety story. Builders running agents in production report that &lt;strong&gt;5-15% of cases need a human&lt;/strong&gt;, and that this is a permanent operating cost, not a defect to be engineered away. On top of that sits 100% of high-risk actions, which pass the gate by definition.&lt;/p&gt;

&lt;p&gt;The arithmetic for a distribution company handling 400 cases a month:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;exception review: 400 × 10% = 40 cases at 3 minutes, so 2 hours a month&lt;/li&gt;
&lt;li&gt;quote approvals: 60 quotes × 4 minutes, so 4 hours a month&lt;/li&gt;
&lt;li&gt;first-contact emails: 80 × 1 minute, roughly 1.5 hours&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Just under 8 hours a month for one person, against the dozens of hours of manual data entry it takes off the team. Run this before the build: if it comes out at 40 hours of review, a rule-based automation is the better product.&lt;/p&gt;

&lt;p&gt;Reviewing everything equally fails too. Somebody clicking "approve" for the fiftieth time stops reading within a week. The gate belongs where the matrix says "high"; the rest runs on its own and lands in the log.&lt;/p&gt;

&lt;h2&gt;
  
  
  Narrowing the blast radius: filters, caps and an allow-list of fields
&lt;/h2&gt;

&lt;p&gt;"Write access to the CRM" is far too broad. I narrow it along four axes, set in configuration rather than in the prompt.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;By record.&lt;/strong&gt; The agent sees only cases in its own queue, not the whole database. Key accounts stay out of reach.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;By field.&lt;/strong&gt; An allow-list of writable fields, everything else read-only. Tax ID, owner details, contract terms and accounting fields never make the list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;By amount and quantity.&lt;/strong&gt; Above €4,600 (20,000 zł) of deal value or a fixed unit cap, the agent can only propose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;By status.&lt;/strong&gt; Closed, invoiced and disputed cases are locked entirely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those limits belong in the API and the permission model, not in the system prompt: a prompt can be bypassed with one carefully written customer email. OWASP's 2026 agent security work puts prompt injection at the centre of agentic risk and reports attacks up 340% year on year, with documented findings against Slack AI, Microsoft 365 Copilot and GitHub MCP. &lt;strong&gt;What the agent has no right to do in the API must stay impossible regardless of what it reads.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The audit trail: what has to survive every agent action
&lt;/h2&gt;

&lt;p&gt;Agents rarely fail loudly. The typical failure is the agent calling the right tool with plausible but wrong parameters, and nobody noticing for a week. OWASP puts mean monitoring coverage across production agent systems at 52%, so close to half of deployed agents are not observed.&lt;/p&gt;

&lt;p&gt;The minimum log record for reconstructing an event:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Example content&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Timestamp and session id&lt;/td&gt;
&lt;td&gt;2026-09-02 11:42, session 8f31&lt;/td&gt;
&lt;td&gt;ties the action to a conversation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who initiated it&lt;/td&gt;
&lt;td&gt;customer, employee, schedule&lt;/td&gt;
&lt;td&gt;accountability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool called and parameters&lt;/td&gt;
&lt;td&gt;crm.update_deal, id 4127, stage=won&lt;/td&gt;
&lt;td&gt;what actually hit the system&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt and model version&lt;/td&gt;
&lt;td&gt;v14, gpt-5.1&lt;/td&gt;
&lt;td&gt;reproducibility after a model upgrade&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State before and after&lt;/td&gt;
&lt;td&gt;stage: was negotiation, now won&lt;/td&gt;
&lt;td&gt;rollback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gate outcome&lt;/td&gt;
&lt;td&gt;approved by Mark K., 11:48&lt;/td&gt;
&lt;td&gt;proof of human oversight&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A legal note, with the disclaimer: I am a developer, not a lawyer. A company running an AI system under its own name is a deployer under the EU AI Act, so the obligations sit with it, not only the contractor. Poland now has a national supervisor too: the AI Development and Safety Commission gains powers to inspect, run proceedings and impose fines &lt;strong&gt;from 28 October 2026&lt;/strong&gt;. The agent's action log is the only thing you can put on the table there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rollback plan: cutting the agent off in a minute
&lt;/h2&gt;

&lt;p&gt;The part that usually falls out of scope and hurts most when missing. Three levels, tested before launch, not after an incident:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pause.&lt;/strong&gt; One switch stops writes, reads keep running. Available to the owner, not only the contractor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cut-off.&lt;/strong&gt; Revoke the service account's API key: seconds in the CRM panel, immediate effect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revert.&lt;/strong&gt; A query over the log for the agent's actions in the last N hours, with the pre-change state, restorable in batches. Without it you read record history one row at a time.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Gartner expects over 40% of agentic AI projects to be cancelled by the end of 2027, and reports that 89% of agent pilots never reach production. Many failures look exactly like this: the pilot worked, then it did something stupid, nobody could say quickly what and how many times, so the project was shut down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to start
&lt;/h2&gt;

&lt;p&gt;The sequence that works: two weeks read-only, then low-risk writes, then one medium-risk action per week. High risk stays behind the gate permanently.&lt;/p&gt;

&lt;p&gt;If you do not know which processes are even suitable for agent write access, start with an &lt;a href="https://buildbyalex.com/en/services/ai-audit" rel="noopener noreferrer"&gt;AI audit&lt;/a&gt; at 4,900 zł (about €1,140): you get the permission matrix and a build quote, sometimes the answer that a plain automation is enough. A sales agent with CRM integration starts at €2,500 with me, a multi-tool agent at €4,500. What that looks like from the business side I covered in &lt;a href="https://buildbyalex.com/en/blog/ai-agent-for-business-gdansk" rel="noopener noreferrer"&gt;deployments in Gdańsk&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Got a specific CRM or ERP process and want to know where the gate belongs? &lt;a href="https://buildbyalex.com/en/contact" rel="noopener noreferrer"&gt;Write to me&lt;/a&gt; and we will go through the matrix for it.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;What permissions should an AI agent have in a CRM to start with?&lt;/strong&gt;&lt;br&gt;
Read access plus low-risk writes only: a note on a case, a lead from an email, updated contact details. Moving deals, emailing customers and anything touching price come later, behind an approval gate. Deleting records, merging counterparties and exporting personal data stay switched off permanently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which agent actions must always go through human approval?&lt;/strong&gt;&lt;br&gt;
Anything that changes money or creates an obligation to a customer: quotes, changes to price, discount or payment terms, issuing an invoice and filing it with the tax system, stock changes above a cap. Add the first email to a new customer and any deal change above a value threshold, say €4,600 (20,000 zł). The rest can run automatically as long as it lands in the log.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How many cases does an agent send to human review?&lt;/strong&gt;&lt;br&gt;
Builders running agents in production report 5-15% of cases as a standing operating cost, regardless of model quality. On top of that, 100% of high-risk actions pass the gate by definition. At 400 cases a month that is 6-8 hours of one person's time. If your calculation lands in the dozens of hours, a rule-based automation is the better answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What has to be logged on every agent action?&lt;/strong&gt;&lt;br&gt;
Six items: timestamp and session id, who initiated the action, the tool called with its parameters, the prompt and model version, the record state before and after, and the gate outcome with the name of the person who approved it. That set is enough to reconstruct an event and revert changes in batches. OWASP puts mean monitoring coverage across production agent systems at 52%, so the default state of the market is a log that is not good enough for this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much does an AI agent with CRM or ERP write access cost?&lt;/strong&gt;&lt;br&gt;
Polish vendors publish €4,600-14,000 (20,000-60,000 zł) for an agent answering from a knowledge base, and €18,500-58,000 (80,000-250,000 zł) setup plus €1,900-9,300 a month for one that reads and writes data in CRM, ERP or logistics. The jump comes from permission design, failure handling and logging, not from the model. With me a sales agent with CRM integration starts at €2,500 and a multi-tool agent at €4,500.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is prompt injection a real threat to an agent with systems access?&lt;/strong&gt;&lt;br&gt;
Yes, and that is why limits belong in API permissions rather than in the model's instructions. OWASP's 2026 report puts prompt injection at the centre of agentic risk and reports attacks up 340% year on year, with documented findings against Slack AI, Microsoft 365 Copilot and GitHub MCP. One well-crafted customer email can make an agent attempt something outside its script. If the action is not in the service account's roles, the attempt ends in a rejection and a log entry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How fast can an agent be cut off from a system?&lt;/strong&gt;&lt;br&gt;
On three levels. Pausing writes is a single switch that leaves the agent read-only. Revoking the service account's API key takes seconds in the CRM panel and applies immediately. Reverting is a query over the log for the agent's recent actions, with the pre-change state. Test all three before launch, not after an incident.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>architecture</category>
      <category>security</category>
    </item>
    <item>
      <title>AI chatbot with a knowledge base for your site: answers from your own data, no made-up replies</title>
      <dc:creator>buildbyalex</dc:creator>
      <pubDate>Thu, 01 Oct 2026 17:28:01 +0000</pubDate>
      <link>https://dev.to/shurinbergo_5451b6dad08f8/ai-chatbot-with-a-knowledge-base-for-your-site-answers-from-your-own-data-no-made-up-replies-1752</link>
      <guid>https://dev.to/shurinbergo_5451b6dad08f8/ai-chatbot-with-a-knowledge-base-for-your-site-answers-from-your-own-data-no-made-up-replies-1752</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://buildbyalex.com/en/blog/ai-chatbot-with-knowledge-base" rel="noopener noreferrer"&gt;buildbyalex.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The main worry about putting an AI bot on your site is simple: "what if it starts making things up and tells a client nonsense in the company's name." That worry is fair - a plain language model really can confidently invent something that doesn't exist. But a chatbot with a knowledge base works differently: it answers from your documents, not off the top of its head, and it says "I don't know" when there's no answer there. I build these bots, and I'll explain how it works, how it differs from a regular bot and what it costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a regular bot makes things up and this one doesn't
&lt;/h2&gt;

&lt;p&gt;The difference is in where the answer comes from. A regular chatbot running on a bare language model generates its reply from everything it "knows" in general - and it can confidently invent a fact, a price or a condition that you don't actually have. For a business that's dangerous: the bot promised a client a discount, in your name, that doesn't exist.&lt;/p&gt;

&lt;p&gt;A chatbot with a knowledge base works on a "find first, then answer" principle. When a question comes in, the system first searches your materials - on the site, in documents, price lists, manuals - for the relevant passage, and only then builds an answer strictly from what it found. If there's nothing in the base, the bot honestly says it doesn't know and offers to connect you with a person instead of inventing something.&lt;/p&gt;

&lt;p&gt;This approach is called RAG - generation grounded in your data. For a business the point is one thing: the bot answers from your facts and doesn't fantasise. That's the difference between "putting AI on the site feels risky" and "we can do it with peace of mind."&lt;/p&gt;

&lt;h2&gt;
  
  
  What a knowledge base is and what it's built from
&lt;/h2&gt;

&lt;p&gt;A knowledge base is everything the bot needs to know about your business, organised so it can find things fast. It's built from what you already have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Site content&lt;/strong&gt; - services, prices, terms, descriptions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documents&lt;/strong&gt; - price lists, manuals, policies, contracts, regulations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frequent questions&lt;/strong&gt; - what clients ask most often, with ready answers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Past enquiries&lt;/strong&gt; - real support conversations, if you have them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fuller and more accurate the materials, the better the bot answers. A key plus: the base is easy to update. Prices or terms change - you update the document, and the bot answers the new way right away, with no retraining or rewriting. That makes it a living tool, not a one-off setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this bot does on your site
&lt;/h2&gt;

&lt;p&gt;The concrete value it's put there for.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Answers client questions instantly and around the clock&lt;/strong&gt; - from your data, at any time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Takes load off support&lt;/strong&gt; - it handles routine questions about prices, terms, availability and delivery on its own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Helps people choose&lt;/strong&gt; - it suggests the right service or product based on what the person needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collects leads&lt;/strong&gt; - if a client is ready, the bot takes their contact and passes it to CRM or a manager.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hands off to a person when needed&lt;/strong&gt; - a complex or unusual question goes to a live employee rather than getting lost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Works beyond the site&lt;/strong&gt; - the same knowledge base can be connected to a bot in Telegram, WhatsApp, or to support.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The effect: the client gets an answer right away instead of waiting, and some people make it all the way to a lead inside the conversation, without leaving to think it over.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it's especially useful
&lt;/h2&gt;

&lt;p&gt;A bot with a knowledge base pays off where clients ask a lot of similar questions and where accuracy matters.&lt;/p&gt;

&lt;p&gt;That's online stores with a wide range and questions about products and delivery, service companies with terms and tariffs, clinics and services with questions about procedures and prices, B2B with documentation and regulations, and any business where support drowns in repeating questions. It's especially useful where answers have to be accurate - prices, terms, rules - and where a mistake or a made-up reply is unacceptable. That's exactly why relying on a knowledge base, not on the model's imagination, is critical here.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it's added to your site
&lt;/h2&gt;

&lt;p&gt;Technically it's a few steps. First we gather your materials and tidy them into a knowledge base. Then we set up search across it and the answer logic - so the bot finds what's relevant and answers strictly from it. Then we connect the widget to the site and, if needed, to messengers, tune the tone to your brand and set the rules for handing off to a person. And we link it to CRM if the bot is meant to collect leads.&lt;/p&gt;

&lt;p&gt;I set this up so the bot fits into your site and your processes rather than living off to the side. And I always set the boundaries: where the bot answers on its own, and where it honestly says "let me check with a specialist" and passes the question to a person. More on the approach is in the &lt;a href="https://buildbyalex.com/en/services/ai-agents" rel="noopener noreferrer"&gt;AI agent implementation service&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;The price depends on the size of the knowledge base and the complexity of the scenarios.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Basic bot with a knowledge base&lt;/strong&gt; - 1,000-3,000 €. Building the base from the site and documents, answering questions, a site widget, handoff to a person.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extended&lt;/strong&gt; - 3,000-7,000 €. A larger base, several channels, lead collection into CRM, product or service recommendations, finer logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complex implementation&lt;/strong&gt; - from 7,000 €. A wide range or heavy documentation, strict accuracy and data requirements.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plus a moderate monthly fee for the model's work based on the volume of enquiries, which is clear in advance. The payback is measured through saved support time and leads collected in conversation. For a business with a stream of similar questions, the bot usually pays for itself by taking load off people and by clients getting an answer right away instead of leaving.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Will the chatbot make up answers?&lt;/strong&gt;&lt;br&gt;
No, not if it's a bot with a knowledge base. It answers not from the model's general knowledge but only from your materials: first it finds the passage relevant to the question in your base, then it builds an answer from it. If there's no answer in the base, the bot honestly says it doesn't know and offers to connect you with a person rather than inventing something. That's the key difference from a regular bot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How is a bot with a knowledge base different from a regular chatbot?&lt;/strong&gt;&lt;br&gt;
A regular bot on a bare model generates its reply from what it knows in general and can confidently invent facts, prices or terms that don't exist. A bot with a knowledge base works on a "find it in your data first, then answer" principle and replies strictly from your materials. For a business that's the difference between the risk of a bot over-promising and peace of mind.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where does the bot get its information?&lt;/strong&gt;&lt;br&gt;
From a knowledge base built from your materials: site content, price lists, documents, manuals, frequent questions and real support conversations, if you have them. The base is easy to update - prices or terms change, you update the document, and the bot answers the new way right away, with no retraining. The fuller the materials, the more accurate the answers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much does a chatbot with a knowledge base cost?&lt;/strong&gt;&lt;br&gt;
A basic bot with the base built from your site and documents, answers and a widget - 1,000-3,000 €. An extended one with a large base, several channels and lead collection into CRM - 3,000-7,000 €. A complex implementation - from 7,000 €. Plus a moderate monthly fee for the model's work based on the volume of enquiries. For a business with a stream of similar questions, the bot usually pays for itself by taking load off support.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can this bot be put somewhere other than the site?&lt;/strong&gt;&lt;br&gt;
Yes. The same knowledge base can be connected to a bot on the site, in Telegram, WhatsApp and in support - the bot will answer just as accurately across all channels and collect leads in one place, for example in CRM. That's handy when clients write to you in different places: one knowledge base, several entry points.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>chatbot</category>
      <category>rag</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
