[{"data":1,"prerenderedAt":2845},["ShallowReactive",2],{"header-latest":3,"post-\u002Fblog\u002Ftelegram-bot-ai-rag-support":1372},[4],{"id":5,"title":6,"body":7,"date":1346,"description":1347,"extension":1348,"faq":1349,"image":1359,"lang":1360,"meta":1361,"navigation":108,"path":1362,"published":108,"readTime":215,"seo":1363,"stem":1364,"tags":1365,"updated":1346,"__hash__":1371},"blog\u002Fblog\u002Fen\u002Ftelegram-web-apps-sells.md","Telegram Web Apps (TWA): How to Launch a Full SaaS Inside Telegram",{"type":8,"value":9,"toc":1335},"minimark",[10,19,22,25,30,33,44,52,63,70,73,856,860,863,868,875,923,927,930,1215,1219,1229,1232,1270,1283,1291,1295,1315,1318,1331],[11,12,13,14,18],"p",{},"Telegram has transcended its origins as a privacy-focused messaging client to become a powerful, cross-platform runtime environment. The introduction of ",[15,16,17],"strong",{},"Telegram Web Apps (TWA)",", officially known as Telegram Mini Apps, allows developers to build highly interactive single-page applications (SPAs) that load directly inside the Telegram application shell.",[11,20,21],{},"This provides startups and enterprises with immediate, frictionless access to over 900 million active users. By embedding your B2B SaaS, CRM interface, or utility tool inside a Telegram bot, you remove the classic conversion hurdles of app store downloads, desktop logins, and sign-up flows.",[11,23,24],{},"In this developer guide, we will analyze the technical architecture of a TWA, implement a secure hash verification check on the backend, and look at frontend integration patterns.",[26,27,29],"h2",{"id":28},"the-telegram-mini-app-ecosystem-architecture","The Telegram Mini App Ecosystem Architecture",[11,31,32],{},"Unlike traditional web applications, a TWA has a dual parent relationship. The frontend runs in a sandboxed browser component (WebView) controlled by the Telegram client app, while the backend speaks both to the Telegram Bot API and your application database.",[34,35,41],"pre",{"className":36,"code":38,"language":39,"meta":40},[37],"language-text","+-----------------------------------------------------------+\n|                      Telegram Client                      |\n|  +-----------------------------------------------------+  |\n|  |                Mini App (WebView UI)                |  |\n|  |     (Uses window.Telegram.WebApp SDK for bridge)    |  |\n|  +--------------------------+--------------------------+  |\n+-----------------------------|-----------------------------+\n       | (initData Query)     | (Secure HTTPS Request)\n       v                      v\n+--------------+       +------------------------------------+\n| Telegram API |       |            Your Backend            |\n|  (Webhooks)  |       |  (Validates initData via HMAC-256) |\n+------+-------+       +-----------------+------------------+\n       |                                 |\n       +----------------> [DB Sync] \u003C----+\n","text","",[42,43,38],"code",{"__ignoreMap":40},[26,45,47,48,51],{"id":46},"_1-security-first-validating-the-initdata-payload","1. Security First: Validating the ",[42,49,50],{},"initData"," Payload",[11,53,54,55,58,59,62],{},"When a user opens your Mini App, Telegram appends a parameter called ",[42,56,57],{},"tgWebAppData"," (or raw search parameters) containing user profiles, launch contexts, and a security ",[42,60,61],{},"hash",".",[11,64,65,66,69],{},"To prevent users from modifying their user IDs or mocking paid subscription privileges, you ",[15,67,68],{},"must validate this hash on your backend"," using your Telegram Bot Token as the HMAC key.",[11,71,72],{},"Here is the cryptographic validation implementation using Node.js:",[34,74,78],{"className":75,"code":76,"language":77,"meta":40,"style":40},"language-typescript shiki shiki-themes github-dark","import crypto from 'crypto';\n\ninterface TelegramUserData {\n  id: number;\n  first_name: string;\n  last_name?: string;\n  username?: string;\n  language_code?: string;\n  is_premium?: boolean;\n}\n\ninterface ValidationResult {\n  isValid: boolean;\n  user?: TelegramUserData;\n}\n\nexport function verifyTelegramInitData(rawQueryString: string, botToken: string): ValidationResult {\n  const urlParams = new URLSearchParams(rawQueryString);\n  const hash = urlParams.get('hash');\n  \n  if (!hash) {\n    return { isValid: false };\n  }\n\n  \u002F\u002F 1. Sort all incoming parameters alphabetically, excluding the hash itself\n  const keys = Array.from(urlParams.keys()).filter(key => key !== 'hash').sort();\n  \n  \u002F\u002F 2. Re-create the verification data-check string\n  const dataCheckString = keys\n    .map(key => `${key}=${urlParams.get(key)}`)\n    .join('\\n');\n\n  \u002F\u002F 3. Generate the secret cryptographic key\n  \u002F\u002F We use the constant string \"WebAppData\" to sign the bot token first\n  const secretKey = crypto\n    .createHmac('sha256', 'WebAppData')\n    .update(botToken)\n    .digest();\n\n  \u002F\u002F 4. Calculate the expected hash of the sorted string\n  const computedHash = crypto\n    .createHmac('sha256', secretKey)\n    .update(dataCheckString)\n    .digest('hex');\n\n  \u002F\u002F 5. Compare computed signature with the signature sent by the client\n  const isValid = computedHash === hash;\n\n  if (!isValid) {\n    return { isValid: false };\n  }\n\n  \u002F\u002F Parse user data object if validation succeeded\n  try {\n    const userRaw = urlParams.get('user');\n    const user: TelegramUserData = userRaw ? JSON.parse(userRaw) : undefined;\n    return { isValid: true, user };\n  } catch (error) {\n    return { isValid: true };\n  }\n}\n","typescript",[42,79,80,103,110,123,139,152,165,177,189,202,208,213,223,235,247,252,257,298,319,343,349,364,379,385,390,397,450,455,461,474,516,536,541,547,553,566,586,597,607,612,618,630,644,654,668,673,679,698,703,715,726,731,736,742,750,772,810,823,835,846,851],{"__ignoreMap":40},[81,82,85,89,93,96,100],"span",{"class":83,"line":84},"line",1,[81,86,88],{"class":87},"snl16","import",[81,90,92],{"class":91},"s95oV"," crypto ",[81,94,95],{"class":87},"from",[81,97,99],{"class":98},"sU2Wk"," 'crypto'",[81,101,102],{"class":91},";\n",[81,104,106],{"class":83,"line":105},2,[81,107,109],{"emptyLinePlaceholder":108},true,"\n",[81,111,113,116,120],{"class":83,"line":112},3,[81,114,115],{"class":87},"interface",[81,117,119],{"class":118},"svObZ"," TelegramUserData",[81,121,122],{"class":91}," {\n",[81,124,126,130,133,137],{"class":83,"line":125},4,[81,127,129],{"class":128},"s9osk","  id",[81,131,132],{"class":87},":",[81,134,136],{"class":135},"sDLfK"," number",[81,138,102],{"class":91},[81,140,142,145,147,150],{"class":83,"line":141},5,[81,143,144],{"class":128},"  first_name",[81,146,132],{"class":87},[81,148,149],{"class":135}," string",[81,151,102],{"class":91},[81,153,155,158,161,163],{"class":83,"line":154},6,[81,156,157],{"class":128},"  last_name",[81,159,160],{"class":87},"?:",[81,162,149],{"class":135},[81,164,102],{"class":91},[81,166,168,171,173,175],{"class":83,"line":167},7,[81,169,170],{"class":128},"  username",[81,172,160],{"class":87},[81,174,149],{"class":135},[81,176,102],{"class":91},[81,178,180,183,185,187],{"class":83,"line":179},8,[81,181,182],{"class":128},"  language_code",[81,184,160],{"class":87},[81,186,149],{"class":135},[81,188,102],{"class":91},[81,190,192,195,197,200],{"class":83,"line":191},9,[81,193,194],{"class":128},"  is_premium",[81,196,160],{"class":87},[81,198,199],{"class":135}," boolean",[81,201,102],{"class":91},[81,203,205],{"class":83,"line":204},10,[81,206,207],{"class":91},"}\n",[81,209,211],{"class":83,"line":210},11,[81,212,109],{"emptyLinePlaceholder":108},[81,214,216,218,221],{"class":83,"line":215},12,[81,217,115],{"class":87},[81,219,220],{"class":118}," ValidationResult",[81,222,122],{"class":91},[81,224,226,229,231,233],{"class":83,"line":225},13,[81,227,228],{"class":128},"  isValid",[81,230,132],{"class":87},[81,232,199],{"class":135},[81,234,102],{"class":91},[81,236,238,241,243,245],{"class":83,"line":237},14,[81,239,240],{"class":128},"  user",[81,242,160],{"class":87},[81,244,119],{"class":118},[81,246,102],{"class":91},[81,248,250],{"class":83,"line":249},15,[81,251,207],{"class":91},[81,253,255],{"class":83,"line":254},16,[81,256,109],{"emptyLinePlaceholder":108},[81,258,260,263,266,269,272,275,277,279,282,285,287,289,292,294,296],{"class":83,"line":259},17,[81,261,262],{"class":87},"export",[81,264,265],{"class":87}," function",[81,267,268],{"class":118}," verifyTelegramInitData",[81,270,271],{"class":91},"(",[81,273,274],{"class":128},"rawQueryString",[81,276,132],{"class":87},[81,278,149],{"class":135},[81,280,281],{"class":91},", ",[81,283,284],{"class":128},"botToken",[81,286,132],{"class":87},[81,288,149],{"class":135},[81,290,291],{"class":91},")",[81,293,132],{"class":87},[81,295,220],{"class":118},[81,297,122],{"class":91},[81,299,301,304,307,310,313,316],{"class":83,"line":300},18,[81,302,303],{"class":87},"  const",[81,305,306],{"class":135}," urlParams",[81,308,309],{"class":87}," =",[81,311,312],{"class":87}," new",[81,314,315],{"class":118}," URLSearchParams",[81,317,318],{"class":91},"(rawQueryString);\n",[81,320,322,324,327,329,332,335,337,340],{"class":83,"line":321},19,[81,323,303],{"class":87},[81,325,326],{"class":135}," hash",[81,328,309],{"class":87},[81,330,331],{"class":91}," urlParams.",[81,333,334],{"class":118},"get",[81,336,271],{"class":91},[81,338,339],{"class":98},"'hash'",[81,341,342],{"class":91},");\n",[81,344,346],{"class":83,"line":345},20,[81,347,348],{"class":91},"  \n",[81,350,352,355,358,361],{"class":83,"line":351},21,[81,353,354],{"class":87},"  if",[81,356,357],{"class":91}," (",[81,359,360],{"class":87},"!",[81,362,363],{"class":91},"hash) {\n",[81,365,367,370,373,376],{"class":83,"line":366},22,[81,368,369],{"class":87},"    return",[81,371,372],{"class":91}," { isValid: ",[81,374,375],{"class":135},"false",[81,377,378],{"class":91}," };\n",[81,380,382],{"class":83,"line":381},23,[81,383,384],{"class":91},"  }\n",[81,386,388],{"class":83,"line":387},24,[81,389,109],{"emptyLinePlaceholder":108},[81,391,393],{"class":83,"line":392},25,[81,394,396],{"class":395},"sAwPA","  \u002F\u002F 1. Sort all incoming parameters alphabetically, excluding the hash itself\n",[81,398,400,402,405,407,410,412,415,418,421,424,426,429,432,435,438,441,444,447],{"class":83,"line":399},26,[81,401,303],{"class":87},[81,403,404],{"class":135}," keys",[81,406,309],{"class":87},[81,408,409],{"class":91}," Array.",[81,411,95],{"class":118},[81,413,414],{"class":91},"(urlParams.",[81,416,417],{"class":118},"keys",[81,419,420],{"class":91},"()).",[81,422,423],{"class":118},"filter",[81,425,271],{"class":91},[81,427,428],{"class":128},"key",[81,430,431],{"class":87}," =>",[81,433,434],{"class":91}," key ",[81,436,437],{"class":87},"!==",[81,439,440],{"class":98}," 'hash'",[81,442,443],{"class":91},").",[81,445,446],{"class":118},"sort",[81,448,449],{"class":91},"();\n",[81,451,453],{"class":83,"line":452},27,[81,454,348],{"class":91},[81,456,458],{"class":83,"line":457},28,[81,459,460],{"class":395},"  \u002F\u002F 2. Re-create the verification data-check string\n",[81,462,464,466,469,471],{"class":83,"line":463},29,[81,465,303],{"class":87},[81,467,468],{"class":135}," dataCheckString",[81,470,309],{"class":87},[81,472,473],{"class":91}," keys\n",[81,475,477,480,483,485,487,489,492,494,497,500,502,504,506,508,510,513],{"class":83,"line":476},30,[81,478,479],{"class":91},"    .",[81,481,482],{"class":118},"map",[81,484,271],{"class":91},[81,486,428],{"class":128},[81,488,431],{"class":87},[81,490,491],{"class":98}," `${",[81,493,428],{"class":91},[81,495,496],{"class":98},"}=${",[81,498,499],{"class":91},"urlParams",[81,501,62],{"class":98},[81,503,334],{"class":118},[81,505,271],{"class":98},[81,507,428],{"class":91},[81,509,291],{"class":98},[81,511,512],{"class":98},"}`",[81,514,515],{"class":91},")\n",[81,517,519,521,524,526,529,532,534],{"class":83,"line":518},31,[81,520,479],{"class":91},[81,522,523],{"class":118},"join",[81,525,271],{"class":91},[81,527,528],{"class":98},"'",[81,530,531],{"class":135},"\\n",[81,533,528],{"class":98},[81,535,342],{"class":91},[81,537,539],{"class":83,"line":538},32,[81,540,109],{"emptyLinePlaceholder":108},[81,542,544],{"class":83,"line":543},33,[81,545,546],{"class":395},"  \u002F\u002F 3. Generate the secret cryptographic key\n",[81,548,550],{"class":83,"line":549},34,[81,551,552],{"class":395},"  \u002F\u002F We use the constant string \"WebAppData\" to sign the bot token first\n",[81,554,556,558,561,563],{"class":83,"line":555},35,[81,557,303],{"class":87},[81,559,560],{"class":135}," secretKey",[81,562,309],{"class":87},[81,564,565],{"class":91}," crypto\n",[81,567,569,571,574,576,579,581,584],{"class":83,"line":568},36,[81,570,479],{"class":91},[81,572,573],{"class":118},"createHmac",[81,575,271],{"class":91},[81,577,578],{"class":98},"'sha256'",[81,580,281],{"class":91},[81,582,583],{"class":98},"'WebAppData'",[81,585,515],{"class":91},[81,587,589,591,594],{"class":83,"line":588},37,[81,590,479],{"class":91},[81,592,593],{"class":118},"update",[81,595,596],{"class":91},"(botToken)\n",[81,598,600,602,605],{"class":83,"line":599},38,[81,601,479],{"class":91},[81,603,604],{"class":118},"digest",[81,606,449],{"class":91},[81,608,610],{"class":83,"line":609},39,[81,611,109],{"emptyLinePlaceholder":108},[81,613,615],{"class":83,"line":614},40,[81,616,617],{"class":395},"  \u002F\u002F 4. Calculate the expected hash of the sorted string\n",[81,619,621,623,626,628],{"class":83,"line":620},41,[81,622,303],{"class":87},[81,624,625],{"class":135}," computedHash",[81,627,309],{"class":87},[81,629,565],{"class":91},[81,631,633,635,637,639,641],{"class":83,"line":632},42,[81,634,479],{"class":91},[81,636,573],{"class":118},[81,638,271],{"class":91},[81,640,578],{"class":98},[81,642,643],{"class":91},", secretKey)\n",[81,645,647,649,651],{"class":83,"line":646},43,[81,648,479],{"class":91},[81,650,593],{"class":118},[81,652,653],{"class":91},"(dataCheckString)\n",[81,655,657,659,661,663,666],{"class":83,"line":656},44,[81,658,479],{"class":91},[81,660,604],{"class":118},[81,662,271],{"class":91},[81,664,665],{"class":98},"'hex'",[81,667,342],{"class":91},[81,669,671],{"class":83,"line":670},45,[81,672,109],{"emptyLinePlaceholder":108},[81,674,676],{"class":83,"line":675},46,[81,677,678],{"class":395},"  \u002F\u002F 5. Compare computed signature with the signature sent by the client\n",[81,680,682,684,687,689,692,695],{"class":83,"line":681},47,[81,683,303],{"class":87},[81,685,686],{"class":135}," isValid",[81,688,309],{"class":87},[81,690,691],{"class":91}," computedHash ",[81,693,694],{"class":87},"===",[81,696,697],{"class":91}," hash;\n",[81,699,701],{"class":83,"line":700},48,[81,702,109],{"emptyLinePlaceholder":108},[81,704,706,708,710,712],{"class":83,"line":705},49,[81,707,354],{"class":87},[81,709,357],{"class":91},[81,711,360],{"class":87},[81,713,714],{"class":91},"isValid) {\n",[81,716,718,720,722,724],{"class":83,"line":717},50,[81,719,369],{"class":87},[81,721,372],{"class":91},[81,723,375],{"class":135},[81,725,378],{"class":91},[81,727,729],{"class":83,"line":728},51,[81,730,384],{"class":91},[81,732,734],{"class":83,"line":733},52,[81,735,109],{"emptyLinePlaceholder":108},[81,737,739],{"class":83,"line":738},53,[81,740,741],{"class":395},"  \u002F\u002F Parse user data object if validation succeeded\n",[81,743,745,748],{"class":83,"line":744},54,[81,746,747],{"class":87},"  try",[81,749,122],{"class":91},[81,751,753,756,759,761,763,765,767,770],{"class":83,"line":752},55,[81,754,755],{"class":87},"    const",[81,757,758],{"class":135}," userRaw",[81,760,309],{"class":87},[81,762,331],{"class":91},[81,764,334],{"class":118},[81,766,271],{"class":91},[81,768,769],{"class":98},"'user'",[81,771,342],{"class":91},[81,773,775,777,780,782,784,786,789,792,795,797,800,803,805,808],{"class":83,"line":774},56,[81,776,755],{"class":87},[81,778,779],{"class":135}," user",[81,781,132],{"class":87},[81,783,119],{"class":118},[81,785,309],{"class":87},[81,787,788],{"class":91}," userRaw ",[81,790,791],{"class":87},"?",[81,793,794],{"class":135}," JSON",[81,796,62],{"class":91},[81,798,799],{"class":118},"parse",[81,801,802],{"class":91},"(userRaw) ",[81,804,132],{"class":87},[81,806,807],{"class":135}," undefined",[81,809,102],{"class":91},[81,811,813,815,817,820],{"class":83,"line":812},57,[81,814,369],{"class":87},[81,816,372],{"class":91},[81,818,819],{"class":135},"true",[81,821,822],{"class":91},", user };\n",[81,824,826,829,832],{"class":83,"line":825},58,[81,827,828],{"class":91},"  } ",[81,830,831],{"class":87},"catch",[81,833,834],{"class":91}," (error) {\n",[81,836,838,840,842,844],{"class":83,"line":837},59,[81,839,369],{"class":87},[81,841,372],{"class":91},[81,843,819],{"class":135},[81,845,378],{"class":91},[81,847,849],{"class":83,"line":848},60,[81,850,384],{"class":91},[81,852,854],{"class":83,"line":853},61,[81,855,207],{"class":91},[26,857,859],{"id":858},"_2-frontend-integration-theme-synchronization","2. Frontend Integration & Theme Synchronization",[11,861,862],{},"To deliver a premium UI\u002FUX, your Mini App should visually blend with the Telegram client’s dark\u002Flight settings. You can access the styles and control client-side behaviors using the official Telegram WebApp JS library.",[864,865,867],"h3",{"id":866},"step-1-include-the-script-in-nuxt-3-html","Step 1: Include the Script in Nuxt 3 \u002F HTML",[11,869,870,871,874],{},"Add the official script to your page header or use the ",[42,872,873],{},"useHead"," composable in Nuxt:",[34,876,878],{"className":75,"code":877,"language":77,"meta":40,"style":40},"\u002F\u002F app.vue or page layout\nuseHead({\n  script: [\n    { src: 'https:\u002F\u002Ftelegram.org\u002Fjs\u002Ftelegram-web-app.js', defer: true }\n  ]\n})\n",[42,879,880,885,892,897,913,918],{"__ignoreMap":40},[81,881,882],{"class":83,"line":84},[81,883,884],{"class":395},"\u002F\u002F app.vue or page layout\n",[81,886,887,889],{"class":83,"line":105},[81,888,873],{"class":118},[81,890,891],{"class":91},"({\n",[81,893,894],{"class":83,"line":112},[81,895,896],{"class":91},"  script: [\n",[81,898,899,902,905,908,910],{"class":83,"line":125},[81,900,901],{"class":91},"    { src: ",[81,903,904],{"class":98},"'https:\u002F\u002Ftelegram.org\u002Fjs\u002Ftelegram-web-app.js'",[81,906,907],{"class":91},", defer: ",[81,909,819],{"class":135},[81,911,912],{"class":91}," }\n",[81,914,915],{"class":83,"line":141},[81,916,917],{"class":91},"  ]\n",[81,919,920],{"class":83,"line":154},[81,921,922],{"class":91},"})\n",[864,924,926],{"id":925},"step-2-access-the-webapp-bridge-in-vue","Step 2: Access the WebApp Bridge in Vue",[11,928,929],{},"Create a Vue composable to access and synchronize Telegram styles:",[34,931,933],{"className":75,"code":932,"language":77,"meta":40,"style":40},"\u002F\u002F composables\u002FuseTelegram.ts\nimport { ref, onMounted } from 'vue';\n\nexport function useTelegram() {\n  const isReady = ref(false);\n  const user = ref\u003Cany>(null);\n\n  onMounted(() => {\n    const tg = (window as any).Telegram?.WebApp;\n    if (tg) {\n      tg.ready();\n      tg.expand(); \u002F\u002F Request the container to fill maximum vertical space\n      \n      user.value = tg.initDataUnsafe?.user;\n      isReady.value = true;\n\n      \u002F\u002F Apply Telegram theme colors to CSS custom properties\n      const root = document.documentElement;\n      root.style.setProperty('--color-tg-bg', tg.themeParams.bg_color);\n      root.style.setProperty('--color-tg-text', tg.themeParams.text_color);\n      root.style.setProperty('--color-tg-button', tg.themeParams.button_color);\n      root.style.setProperty('--color-tg-button-text', tg.themeParams.button_text_color);\n    }\n  });\n\n  return { isReady, user };\n}\n",[42,934,935,940,954,958,970,988,1012,1016,1029,1050,1058,1068,1081,1086,1097,1109,1113,1118,1131,1147,1161,1175,1189,1194,1199,1203,1211],{"__ignoreMap":40},[81,936,937],{"class":83,"line":84},[81,938,939],{"class":395},"\u002F\u002F composables\u002FuseTelegram.ts\n",[81,941,942,944,947,949,952],{"class":83,"line":105},[81,943,88],{"class":87},[81,945,946],{"class":91}," { ref, onMounted } ",[81,948,95],{"class":87},[81,950,951],{"class":98}," 'vue'",[81,953,102],{"class":91},[81,955,956],{"class":83,"line":112},[81,957,109],{"emptyLinePlaceholder":108},[81,959,960,962,964,967],{"class":83,"line":125},[81,961,262],{"class":87},[81,963,265],{"class":87},[81,965,966],{"class":118}," useTelegram",[81,968,969],{"class":91},"() {\n",[81,971,972,974,977,979,982,984,986],{"class":83,"line":141},[81,973,303],{"class":87},[81,975,976],{"class":135}," isReady",[81,978,309],{"class":87},[81,980,981],{"class":118}," ref",[81,983,271],{"class":91},[81,985,375],{"class":135},[81,987,342],{"class":91},[81,989,990,992,994,996,998,1001,1004,1007,1010],{"class":83,"line":154},[81,991,303],{"class":87},[81,993,779],{"class":135},[81,995,309],{"class":87},[81,997,981],{"class":118},[81,999,1000],{"class":91},"\u003C",[81,1002,1003],{"class":135},"any",[81,1005,1006],{"class":91},">(",[81,1008,1009],{"class":135},"null",[81,1011,342],{"class":91},[81,1013,1014],{"class":83,"line":167},[81,1015,109],{"emptyLinePlaceholder":108},[81,1017,1018,1021,1024,1027],{"class":83,"line":179},[81,1019,1020],{"class":118},"  onMounted",[81,1022,1023],{"class":91},"(() ",[81,1025,1026],{"class":87},"=>",[81,1028,122],{"class":91},[81,1030,1031,1033,1036,1038,1041,1044,1047],{"class":83,"line":191},[81,1032,755],{"class":87},[81,1034,1035],{"class":135}," tg",[81,1037,309],{"class":87},[81,1039,1040],{"class":91}," (window ",[81,1042,1043],{"class":87},"as",[81,1045,1046],{"class":135}," any",[81,1048,1049],{"class":91},").Telegram?.WebApp;\n",[81,1051,1052,1055],{"class":83,"line":204},[81,1053,1054],{"class":87},"    if",[81,1056,1057],{"class":91}," (tg) {\n",[81,1059,1060,1063,1066],{"class":83,"line":210},[81,1061,1062],{"class":91},"      tg.",[81,1064,1065],{"class":118},"ready",[81,1067,449],{"class":91},[81,1069,1070,1072,1075,1078],{"class":83,"line":215},[81,1071,1062],{"class":91},[81,1073,1074],{"class":118},"expand",[81,1076,1077],{"class":91},"(); ",[81,1079,1080],{"class":395},"\u002F\u002F Request the container to fill maximum vertical space\n",[81,1082,1083],{"class":83,"line":225},[81,1084,1085],{"class":91},"      \n",[81,1087,1088,1091,1094],{"class":83,"line":237},[81,1089,1090],{"class":91},"      user.value ",[81,1092,1093],{"class":87},"=",[81,1095,1096],{"class":91}," tg.initDataUnsafe?.user;\n",[81,1098,1099,1102,1104,1107],{"class":83,"line":249},[81,1100,1101],{"class":91},"      isReady.value ",[81,1103,1093],{"class":87},[81,1105,1106],{"class":135}," true",[81,1108,102],{"class":91},[81,1110,1111],{"class":83,"line":254},[81,1112,109],{"emptyLinePlaceholder":108},[81,1114,1115],{"class":83,"line":259},[81,1116,1117],{"class":395},"      \u002F\u002F Apply Telegram theme colors to CSS custom properties\n",[81,1119,1120,1123,1126,1128],{"class":83,"line":300},[81,1121,1122],{"class":87},"      const",[81,1124,1125],{"class":135}," root",[81,1127,309],{"class":87},[81,1129,1130],{"class":91}," document.documentElement;\n",[81,1132,1133,1136,1139,1141,1144],{"class":83,"line":321},[81,1134,1135],{"class":91},"      root.style.",[81,1137,1138],{"class":118},"setProperty",[81,1140,271],{"class":91},[81,1142,1143],{"class":98},"'--color-tg-bg'",[81,1145,1146],{"class":91},", tg.themeParams.bg_color);\n",[81,1148,1149,1151,1153,1155,1158],{"class":83,"line":345},[81,1150,1135],{"class":91},[81,1152,1138],{"class":118},[81,1154,271],{"class":91},[81,1156,1157],{"class":98},"'--color-tg-text'",[81,1159,1160],{"class":91},", tg.themeParams.text_color);\n",[81,1162,1163,1165,1167,1169,1172],{"class":83,"line":351},[81,1164,1135],{"class":91},[81,1166,1138],{"class":118},[81,1168,271],{"class":91},[81,1170,1171],{"class":98},"'--color-tg-button'",[81,1173,1174],{"class":91},", tg.themeParams.button_color);\n",[81,1176,1177,1179,1181,1183,1186],{"class":83,"line":366},[81,1178,1135],{"class":91},[81,1180,1138],{"class":118},[81,1182,271],{"class":91},[81,1184,1185],{"class":98},"'--color-tg-button-text'",[81,1187,1188],{"class":91},", tg.themeParams.button_text_color);\n",[81,1190,1191],{"class":83,"line":381},[81,1192,1193],{"class":91},"    }\n",[81,1195,1196],{"class":83,"line":387},[81,1197,1198],{"class":91},"  });\n",[81,1200,1201],{"class":83,"line":392},[81,1202,109],{"emptyLinePlaceholder":108},[81,1204,1205,1208],{"class":83,"line":399},[81,1206,1207],{"class":87},"  return",[81,1209,1210],{"class":91}," { isReady, user };\n",[81,1212,1213],{"class":83,"line":452},[81,1214,207],{"class":91},[26,1216,1218],{"id":1217},"_3-monetization-payments-via-telegram-stars","3. Monetization: Payments via Telegram Stars",[11,1220,1221,1222,357,1225,1228],{},"When operating inside Telegram, all digital services or content purchases must comply with Apple App Store and Google Play policies. Telegram enforces this by requiring the use of ",[15,1223,1224],{},"Telegram Stars",[42,1226,1227],{},"XTR",") for digital goods.",[11,1230,1231],{},"The flow for receiving Stars payments:",[1233,1234,1235,1242,1254,1260],"ol",{},[1236,1237,1238,1241],"li",{},[15,1239,1240],{},"Request Invoice",": The TWA requests the backend to generate an invoice.",[1236,1243,1244,1247,1248,1251,1252,62],{},[15,1245,1246],{},"Bot Sends Invoice",": The backend calls Bot API ",[42,1249,1250],{},"sendInvoice"," using the currency ",[42,1253,1227],{},[1236,1255,1256,1259],{},[15,1257,1258],{},"Client Checkout",": The Telegram client opens a native overlay allowing the user to pay using Stars purchased in-app.",[1236,1261,1262,1265,1266,1269],{},[15,1263,1264],{},"Verification",": Telegram sends a webhook ",[42,1267,1268],{},"successful_payment"," to your bot, which credits the user's account in your DB.",[11,1271,1272,1273,1278,1279,62],{},"In my Telegram bot hosting platform, ",[1274,1275,1277],"a",{"href":1276},"\u002Fprojects\u002Ftelego","TeleGo.io",", we provide exactly these billing options, allowing users to spin up their own TWAs and receive Stars instantly. Additionally, we support hybrid human-AI helpdesks, which you can learn to build in my guide on ",[1274,1280,1282],{"href":1281},"\u002Fblog\u002Ftelegram-bot-ai-rag-support","RAG AI Customer Support Bots",[11,1284,1285,1286,1290],{},"For physical goods and consulting services, traditional gateways can be used. Read my ",[1274,1287,1289],{"href":1288},"\u002Fblog\u002Fsaas-stripe-billing-integration","SaaS Stripe Billing Integration guide"," for Express\u002FTypeScript templates.",[26,1292,1294],{"id":1293},"sources-and-documentation","Sources and documentation",[1296,1297,1298,1307],"ul",{},[1236,1299,1300,1306],{},[1274,1301,1305],{"href":1302,"rel":1303},"https:\u002F\u002Fcore.telegram.org\u002Fbots\u002Fwebapps",[1304],"nofollow","Telegram Mini Apps"," — the official WebApp SDK and initData documentation",[1236,1308,1309,1314],{},[1274,1310,1313],{"href":1311,"rel":1312},"https:\u002F\u002Fcore.telegram.org\u002Fbots\u002Fpayments",[1304],"Bot Payments API"," — invoices, Telegram Stars, and payment webhooks",[11,1316,1317],{},"By combining native web frameworks with the Telegram WebApp API, developers can ship complex SaaS platforms and digital products directly into active messaging channels.",[11,1319,1320,1321,1325,1326,1330],{},"If you are planning to build a high-performance Telegram Mini App with secure billing integrations and dynamic frontend design, check out my ",[1274,1322,1324],{"href":1323},"\u002Ftelegram-bots","Telegram Bot Development Service"," or request a ",[1274,1327,1329],{"href":1328},"\u002Fconsultations","Technical Architecture Consultation"," to get a production blueprint.",[1332,1333,1334],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":40,"searchDepth":105,"depth":105,"links":1336},[1337,1338,1340,1344,1345],{"id":28,"depth":105,"text":29},{"id":46,"depth":105,"text":1339},"1. Security First: Validating the initData Payload",{"id":858,"depth":105,"text":859,"children":1341},[1342,1343],{"id":866,"depth":112,"text":867},{"id":925,"depth":112,"text":926},{"id":1217,"depth":105,"text":1218},{"id":1293,"depth":105,"text":1294},"2026-07-05","A comprehensive developer's guide to using Telegram Mini Apps as a dynamic web frontend, implementing secure hash validation, and processing in-app purchases.","md",[1350,1353,1356],{"q":1351,"a":1352},"How is a Telegram Mini App different from a regular bot?","A bot communicates through messages and buttons, while a Mini App opens a full web interface inside Telegram: dashboards, catalogs, forms, charts. It's an SPA running in a WebView with its own frontend, and authorization happens automatically through the Telegram account.",{"q":1354,"a":1355},"How do I verify that user data in a Mini App isn't forged?","Through cryptographic initData validation: Telegram signs the user data with an HMAC-SHA256 signature derived from the bot token. The backend must recompute and compare the signature on every request — without it, anyone can impersonate any user.",{"q":1357,"a":1358},"When are Telegram Stars mandatory, and when can I use Stripe?","Digital goods and subscriptions inside a Mini App must be paid with Telegram Stars per Apple and Google policies. Physical goods, services, and consulting can be sold through classic gateways like Stripe.","\u002Fimages\u002Fblog\u002Fblog_telegram_twa.jpg","en",{},"\u002Fblog\u002Fen\u002Ftelegram-web-apps-sells",{"title":6,"description":1347},"blog\u002Fen\u002Ftelegram-web-apps-sells",[1366,1367,1368,1369,1370],"Telegram","Mini Apps","SaaS","Node.js","Frontend","4blcVbDR853MsonsChj72pfs0C4mqm-aL83XkBOZ0io",{"id":1373,"title":1374,"body":1375,"date":2822,"description":2823,"extension":1348,"faq":2824,"image":2834,"lang":1360,"meta":2835,"navigation":108,"path":2836,"published":108,"readTime":215,"seo":2837,"stem":2838,"tags":2839,"updated":1346,"__hash__":2844},"blog\u002Fblog\u002Fen\u002Ftelegram-bot-ai-rag-support.md","AI-Powered Customer Support: Building a RAG Bot with Live Agent Handover",{"type":8,"value":1376,"toc":2814},[1377,1380,1392,1399,1406,1410,1416,1420,1430,1504,1507,1591,1595,1598,2032,2036,2039,2293,2297,2304,2753,2759,2771,2773,2798,2801,2811],[11,1378,1379],{},"Deploying large language models (LLMs) like ChatGPT to answer client support queries is a highly effective way to reduce operational costs. However, standard LLMs suffer from two major flaws in corporate environments: they have no access to your private company data (like API documentation, refund guidelines, or custom plans), and they occasionally hallucinate incorrect information.",[11,1381,1382,1383,1386,1387,1391],{},"To solve this, modern production architectures use the ",[15,1384,1385],{},"RAG (Retrieval-Augmented Generation)"," framework. RAG restricts the AI to search a local database first, extract relevant knowledge blocks, and answer the user query based ",[1388,1389,1390],"em",{},"strictly"," on that retrieved text.",[11,1393,1394,1395,1398],{},"Furthermore, to maintain high brand standards, your support system must implement a ",[15,1396,1397],{},"live agent handover protocol"," to route the conversation to a human support manager the second the AI struggles to find an answer.",[11,1400,1401,1402,1405],{},"In this developer guide, we will build a complete RAG execution loop using Node.js, PostgreSQL ",[42,1403,1404],{},"pgvector",", and a hybrid user-to-manager routing machine.",[26,1407,1409],{"id":1408},"technical-rag-workflow","Technical RAG Workflow",[34,1411,1414],{"className":1412,"code":1413,"language":39,"meta":40},[37],"[User asks: \"How to connect Stripe?\"]\n               │\n               v\n  (Generate Vector Embedding)\n  [text-embedding-3-small] (1536 dim)\n               │\n               v\n  (Vector Search in PostgreSQL)\n  SELECT * FROM match_kb_docs(vector, threshold = 0.75)\n               │\n               +----------------------+\n               |                      |\n      (Context Found)        (Context NOT Found \u002F Low Score)\n               │                      │\n               v                      v\n  [Assemble Prompt with Docs]   [Set Session = human_handling]\n               │                      │\n               v                      v\n  [Send to gpt-4o-mini]         [Notify Support Team in Slack]\n               │                      │\n               v                      v\n    [Send Answer to User]        [Alert user: \"Transferring...\"]\n",[42,1415,1413],{"__ignoreMap":40},[26,1417,1419],{"id":1418},"_1-setting-up-pgvector-database","1. Setting Up Pgvector Database",[11,1421,1422,1423,1426,1427,443],{},"First, enable the ",[42,1424,1425],{},"vector"," extension in PostgreSQL and create a table to store documentation chunks and their corresponding embedding coordinates (dimensions must match the embedding model, e.g., 1536 dimensions for OpenAI's ",[42,1428,1429],{},"text-embedding-3-small",[34,1431,1435],{"className":1432,"code":1433,"language":1434,"meta":40,"style":40},"language-sql shiki shiki-themes github-dark","-- Enable vector support extension\nCREATE EXTENSION IF NOT EXISTS vector;\n\n-- Create Knowledge Base table\nCREATE TABLE kb_documents (\n  id SERIAL PRIMARY KEY,\n  content TEXT NOT NULL,\n  embedding VECTOR(1536) NOT NULL,\n  category VARCHAR(50) DEFAULT 'general',\n  created_at TIMESTAMP DEFAULT NOW()\n);\n\n-- Index vector column for fast search (HNSW index recommended for scalability)\nCREATE INDEX kb_hnsw_idx ON kb_documents USING hnsw (embedding vector_cosine_ops);\n","sql",[42,1436,1437,1442,1447,1451,1456,1461,1466,1471,1476,1481,1486,1490,1494,1499],{"__ignoreMap":40},[81,1438,1439],{"class":83,"line":84},[81,1440,1441],{},"-- Enable vector support extension\n",[81,1443,1444],{"class":83,"line":105},[81,1445,1446],{},"CREATE EXTENSION IF NOT EXISTS vector;\n",[81,1448,1449],{"class":83,"line":112},[81,1450,109],{"emptyLinePlaceholder":108},[81,1452,1453],{"class":83,"line":125},[81,1454,1455],{},"-- Create Knowledge Base table\n",[81,1457,1458],{"class":83,"line":141},[81,1459,1460],{},"CREATE TABLE kb_documents (\n",[81,1462,1463],{"class":83,"line":154},[81,1464,1465],{},"  id SERIAL PRIMARY KEY,\n",[81,1467,1468],{"class":83,"line":167},[81,1469,1470],{},"  content TEXT NOT NULL,\n",[81,1472,1473],{"class":83,"line":179},[81,1474,1475],{},"  embedding VECTOR(1536) NOT NULL,\n",[81,1477,1478],{"class":83,"line":191},[81,1479,1480],{},"  category VARCHAR(50) DEFAULT 'general',\n",[81,1482,1483],{"class":83,"line":204},[81,1484,1485],{},"  created_at TIMESTAMP DEFAULT NOW()\n",[81,1487,1488],{"class":83,"line":210},[81,1489,342],{},[81,1491,1492],{"class":83,"line":215},[81,1493,109],{"emptyLinePlaceholder":108},[81,1495,1496],{"class":83,"line":225},[81,1497,1498],{},"-- Index vector column for fast search (HNSW index recommended for scalability)\n",[81,1500,1501],{"class":83,"line":237},[81,1502,1503],{},"CREATE INDEX kb_hnsw_idx ON kb_documents USING hnsw (embedding vector_cosine_ops);\n",[11,1505,1506],{},"Next, create the SQL function to retrieve documents based on cosine similarity:",[34,1508,1510],{"className":1432,"code":1509,"language":1434,"meta":40,"style":40},"CREATE OR REPLACE FUNCTION match_kb_docs(\n  query_embedding VECTOR(1536),\n  match_threshold FLOAT,\n  match_count INT\n)\nRETURNS TABLE (id INT, content TEXT, similarity FLOAT)\nLANGUAGE plpgsql AS $$\nBEGIN\n  RETURN QUERY\n  SELECT kb.id, kb.content, 1 - (kb.embedding \u003C=> query_embedding) AS similarity\n  FROM kb_documents kb\n  WHERE 1 - (kb.embedding \u003C=> query_embedding) > match_threshold\n  ORDER BY kb.embedding \u003C=> query_embedding\n  LIMIT match_count;\nEND;\n$$;\n",[42,1511,1512,1517,1522,1527,1532,1536,1541,1546,1551,1556,1561,1566,1571,1576,1581,1586],{"__ignoreMap":40},[81,1513,1514],{"class":83,"line":84},[81,1515,1516],{},"CREATE OR REPLACE FUNCTION match_kb_docs(\n",[81,1518,1519],{"class":83,"line":105},[81,1520,1521],{},"  query_embedding VECTOR(1536),\n",[81,1523,1524],{"class":83,"line":112},[81,1525,1526],{},"  match_threshold FLOAT,\n",[81,1528,1529],{"class":83,"line":125},[81,1530,1531],{},"  match_count INT\n",[81,1533,1534],{"class":83,"line":141},[81,1535,515],{},[81,1537,1538],{"class":83,"line":154},[81,1539,1540],{},"RETURNS TABLE (id INT, content TEXT, similarity FLOAT)\n",[81,1542,1543],{"class":83,"line":167},[81,1544,1545],{},"LANGUAGE plpgsql AS $$\n",[81,1547,1548],{"class":83,"line":179},[81,1549,1550],{},"BEGIN\n",[81,1552,1553],{"class":83,"line":191},[81,1554,1555],{},"  RETURN QUERY\n",[81,1557,1558],{"class":83,"line":204},[81,1559,1560],{},"  SELECT kb.id, kb.content, 1 - (kb.embedding \u003C=> query_embedding) AS similarity\n",[81,1562,1563],{"class":83,"line":210},[81,1564,1565],{},"  FROM kb_documents kb\n",[81,1567,1568],{"class":83,"line":215},[81,1569,1570],{},"  WHERE 1 - (kb.embedding \u003C=> query_embedding) > match_threshold\n",[81,1572,1573],{"class":83,"line":225},[81,1574,1575],{},"  ORDER BY kb.embedding \u003C=> query_embedding\n",[81,1577,1578],{"class":83,"line":237},[81,1579,1580],{},"  LIMIT match_count;\n",[81,1582,1583],{"class":83,"line":249},[81,1584,1585],{},"END;\n",[81,1587,1588],{"class":83,"line":254},[81,1589,1590],{},"$$;\n",[26,1592,1594],{"id":1593},"_2-implementing-rag-fetching-in-nodejs","2. Implementing RAG Fetching in Node.js",[11,1596,1597],{},"We write a service to generate embeddings for user questions via the OpenAI API and execute the vector search.",[34,1599,1601],{"className":75,"code":1600,"language":77,"meta":40,"style":40},"import { OpenAI } from 'openai';\nimport pg from 'pg';\n\nconst openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });\nconst pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });\n\nexport async function getEmbedding(text: string): Promise\u003Cnumber[]> {\n  const response = await openai.embeddings.create({\n    model: 'text-embedding-3-small',\n    input: text.replace(\u002F\\n\u002Fg, ' '),\n  });\n  return response.data[0].embedding;\n}\n\nexport async function findRelevantContext(queryText: string): Promise\u003Cstring> {\n  const queryVector = await getEmbedding(queryText);\n  const sql = `SELECT * FROM match_kb_docs($1::vector, 0.75, 3)`;\n  \n  const client = await pool.connect();\n  try {\n    const result = await client.query(sql, [JSON.stringify(queryVector)]);\n    if (result.rows.length === 0) {\n      return '';\n    }\n    \u002F\u002F Combine top matches into a single text block\n    return result.rows.map(row => row.content).join('\\n---\\n');\n  } finally {\n    client.release();\n  }\n}\n",[42,1602,1603,1617,1631,1635,1659,1684,1688,1723,1743,1754,1782,1786,1799,1803,1807,1841,1857,1871,1875,1894,1900,1931,1950,1960,1964,1969,2005,2014,2024,2028],{"__ignoreMap":40},[81,1604,1605,1607,1610,1612,1615],{"class":83,"line":84},[81,1606,88],{"class":87},[81,1608,1609],{"class":91}," { OpenAI } ",[81,1611,95],{"class":87},[81,1613,1614],{"class":98}," 'openai'",[81,1616,102],{"class":91},[81,1618,1619,1621,1624,1626,1629],{"class":83,"line":105},[81,1620,88],{"class":87},[81,1622,1623],{"class":91}," pg ",[81,1625,95],{"class":87},[81,1627,1628],{"class":98}," 'pg'",[81,1630,102],{"class":91},[81,1632,1633],{"class":83,"line":112},[81,1634,109],{"emptyLinePlaceholder":108},[81,1636,1637,1640,1643,1645,1647,1650,1653,1656],{"class":83,"line":125},[81,1638,1639],{"class":87},"const",[81,1641,1642],{"class":135}," openai",[81,1644,309],{"class":87},[81,1646,312],{"class":87},[81,1648,1649],{"class":118}," OpenAI",[81,1651,1652],{"class":91},"({ apiKey: process.env.",[81,1654,1655],{"class":135},"OPENAI_API_KEY",[81,1657,1658],{"class":91}," });\n",[81,1660,1661,1663,1666,1668,1670,1673,1676,1679,1682],{"class":83,"line":141},[81,1662,1639],{"class":87},[81,1664,1665],{"class":135}," pool",[81,1667,309],{"class":87},[81,1669,312],{"class":87},[81,1671,1672],{"class":91}," pg.",[81,1674,1675],{"class":118},"Pool",[81,1677,1678],{"class":91},"({ connectionString: process.env.",[81,1680,1681],{"class":135},"DATABASE_URL",[81,1683,1658],{"class":91},[81,1685,1686],{"class":83,"line":154},[81,1687,109],{"emptyLinePlaceholder":108},[81,1689,1690,1692,1695,1697,1700,1702,1704,1706,1708,1710,1712,1715,1717,1720],{"class":83,"line":167},[81,1691,262],{"class":87},[81,1693,1694],{"class":87}," async",[81,1696,265],{"class":87},[81,1698,1699],{"class":118}," getEmbedding",[81,1701,271],{"class":91},[81,1703,39],{"class":128},[81,1705,132],{"class":87},[81,1707,149],{"class":135},[81,1709,291],{"class":91},[81,1711,132],{"class":87},[81,1713,1714],{"class":118}," Promise",[81,1716,1000],{"class":91},[81,1718,1719],{"class":135},"number",[81,1721,1722],{"class":91},"[]> {\n",[81,1724,1725,1727,1730,1732,1735,1738,1741],{"class":83,"line":179},[81,1726,303],{"class":87},[81,1728,1729],{"class":135}," response",[81,1731,309],{"class":87},[81,1733,1734],{"class":87}," await",[81,1736,1737],{"class":91}," openai.embeddings.",[81,1739,1740],{"class":118},"create",[81,1742,891],{"class":91},[81,1744,1745,1748,1751],{"class":83,"line":191},[81,1746,1747],{"class":91},"    model: ",[81,1749,1750],{"class":98},"'text-embedding-3-small'",[81,1752,1753],{"class":91},",\n",[81,1755,1756,1759,1762,1764,1767,1769,1771,1774,1776,1779],{"class":83,"line":204},[81,1757,1758],{"class":91},"    input: text.",[81,1760,1761],{"class":118},"replace",[81,1763,271],{"class":91},[81,1765,1766],{"class":98},"\u002F",[81,1768,531],{"class":135},[81,1770,1766],{"class":98},[81,1772,1773],{"class":87},"g",[81,1775,281],{"class":91},[81,1777,1778],{"class":98},"' '",[81,1780,1781],{"class":91},"),\n",[81,1783,1784],{"class":83,"line":210},[81,1785,1198],{"class":91},[81,1787,1788,1790,1793,1796],{"class":83,"line":215},[81,1789,1207],{"class":87},[81,1791,1792],{"class":91}," response.data[",[81,1794,1795],{"class":135},"0",[81,1797,1798],{"class":91},"].embedding;\n",[81,1800,1801],{"class":83,"line":225},[81,1802,207],{"class":91},[81,1804,1805],{"class":83,"line":237},[81,1806,109],{"emptyLinePlaceholder":108},[81,1808,1809,1811,1813,1815,1818,1820,1823,1825,1827,1829,1831,1833,1835,1838],{"class":83,"line":249},[81,1810,262],{"class":87},[81,1812,1694],{"class":87},[81,1814,265],{"class":87},[81,1816,1817],{"class":118}," findRelevantContext",[81,1819,271],{"class":91},[81,1821,1822],{"class":128},"queryText",[81,1824,132],{"class":87},[81,1826,149],{"class":135},[81,1828,291],{"class":91},[81,1830,132],{"class":87},[81,1832,1714],{"class":118},[81,1834,1000],{"class":91},[81,1836,1837],{"class":135},"string",[81,1839,1840],{"class":91},"> {\n",[81,1842,1843,1845,1848,1850,1852,1854],{"class":83,"line":254},[81,1844,303],{"class":87},[81,1846,1847],{"class":135}," queryVector",[81,1849,309],{"class":87},[81,1851,1734],{"class":87},[81,1853,1699],{"class":118},[81,1855,1856],{"class":91},"(queryText);\n",[81,1858,1859,1861,1864,1866,1869],{"class":83,"line":259},[81,1860,303],{"class":87},[81,1862,1863],{"class":135}," sql",[81,1865,309],{"class":87},[81,1867,1868],{"class":98}," `SELECT * FROM match_kb_docs($1::vector, 0.75, 3)`",[81,1870,102],{"class":91},[81,1872,1873],{"class":83,"line":300},[81,1874,348],{"class":91},[81,1876,1877,1879,1882,1884,1886,1889,1892],{"class":83,"line":321},[81,1878,303],{"class":87},[81,1880,1881],{"class":135}," client",[81,1883,309],{"class":87},[81,1885,1734],{"class":87},[81,1887,1888],{"class":91}," pool.",[81,1890,1891],{"class":118},"connect",[81,1893,449],{"class":91},[81,1895,1896,1898],{"class":83,"line":345},[81,1897,747],{"class":87},[81,1899,122],{"class":91},[81,1901,1902,1904,1907,1909,1911,1914,1917,1920,1923,1925,1928],{"class":83,"line":351},[81,1903,755],{"class":87},[81,1905,1906],{"class":135}," result",[81,1908,309],{"class":87},[81,1910,1734],{"class":87},[81,1912,1913],{"class":91}," client.",[81,1915,1916],{"class":118},"query",[81,1918,1919],{"class":91},"(sql, [",[81,1921,1922],{"class":135},"JSON",[81,1924,62],{"class":91},[81,1926,1927],{"class":118},"stringify",[81,1929,1930],{"class":91},"(queryVector)]);\n",[81,1932,1933,1935,1938,1941,1944,1947],{"class":83,"line":366},[81,1934,1054],{"class":87},[81,1936,1937],{"class":91}," (result.rows.",[81,1939,1940],{"class":135},"length",[81,1942,1943],{"class":87}," ===",[81,1945,1946],{"class":135}," 0",[81,1948,1949],{"class":91},") {\n",[81,1951,1952,1955,1958],{"class":83,"line":381},[81,1953,1954],{"class":87},"      return",[81,1956,1957],{"class":98}," ''",[81,1959,102],{"class":91},[81,1961,1962],{"class":83,"line":387},[81,1963,1193],{"class":91},[81,1965,1966],{"class":83,"line":392},[81,1967,1968],{"class":395},"    \u002F\u002F Combine top matches into a single text block\n",[81,1970,1971,1973,1976,1978,1980,1983,1985,1988,1990,1992,1994,1996,1999,2001,2003],{"class":83,"line":399},[81,1972,369],{"class":87},[81,1974,1975],{"class":91}," result.rows.",[81,1977,482],{"class":118},[81,1979,271],{"class":91},[81,1981,1982],{"class":128},"row",[81,1984,431],{"class":87},[81,1986,1987],{"class":91}," row.content).",[81,1989,523],{"class":118},[81,1991,271],{"class":91},[81,1993,528],{"class":98},[81,1995,531],{"class":135},[81,1997,1998],{"class":98},"---",[81,2000,531],{"class":135},[81,2002,528],{"class":98},[81,2004,342],{"class":91},[81,2006,2007,2009,2012],{"class":83,"line":452},[81,2008,828],{"class":91},[81,2010,2011],{"class":87},"finally",[81,2013,122],{"class":91},[81,2015,2016,2019,2022],{"class":83,"line":457},[81,2017,2018],{"class":91},"    client.",[81,2020,2021],{"class":118},"release",[81,2023,449],{"class":91},[81,2025,2026],{"class":83,"line":463},[81,2027,384],{"class":91},[81,2029,2030],{"class":83,"line":476},[81,2031,207],{"class":91},[26,2033,2035],{"id":2034},"_3-strict-prompting-fallback-logic","3. Strict Prompting & Fallback Logic",[11,2037,2038],{},"To avoid AI hallucinations, we enforce strict rules inside the system prompt:",[34,2040,2042],{"className":75,"code":2041,"language":77,"meta":40,"style":40},"export async function generateAIResponse(userQuestion: string, context: string): Promise\u003Cstring> {\n  if (!context) {\n    return 'ERROR_UNRESOLVED';\n  }\n\n  const systemPrompt = `\nYou are a technical support assistant for TeleGo.io.\nAnswer the customer's question using ONLY the context provided below.\nRules:\n1. Rely ONLY on the context details. Do not use external knowledge or invent facts.\n2. If the context is insufficient or unrelated to the question, respond strictly with: \"ERROR_UNRESOLVED\".\n3. Maintain a polite, professional tone.\n\nContext:\n---\n${context}\n---\n`;\n\n  const response = await openai.chat.completions.create({\n    model: 'gpt-4o-mini',\n    messages: [\n      { role: 'system', content: systemPrompt },\n      { role: 'user', content: userQuestion }\n    ],\n    temperature: 0.0, \u002F\u002F Force deterministic outputs\n  });\n\n  return response.choices[0].message.content || 'ERROR_UNRESOLVED';\n}\n",[42,2043,2044,2085,2096,2105,2109,2113,2125,2130,2135,2140,2145,2150,2155,2159,2164,2169,2178,2182,2189,2193,2210,2219,2224,2235,2244,2249,2262,2266,2270,2289],{"__ignoreMap":40},[81,2045,2046,2048,2050,2052,2055,2057,2060,2062,2064,2066,2069,2071,2073,2075,2077,2079,2081,2083],{"class":83,"line":84},[81,2047,262],{"class":87},[81,2049,1694],{"class":87},[81,2051,265],{"class":87},[81,2053,2054],{"class":118}," generateAIResponse",[81,2056,271],{"class":91},[81,2058,2059],{"class":128},"userQuestion",[81,2061,132],{"class":87},[81,2063,149],{"class":135},[81,2065,281],{"class":91},[81,2067,2068],{"class":128},"context",[81,2070,132],{"class":87},[81,2072,149],{"class":135},[81,2074,291],{"class":91},[81,2076,132],{"class":87},[81,2078,1714],{"class":118},[81,2080,1000],{"class":91},[81,2082,1837],{"class":135},[81,2084,1840],{"class":91},[81,2086,2087,2089,2091,2093],{"class":83,"line":105},[81,2088,354],{"class":87},[81,2090,357],{"class":91},[81,2092,360],{"class":87},[81,2094,2095],{"class":91},"context) {\n",[81,2097,2098,2100,2103],{"class":83,"line":112},[81,2099,369],{"class":87},[81,2101,2102],{"class":98}," 'ERROR_UNRESOLVED'",[81,2104,102],{"class":91},[81,2106,2107],{"class":83,"line":125},[81,2108,384],{"class":91},[81,2110,2111],{"class":83,"line":141},[81,2112,109],{"emptyLinePlaceholder":108},[81,2114,2115,2117,2120,2122],{"class":83,"line":154},[81,2116,303],{"class":87},[81,2118,2119],{"class":135}," systemPrompt",[81,2121,309],{"class":87},[81,2123,2124],{"class":98}," `\n",[81,2126,2127],{"class":83,"line":167},[81,2128,2129],{"class":98},"You are a technical support assistant for TeleGo.io.\n",[81,2131,2132],{"class":83,"line":179},[81,2133,2134],{"class":98},"Answer the customer's question using ONLY the context provided below.\n",[81,2136,2137],{"class":83,"line":191},[81,2138,2139],{"class":98},"Rules:\n",[81,2141,2142],{"class":83,"line":204},[81,2143,2144],{"class":98},"1. Rely ONLY on the context details. Do not use external knowledge or invent facts.\n",[81,2146,2147],{"class":83,"line":210},[81,2148,2149],{"class":98},"2. If the context is insufficient or unrelated to the question, respond strictly with: \"ERROR_UNRESOLVED\".\n",[81,2151,2152],{"class":83,"line":215},[81,2153,2154],{"class":98},"3. Maintain a polite, professional tone.\n",[81,2156,2157],{"class":83,"line":225},[81,2158,109],{"emptyLinePlaceholder":108},[81,2160,2161],{"class":83,"line":237},[81,2162,2163],{"class":98},"Context:\n",[81,2165,2166],{"class":83,"line":249},[81,2167,2168],{"class":98},"---\n",[81,2170,2171,2174,2176],{"class":83,"line":254},[81,2172,2173],{"class":98},"${",[81,2175,2068],{"class":91},[81,2177,207],{"class":98},[81,2179,2180],{"class":83,"line":259},[81,2181,2168],{"class":98},[81,2183,2184,2187],{"class":83,"line":300},[81,2185,2186],{"class":98},"`",[81,2188,102],{"class":91},[81,2190,2191],{"class":83,"line":321},[81,2192,109],{"emptyLinePlaceholder":108},[81,2194,2195,2197,2199,2201,2203,2206,2208],{"class":83,"line":345},[81,2196,303],{"class":87},[81,2198,1729],{"class":135},[81,2200,309],{"class":87},[81,2202,1734],{"class":87},[81,2204,2205],{"class":91}," openai.chat.completions.",[81,2207,1740],{"class":118},[81,2209,891],{"class":91},[81,2211,2212,2214,2217],{"class":83,"line":351},[81,2213,1747],{"class":91},[81,2215,2216],{"class":98},"'gpt-4o-mini'",[81,2218,1753],{"class":91},[81,2220,2221],{"class":83,"line":366},[81,2222,2223],{"class":91},"    messages: [\n",[81,2225,2226,2229,2232],{"class":83,"line":381},[81,2227,2228],{"class":91},"      { role: ",[81,2230,2231],{"class":98},"'system'",[81,2233,2234],{"class":91},", content: systemPrompt },\n",[81,2236,2237,2239,2241],{"class":83,"line":387},[81,2238,2228],{"class":91},[81,2240,769],{"class":98},[81,2242,2243],{"class":91},", content: userQuestion }\n",[81,2245,2246],{"class":83,"line":392},[81,2247,2248],{"class":91},"    ],\n",[81,2250,2251,2254,2257,2259],{"class":83,"line":399},[81,2252,2253],{"class":91},"    temperature: ",[81,2255,2256],{"class":135},"0.0",[81,2258,281],{"class":91},[81,2260,2261],{"class":395},"\u002F\u002F Force deterministic outputs\n",[81,2263,2264],{"class":83,"line":452},[81,2265,1198],{"class":91},[81,2267,2268],{"class":83,"line":457},[81,2269,109],{"emptyLinePlaceholder":108},[81,2271,2272,2274,2277,2279,2282,2285,2287],{"class":83,"line":463},[81,2273,1207],{"class":87},[81,2275,2276],{"class":91}," response.choices[",[81,2278,1795],{"class":135},[81,2280,2281],{"class":91},"].message.content ",[81,2283,2284],{"class":87},"||",[81,2286,2102],{"class":98},[81,2288,102],{"class":91},[81,2290,2291],{"class":83,"line":476},[81,2292,207],{"class":91},[26,2294,2296],{"id":2295},"_4-chat-routing-and-human-handover","4. Chat Routing and Human Handover",[11,2298,2299,2300,2303],{},"When a message is received in the Telegram bot, we check if the user is currently flagged as speaking to a human manager. If not, we run the RAG query. If the RAG engine returns ",[42,2301,2302],{},"ERROR_UNRESOLVED",", we transfer the session.",[34,2305,2307],{"className":75,"code":2306,"language":77,"meta":40,"style":40},"import TelegramBot from 'node-telegram-bot-api';\n\nconst bot = new TelegramBot(process.env.TELEGRAM_BOT_TOKEN!, { polling: true });\n\nbot.on('message', async (msg) => {\n  const chatId = msg.chat.id;\n  const userText = msg.text;\n\n  if (!userText) return;\n\n  \u002F\u002F 1. Get current session status from DB\n  const session = await db.getChatSession(chatId);\n\n  \u002F\u002F If in human handling, forward messages to manager Slack\u002FTelegram\n  if (session?.status === 'human_handling') {\n    await forwardToManagers(chatId, msg);\n    return;\n  }\n\n  \u002F\u002F 2. Perform RAG query\n  try {\n    const context = await findRelevantContext(userText);\n    const aiResponse = await generateAIResponse(userText, context);\n\n    \u002F\u002F 3. Escalation check\n    if (aiResponse.trim() === 'ERROR_UNRESOLVED') {\n      \u002F\u002F Set session to human handling in DB\n      await db.updateChatSession(chatId, { status: 'human_handling' });\n      \n      \u002F\u002F Notify support staff via webhook\n      await notifySupportStaff(chatId, userText);\n      \n      await bot.sendMessage(\n        chatId, \n        \"I couldn't find the answer in our documentation. I've transferred your chat to our support team. A representative will respond shortly.\"\n      );\n      return;\n    }\n\n    \u002F\u002F 4. Send AI answer to user\n    await bot.sendMessage(chatId, aiResponse);\n\n  } catch (error) {\n    console.error('RAG workflow error:', error);\n    await bot.sendMessage(chatId, \"Something went wrong. Let me transfer you to a human manager.\");\n    await db.updateChatSession(chatId, { status: 'human_handling' });\n  }\n});\n",[42,2308,2309,2323,2327,2356,2360,2390,2402,2414,2418,2434,2438,2443,2463,2467,2472,2486,2497,2503,2507,2511,2516,2522,2538,2554,2558,2563,2582,2587,2605,2609,2614,2624,2628,2641,2646,2651,2656,2662,2666,2670,2675,2686,2690,2698,2714,2730,2744,2748],{"__ignoreMap":40},[81,2310,2311,2313,2316,2318,2321],{"class":83,"line":84},[81,2312,88],{"class":87},[81,2314,2315],{"class":91}," TelegramBot ",[81,2317,95],{"class":87},[81,2319,2320],{"class":98}," 'node-telegram-bot-api'",[81,2322,102],{"class":91},[81,2324,2325],{"class":83,"line":105},[81,2326,109],{"emptyLinePlaceholder":108},[81,2328,2329,2331,2334,2336,2338,2341,2344,2347,2349,2352,2354],{"class":83,"line":112},[81,2330,1639],{"class":87},[81,2332,2333],{"class":135}," bot",[81,2335,309],{"class":87},[81,2337,312],{"class":87},[81,2339,2340],{"class":118}," TelegramBot",[81,2342,2343],{"class":91},"(process.env.",[81,2345,2346],{"class":135},"TELEGRAM_BOT_TOKEN",[81,2348,360],{"class":87},[81,2350,2351],{"class":91},", { polling: ",[81,2353,819],{"class":135},[81,2355,1658],{"class":91},[81,2357,2358],{"class":83,"line":125},[81,2359,109],{"emptyLinePlaceholder":108},[81,2361,2362,2365,2368,2370,2373,2375,2378,2380,2383,2386,2388],{"class":83,"line":141},[81,2363,2364],{"class":91},"bot.",[81,2366,2367],{"class":118},"on",[81,2369,271],{"class":91},[81,2371,2372],{"class":98},"'message'",[81,2374,281],{"class":91},[81,2376,2377],{"class":87},"async",[81,2379,357],{"class":91},[81,2381,2382],{"class":128},"msg",[81,2384,2385],{"class":91},") ",[81,2387,1026],{"class":87},[81,2389,122],{"class":91},[81,2391,2392,2394,2397,2399],{"class":83,"line":154},[81,2393,303],{"class":87},[81,2395,2396],{"class":135}," chatId",[81,2398,309],{"class":87},[81,2400,2401],{"class":91}," msg.chat.id;\n",[81,2403,2404,2406,2409,2411],{"class":83,"line":167},[81,2405,303],{"class":87},[81,2407,2408],{"class":135}," userText",[81,2410,309],{"class":87},[81,2412,2413],{"class":91}," msg.text;\n",[81,2415,2416],{"class":83,"line":179},[81,2417,109],{"emptyLinePlaceholder":108},[81,2419,2420,2422,2424,2426,2429,2432],{"class":83,"line":191},[81,2421,354],{"class":87},[81,2423,357],{"class":91},[81,2425,360],{"class":87},[81,2427,2428],{"class":91},"userText) ",[81,2430,2431],{"class":87},"return",[81,2433,102],{"class":91},[81,2435,2436],{"class":83,"line":204},[81,2437,109],{"emptyLinePlaceholder":108},[81,2439,2440],{"class":83,"line":210},[81,2441,2442],{"class":395},"  \u002F\u002F 1. Get current session status from DB\n",[81,2444,2445,2447,2450,2452,2454,2457,2460],{"class":83,"line":215},[81,2446,303],{"class":87},[81,2448,2449],{"class":135}," session",[81,2451,309],{"class":87},[81,2453,1734],{"class":87},[81,2455,2456],{"class":91}," db.",[81,2458,2459],{"class":118},"getChatSession",[81,2461,2462],{"class":91},"(chatId);\n",[81,2464,2465],{"class":83,"line":225},[81,2466,109],{"emptyLinePlaceholder":108},[81,2468,2469],{"class":83,"line":237},[81,2470,2471],{"class":395},"  \u002F\u002F If in human handling, forward messages to manager Slack\u002FTelegram\n",[81,2473,2474,2476,2479,2481,2484],{"class":83,"line":249},[81,2475,354],{"class":87},[81,2477,2478],{"class":91}," (session?.status ",[81,2480,694],{"class":87},[81,2482,2483],{"class":98}," 'human_handling'",[81,2485,1949],{"class":91},[81,2487,2488,2491,2494],{"class":83,"line":254},[81,2489,2490],{"class":87},"    await",[81,2492,2493],{"class":118}," forwardToManagers",[81,2495,2496],{"class":91},"(chatId, msg);\n",[81,2498,2499,2501],{"class":83,"line":259},[81,2500,369],{"class":87},[81,2502,102],{"class":91},[81,2504,2505],{"class":83,"line":300},[81,2506,384],{"class":91},[81,2508,2509],{"class":83,"line":321},[81,2510,109],{"emptyLinePlaceholder":108},[81,2512,2513],{"class":83,"line":345},[81,2514,2515],{"class":395},"  \u002F\u002F 2. Perform RAG query\n",[81,2517,2518,2520],{"class":83,"line":351},[81,2519,747],{"class":87},[81,2521,122],{"class":91},[81,2523,2524,2526,2529,2531,2533,2535],{"class":83,"line":366},[81,2525,755],{"class":87},[81,2527,2528],{"class":135}," context",[81,2530,309],{"class":87},[81,2532,1734],{"class":87},[81,2534,1817],{"class":118},[81,2536,2537],{"class":91},"(userText);\n",[81,2539,2540,2542,2545,2547,2549,2551],{"class":83,"line":381},[81,2541,755],{"class":87},[81,2543,2544],{"class":135}," aiResponse",[81,2546,309],{"class":87},[81,2548,1734],{"class":87},[81,2550,2054],{"class":118},[81,2552,2553],{"class":91},"(userText, context);\n",[81,2555,2556],{"class":83,"line":387},[81,2557,109],{"emptyLinePlaceholder":108},[81,2559,2560],{"class":83,"line":392},[81,2561,2562],{"class":395},"    \u002F\u002F 3. Escalation check\n",[81,2564,2565,2567,2570,2573,2576,2578,2580],{"class":83,"line":399},[81,2566,1054],{"class":87},[81,2568,2569],{"class":91}," (aiResponse.",[81,2571,2572],{"class":118},"trim",[81,2574,2575],{"class":91},"() ",[81,2577,694],{"class":87},[81,2579,2102],{"class":98},[81,2581,1949],{"class":91},[81,2583,2584],{"class":83,"line":452},[81,2585,2586],{"class":395},"      \u002F\u002F Set session to human handling in DB\n",[81,2588,2589,2592,2594,2597,2600,2603],{"class":83,"line":457},[81,2590,2591],{"class":87},"      await",[81,2593,2456],{"class":91},[81,2595,2596],{"class":118},"updateChatSession",[81,2598,2599],{"class":91},"(chatId, { status: ",[81,2601,2602],{"class":98},"'human_handling'",[81,2604,1658],{"class":91},[81,2606,2607],{"class":83,"line":463},[81,2608,1085],{"class":91},[81,2610,2611],{"class":83,"line":476},[81,2612,2613],{"class":395},"      \u002F\u002F Notify support staff via webhook\n",[81,2615,2616,2618,2621],{"class":83,"line":518},[81,2617,2591],{"class":87},[81,2619,2620],{"class":118}," notifySupportStaff",[81,2622,2623],{"class":91},"(chatId, userText);\n",[81,2625,2626],{"class":83,"line":538},[81,2627,1085],{"class":91},[81,2629,2630,2632,2635,2638],{"class":83,"line":543},[81,2631,2591],{"class":87},[81,2633,2634],{"class":91}," bot.",[81,2636,2637],{"class":118},"sendMessage",[81,2639,2640],{"class":91},"(\n",[81,2642,2643],{"class":83,"line":549},[81,2644,2645],{"class":91},"        chatId, \n",[81,2647,2648],{"class":83,"line":555},[81,2649,2650],{"class":98},"        \"I couldn't find the answer in our documentation. I've transferred your chat to our support team. A representative will respond shortly.\"\n",[81,2652,2653],{"class":83,"line":568},[81,2654,2655],{"class":91},"      );\n",[81,2657,2658,2660],{"class":83,"line":588},[81,2659,1954],{"class":87},[81,2661,102],{"class":91},[81,2663,2664],{"class":83,"line":599},[81,2665,1193],{"class":91},[81,2667,2668],{"class":83,"line":609},[81,2669,109],{"emptyLinePlaceholder":108},[81,2671,2672],{"class":83,"line":614},[81,2673,2674],{"class":395},"    \u002F\u002F 4. Send AI answer to user\n",[81,2676,2677,2679,2681,2683],{"class":83,"line":620},[81,2678,2490],{"class":87},[81,2680,2634],{"class":91},[81,2682,2637],{"class":118},[81,2684,2685],{"class":91},"(chatId, aiResponse);\n",[81,2687,2688],{"class":83,"line":632},[81,2689,109],{"emptyLinePlaceholder":108},[81,2691,2692,2694,2696],{"class":83,"line":646},[81,2693,828],{"class":91},[81,2695,831],{"class":87},[81,2697,834],{"class":91},[81,2699,2700,2703,2706,2708,2711],{"class":83,"line":656},[81,2701,2702],{"class":91},"    console.",[81,2704,2705],{"class":118},"error",[81,2707,271],{"class":91},[81,2709,2710],{"class":98},"'RAG workflow error:'",[81,2712,2713],{"class":91},", error);\n",[81,2715,2716,2718,2720,2722,2725,2728],{"class":83,"line":670},[81,2717,2490],{"class":87},[81,2719,2634],{"class":91},[81,2721,2637],{"class":118},[81,2723,2724],{"class":91},"(chatId, ",[81,2726,2727],{"class":98},"\"Something went wrong. Let me transfer you to a human manager.\"",[81,2729,342],{"class":91},[81,2731,2732,2734,2736,2738,2740,2742],{"class":83,"line":675},[81,2733,2490],{"class":87},[81,2735,2456],{"class":91},[81,2737,2596],{"class":118},[81,2739,2599],{"class":91},[81,2741,2602],{"class":98},[81,2743,1658],{"class":91},[81,2745,2746],{"class":83,"line":681},[81,2747,384],{"class":91},[81,2749,2750],{"class":83,"line":700},[81,2751,2752],{"class":91},"});\n",[11,2754,2755,2756,2758],{},"In my project ",[1274,2757,1277],{"href":1276},", we built a similar RAG system to handle initial customer questions regarding billing or scenario configurations. When a user asks a complex technical question that isn't answered in our docs, the system automatically redirects the ticket to the manager dashboard.",[11,2760,2761,2762,2766,2767,62],{},"This escalation flow is even more powerful when paired with ",[1274,2763,2765],{"href":2764},"\u002Fblog\u002Ftelegram-bot-crm-integration","Telegram Bot CRM Integrations"," to automatically log these events. Furthermore, you can host the manager communication dashboard directly inside a ",[1274,2768,2770],{"href":2769},"\u002Fblog\u002Ftelegram-web-apps-sells","Telegram Web App",[26,2772,1294],{"id":1293},[1296,2774,2775,2783,2790],{},[1236,2776,2777,2782],{},[1274,2778,2781],{"href":2779,"rel":2780},"https:\u002F\u002Fplatform.openai.com\u002Fdocs\u002Fguides\u002Fembeddings",[1304],"OpenAI Embeddings"," — vector generation, including the text-embedding-3-small model from the example",[1236,2784,2785,2789],{},[1274,2786,1404],{"href":2787,"rel":2788},"https:\u002F\u002Fgithub.com\u002Fpgvector\u002Fpgvector",[1304]," — the PostgreSQL extension for vector search",[1236,2791,2792,2797],{},[1274,2793,2796],{"href":2794,"rel":2795},"https:\u002F\u002Fcore.telegram.org\u002Fbots\u002Fapi",[1304],"Telegram Bot API"," — receiving messages and long polling",[11,2799,2800],{},"Implementing RAG alongside an automated escalation policy lets the AI close routine tickets while humans join only the complex dialogues — cutting support costs many times over without sacrificing service quality.",[11,2802,2803,2804,2806,2807,2810],{},"If you want to implement a custom AI support chatbot, set up PostgreSQL Pgvector indexing, or build a complex CRM-integrated helpdesk, learn more about my ",[1274,2805,1324],{"href":1323}," or book a ",[1274,2808,2809],{"href":1328},"Technical Consultation"," to get started.",[1332,2812,2813],{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":40,"searchDepth":105,"depth":105,"links":2815},[2816,2817,2818,2819,2820,2821],{"id":1408,"depth":105,"text":1409},{"id":1418,"depth":105,"text":1419},{"id":1593,"depth":105,"text":1594},{"id":2034,"depth":105,"text":2035},{"id":2295,"depth":105,"text":2296},{"id":1293,"depth":105,"text":1294},"2026-06-04","A technical guide on engineering a professional customer support Telegram bot using OpenAI Embeddings, PostgreSQL Pgvector, and a hybrid AI-to-human escalation workflow.",[2825,2828,2831],{"q":2826,"a":2827},"How is a RAG bot different from 'just ChatGPT in a bot'?","A RAG bot answers strictly from your knowledge base: it first retrieves relevant documentation fragments via vector search, then passes them to the model as context. This removes the two main problems of a bare LLM — hallucinations and ignorance of your prices, guides, and API.",{"q":2829,"a":2830},"What happens when the bot doesn't know the answer?","The escalation protocol kicks in: if vector search finds no relevant context or the model returns an uncertainty marker, the session is flagged human_handling, managers get notified, and every following client message is forwarded to a live operator.",{"q":2832,"a":2833},"What does running such a bot cost?","Two cost lines: embedding generation when indexing the knowledge base (one-off and cheap) and LLM calls per question, billed by actual API usage. On a typical question flow this is well below a support operator's rate.","\u002Fimages\u002Fblog\u002Fblog_ai_rag_support.jpg",{},"\u002Fblog\u002Fen\u002Ftelegram-bot-ai-rag-support",{"title":1374,"description":2823},"blog\u002Fen\u002Ftelegram-bot-ai-rag-support",[1366,2840,2841,2842,1369,2843],"AI","RAG","LLM","PostgreSQL","xenEUBpqI44ftTkRVApMiAkbHYr0IaaKjlrpTjNmWKk",1784561425625]