<?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: Philipp Shayzhanov</title>
    <description>The latest articles on DEV Community by Philipp Shayzhanov (@kinzarra).</description>
    <link>https://dev.to/kinzarra</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%2F4243638%2F78b8ff60-3365-4281-b56d-55b7dd293a25.jpg</url>
      <title>DEV Community: Philipp Shayzhanov</title>
      <link>https://dev.to/kinzarra</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kinzarra"/>
    <language>en</language>
    <item>
      <title>Deploy a Telegram bot for free with Claude Code</title>
      <dc:creator>Philipp Shayzhanov</dc:creator>
      <pubDate>Fri, 13 Nov 2026 23:01:45 +0000</pubDate>
      <link>https://dev.to/kinzarra/deploy-a-telegram-bot-for-free-with-claude-code-4o74</link>
      <guid>https://dev.to/kinzarra/deploy-a-telegram-bot-for-free-with-claude-code-4o74</guid>
      <description>&lt;p&gt;You want a Telegram bot that works all the time, not only when your laptop is on. For that you need three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a bot token from BotFather,&lt;/li&gt;
&lt;li&gt;a public HTTPS address, so Telegram can send messages to your bot,&lt;/li&gt;
&lt;li&gt;a server that keeps the bot running.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The token is easy. The other two usually mean a VPS: you rent a server, set up a web server, get a certificate, and make sure the bot restarts when it crashes.&lt;/p&gt;

&lt;p&gt;This guide skips all of that. You tell Claude Code what bot you want. It builds the bot on vbtnl and gives you a live address. The full code is below, so you can see what runs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  you                Claude Code               vbtnl                 Telegram
   |                      |                      |                      |
   |  "build a bot"       |                      |                      |
   |---------------------&amp;gt;|  write, run, publish |                      |
   |                      |---------------------&amp;gt;|  setWebhook (HTTPS)  |
   |                      |                      |---------------------&amp;gt;|
   |                      |                      |                      |
   |  a message to the bot                                              |
   |--------------------------------------------------------------------&amp;gt;|
   |                      |                      |   POST /webhook      |
   |                      |                      |&amp;lt;---------------------|
   |                      |                      |   sendMessage        |
   |                      |                      |---------------------&amp;gt;|
   |  the bot replies                                                   |
   |&amp;lt;--------------------------------------------------------------------|
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Claude Code. Codex works too, the steps are the same.&lt;/li&gt;
&lt;li&gt;A Telegram account.&lt;/li&gt;
&lt;li&gt;A vbtnl account. The free plan is enough for one bot, and you don't need a card.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1. Get a token from BotFather
&lt;/h2&gt;

&lt;p&gt;In Telegram, open &lt;code&gt;@BotFather&lt;/code&gt; and send &lt;code&gt;/newbot&lt;/code&gt;. Choose a name, then a username that ends with &lt;code&gt;bot&lt;/code&gt;. BotFather sends you a token. It looks like &lt;code&gt;123456789:AAH...&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Don't share the token. Anyone who has it can control your bot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2. Connect vbtnl to Claude Code
&lt;/h2&gt;

&lt;p&gt;vbtnl is an MCP server. You add it to Claude Code once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add --transport http vbtnl https://api.vbtnl.com/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then type &lt;code&gt;/mcp&lt;/code&gt; in Claude Code, pick vbtnl and log in. Now Claude Code can create projects on vbtnl, write files there, run commands and publish.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3. Ask for the bot
&lt;/h2&gt;

&lt;p&gt;Here is the prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Build a Telegram bot on vbtnl. It answers /start with a greeting and repeats any other
message back. Use a webhook, not polling. I will give you the bot token as a secret.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude Code will ask for the token. Paste it in the chat. It saves the token as a secret, so the token never ends up in a file.&lt;/p&gt;

&lt;p&gt;Then the agent does the work, step by step:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;create_project&lt;/code&gt; creates a project with two addresses: a draft for testing and production for real use.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;set_secrets&lt;/code&gt; saves the token and a random &lt;code&gt;WEBHOOK_SECRET&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;write_files&lt;/code&gt; writes the code.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;exec&lt;/code&gt; sends a test message to the draft to check that it answers.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;publish&lt;/code&gt; puts the bot into production.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The code
&lt;/h2&gt;

&lt;p&gt;The agent writes the code for you, so your bot will look a bit different. But it will be close to this. There are three files.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;vbtnl.json&lt;/code&gt; tells vbtnl how to start the app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"stack"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"python"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"uvicorn main:app --host 0.0.0.0 --port $PORT"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;requirements.txt&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fastapi
uvicorn
httpx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;main.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;contextlib&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asynccontextmanager&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt;

&lt;span class="no"&gt;TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"TELEGRAM_BOT_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="no"&gt;SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"WEBHOOK_SECRET"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="no"&gt;API&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"https://api.telegram.org/bot{TOKEN}"&lt;/span&gt;


&lt;span class="nd"&gt;@asynccontextmanager&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lifespan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Only production connects to Telegram. The draft is tested with curl.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&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="s2"&gt;"VBTNL_ENV"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"prod"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"{API}/setWebhook"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="s2"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"{os.environ['VBTNL_URL']}/webhook"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s2"&gt;"secret_token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;SECRET&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;yield&lt;/span&gt;


&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lifespan&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;lifespan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"/webhook"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;webhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x_telegram_bot_api_secret_token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;x_telegram_bot_api_secret_token&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;SECRET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;update&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;request&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="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;update&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="s2"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;chat_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&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="s2"&gt;"chat"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;chat_id&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&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="s2"&gt;"text"&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="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"/start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Hi! Send me anything and I will repeat it."&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"You said: {text}"&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"{API}/sendMessage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"chat_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;chat_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You don't set &lt;code&gt;PORT&lt;/code&gt;, &lt;code&gt;VBTNL_ENV&lt;/code&gt; or &lt;code&gt;VBTNL_URL&lt;/code&gt; yourself, vbtnl does it. &lt;code&gt;VBTNL_URL&lt;/code&gt; is the public address of the bot. When the bot starts, it sends this address to Telegram, so you never copy a URL by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Webhook or polling?
&lt;/h2&gt;

&lt;p&gt;A bot can get messages in two ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Polling.&lt;/strong&gt; The bot asks Telegram again and again: "Any new messages?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhook.&lt;/strong&gt; Telegram sends every new message to the bot's address.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Polling is fine on your laptop. On a server, a webhook is better. The bot does nothing until a message comes, it answers faster, and it is just a normal web app. Telegram sends webhooks only to HTTPS addresses. Every vbtnl address already has HTTPS, so there is no certificate to set up.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Watch out.&lt;/strong&gt; One bot token can have only one webhook. If both the draft and production register it, the one that started last gets the messages and the other one is silent. That is why the code registers the webhook only in production.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The secret token
&lt;/h2&gt;

&lt;p&gt;The webhook address is public. Anyone who finds it can send fake messages to your bot.&lt;/p&gt;

&lt;p&gt;Telegram has a simple fix. When you call &lt;code&gt;setWebhook&lt;/code&gt;, you also send a &lt;code&gt;secret_token&lt;/code&gt;. Then Telegram puts this value in the &lt;code&gt;X-Telegram-Bot-Api-Secret-Token&lt;/code&gt; header of every request. The code checks the header and answers 403 if it is wrong.&lt;/p&gt;

&lt;p&gt;Keep this secret where you keep the bot token: in the project secrets, not in the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4. Test, then publish
&lt;/h2&gt;

&lt;p&gt;The draft doesn't get real messages from Telegram. So ask the agent to test it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Send a fake /start update to the draft and show me the response.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It sends a fake message with &lt;code&gt;curl&lt;/code&gt; and shows you the answer and the logs. If everything looks good, write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ship it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent publishes the bot. Production starts, connects to Telegram, and the bot is live. Open it in Telegram and send &lt;code&gt;/start&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the bot write first
&lt;/h2&gt;

&lt;p&gt;Sometimes you want the bot to send something on its own. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Every day at 9:00 UTC send "Good morning" to my chat. My chat id is 123456789.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent adds a small script and a schedule to &lt;code&gt;vbtnl.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"stack"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"python"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"uvicorn main:app --host 0.0.0.0 --port $PORT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"cron"&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="nl"&gt;"schedule"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"0 9 * * *"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"run"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"python daily.py"&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;Schedules use the normal cron format and UTC time. A project can have up to 10 of them. Don't know your chat id? Send any message to the bot and ask the agent to find it in the logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  If something goes wrong
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The bot doesn't answer after publishing.&lt;/strong&gt; Ask the agent to check the production logs and the webhook. Most of the time the token is wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It worked, but stopped after a change.&lt;/strong&gt; Ask the agent to roll back. Every publish is saved as a version, and going back takes one step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want to save users or messages.&lt;/strong&gt; Ask for a database. The agent adds Postgres, and the bot gets its address in &lt;code&gt;DATABASE_URL&lt;/code&gt;. Don't save data to files in production: files are lost when the app restarts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Is it really free and online 24/7?
&lt;/h2&gt;

&lt;p&gt;For one bot, yes. The free plan has one project, and you don't need a card. Production doesn't sleep when nobody writes to the bot.&lt;/p&gt;

&lt;p&gt;This is important for bots. Many free hosts stop your app after a few minutes without traffic. With a webhook that breaks the bot: Telegram sends a message, and nothing is there to answer. On vbtnl only the draft sleeps, and it wakes up when the agent works with it.&lt;/p&gt;

&lt;p&gt;On the free plan the bot gets 0.5 vCPU, 1.25 GB of RAM, a 1 GB Postgres database and an address on &lt;code&gt;vbtnl.site&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;One bot is free. Paid plans start at $19 a month. You get more projects, more CPU and memory, and your own domain. See &lt;a href="https://vbtnl.com/#pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Do I need a VPS for a Telegram bot?
&lt;/h3&gt;

&lt;p&gt;No. With a VPS you install Python, a web server, a certificate and a process manager yourself. Here vbtnl runs the app, gives it HTTPS and restarts it when needed. The agent does the setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use Codex instead of Claude Code?
&lt;/h3&gt;

&lt;p&gt;Yes. Add vbtnl with &lt;code&gt;codex mcp add vbtnl --url https://api.vbtnl.com/mcp&lt;/code&gt;, then run &lt;code&gt;codex mcp login vbtnl&lt;/code&gt;. Use the same prompt.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I write the bot in Node.js?
&lt;/h3&gt;

&lt;p&gt;Yes. vbtnl runs Python 3.12 and Node.js 22. Say which one you want in the prompt.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need to know Python?
&lt;/h3&gt;

&lt;p&gt;No. The agent writes the code and fixes it. The code is here so you can see what runs, not because you need to type it.&lt;/p&gt;

&lt;h3&gt;
  
  
  I already have a bot. Can I move it here?
&lt;/h3&gt;

&lt;p&gt;Yes. Ask the agent to put your files into a project and publish them. Two rules: the app must listen on &lt;code&gt;$PORT&lt;/code&gt;, and it must read the token from an environment variable.&lt;/p&gt;

&lt;p&gt;More answers are in the &lt;a href="https://vbtnl.com/#faq" rel="noopener noreferrer"&gt;FAQ&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build your bot with one message.&lt;/strong&gt; Connect &lt;a href="https://vbtnl.com" rel="noopener noreferrer"&gt;vbtnl&lt;/a&gt; to Claude Code or Codex, describe the bot, and get a live address.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://vbtnl.com/blog/how-to/deploy-telegram-bot-free-claude-code" rel="noopener noreferrer"&gt;vbtnl blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>telegram</category>
      <category>python</category>
      <category>ai</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
