Library · Four projects, start to finish

Build-Along: a Telegram bot for a business in 7 days

BuilderUpdated: October 2026
102 of 105 in the library

Time: about 30 hours over 7 days (build + sell + ship)


The gist

A lecture is an architect showing you the blueprint of a house. A lab is you and a crew laying bricks day by day, and by Sunday the lights are on and the first tenant has moved in.

This lesson isn't theory. It's a step-by-step build of a real AI product, day by day through the week. Not "how people build Telegram bots in theory," but "here you are on Monday, here's what you do hour by hour, and on Sunday you have a bot deployed to production, your first client connected, and Stripe ready to send an invoice every month."

After the main lessons of the course, you know how it works in theory. Now you prove to yourself that you can ship a product end-to-end in a week.

Important: the amounts in this lesson (for example, $500 a month) are examples for the math, not an income forecast and not a pricing recommendation. Whether you find a client and how much they pay depends on your niche, your market and your work: the course doesn't guarantee results.

🎨 Picture this: lecture vs. lab is the difference between reading a driver's ed manual and actually driving around town with an instructor. You can reread the manual. The drive is where you sweat, brake in the wrong spot and hear the instructor yell, but by the end of 7 days you can drive.


🎯 End state Day 7

By Sunday evening you have:

  • ✅ A Telegram bot deployed on Cloudflare Workers, answering 24/7
  • ✅ Your first client connected, with billing set up through Stripe (you agree on the amount together; $500/month in this lesson is an example)
  • ✅ A connected tool (the Stripe API or email, depending on the niche)
  • ✅ State management with Cloudflare KV (the bot remembers context)
  • ✅ Rate limiting + cost guardrails (so the API doesn't bankrupt you)
  • ✅ An audit log of every interaction
  • ✅ Call monitoring (this lesson uses Helicone; see the note in Day 7)
  • ✅ An onboarding template for clients #2-10

Economics: there's a sample calculation in the "Real cost breakdown" section at the end of the lesson. The numbers there are illustrative: run the math with your own plans and your own price.


📋 Pre-requisites

For this lab to make sense, you need:

  • ✓ A paid Claude plan with Claude Code (Pro or Max; current prices: What's current)
  • ✓ An API key from the Claude Console: the API is billed separately from the subscription
  • ✓ Claude Code installed and working (see the lesson Installation and setup)
  • ✓ A Cloudflare account (the free tier is enough)
  • ✓ A Stripe account (for billing)
  • ✓ A Telegram account
  • ✓ The main course lessons completed (you understand CLAUDE.md, hooks and deploy basics)
  • ✓ About 30 hours over 7 days (4-5 hours a day)
  • ✓ Willingness to call people you don't know (Day 1 is discovery calls)

🎨 Picture this: a chemist doesn't walk into the lab without a coat and goggles. You don't walk into this lab without the accounts and the time. If you don't have them yet, get set up first, then come back.


DAY 1 (5 hours): Niche + first customer interviews

Goal for the day: by evening you know a specific niche + a specific pain point, and 2-3 real potential clients have agreed to a call.

Hour 1: Pick a niche with the 3-question method

Don't pick "small business." That's not a niche, it's a universe. A niche is the intersection of:

  1. Which group of people do you know better than most? (real estate agents in Ecuador, dentists in Chicago, fitness trainers in Miami)
  2. Which of them hurt so much that they already pay for weak solutions? (for example, Calendly + Zoom + Notion + a freelance assistant)
  3. What part of that pain can a Telegram bot with an LLM inside solve? (lead qualification, appointment booking, FAQ auto-replies, follow-up sequences)

Write down your answers. Where the three answers overlap = your niche.

🎨 Picture this: a niche is where three circles overlap. One circle is your experience. The second is a real pain in the market. The third is what a bot can do. Where all three overlap, that's where you stand.

Hour 2: 20 potential clients

Open Google Sheets. Columns: name, company, contact, source, status.

20 rows. Sources: LinkedIn (search by niche), Instagram, local business groups and chats, referrals from people you know.

NO mass scraping. 20 real names, each one with a reason you can explain: "why this particular person is a fit."

Hour 3: Cold outreach to 5 contacts

Message template (adapt it to your niche):

Type this into the chat
Hi [Name]!

I'm building an AI assistant for [niche]: it answers clients' common
questions in Telegram, qualifies leads and books meetings. No canned scripts.

I'm not selling anything, I'm researching the problem. Could you spare
15 minutes on a call to tell me how you handle clients right now?
What drives you crazy?

As a thank-you for your time, I'll send you a short industry summary
(3 pages, 3 specific ideas for saving hours every week).

What works for you, tomorrow or the day after?

Send 5 DMs, on different platforms. NO copy-paste: rewrite the first 2 lines for each person.

Hours 4-5: 2-3 discovery calls

Some of the 5 will reply; if nobody does, change the message or the niche. The call is 15 minutes. Script:

Type this into the chat
1. (2 min) Tell me about yourself and your business.
2. (5 min) How do you handle clients right now after first contact?
   Who answers messages? How many hours a day does it take?
3. (3 min) What are the 3 questions clients ask MOST OFTEN?
4. (2 min) If you had an assistant answering those 3 questions
   in Telegram 24/7, how much time would that save you,
   in hours per week?
5. (3 min) If a tool like that cost [your price] a month, would that be
   expensive, fair or cheap? Why?

Write it down word for word. Don't "optimize" it in your head.

Output Day 1

At the end of the day, in a Google Doc:

  • Niche (one line)
  • 3 client pain points (word-for-word quotes)
  • 3 common client questions the bot will handle
  • Reaction to your price (ready / too expensive / fair)
  • The name of the first person willing to be a test client

If you don't have a first test client, don't start Day 2. Go back to Hour 3.

🎨 Picture this: building a house without a buyer is building your dream cabin in an empty field and hoping someone buys it. Buyer first, foundation second.


DAY 2 (5 hours): MVP design + CLAUDE.md + scaffold

Goal for the day: 3 happy paths designed + CLAUDE.md written + Worker scaffold created + bot registered with BotFather.

Hour 1: Conversation flows (3 happy paths)

On paper or in Miro, draw 3 conversations the bot has to handle end-to-end.

Example for a real estate agent in Ecuador:

Flow 1: Qualified lead

Type this into the chat
Client: Hi, I'm looking for an apartment in Cuenca under $80K
Bot: Hi! I can help you find one. 3 questions:
     1. How many bedrooms do you need?
     2. Does the neighborhood matter, or are you flexible?
     3. When are you planning to move?
Client: 2 bedrooms, downtown, in 3 months
Bot: Got it. You're a great fit, so I'm passing you to our agent.
     They'll call you today before 6 PM. Please confirm your phone number.

Flow 2: FAQ auto-reply

Type this into the chat
Client: Do I need a visa to buy an apartment?
Bot: Foreigners can buy property in Ecuador without a visa.
     You only need a passport + a RUC (you get it in 1 day).
     If you'd like to discuss it with an agent, just type "call".

The bot's answer in this example is illustrative: the bot takes legal facts only from an FAQ the client has checked.

Flow 3: Not our client

Type this into the chat
Client: Can you get me a mortgage?
Bot: We work with direct purchases, not mortgages.
     For a mortgage I can recommend [partner]. Want their contact?

Why do this now: on Day 3 you won't be inventing "what the bot should say." You'll only be building what's on the drawing.

Hour 2: CLAUDE.md system prompt

Create the project folder:

bash
mkdir telegram-bot-prod && cd telegram-bot-prod
git init

CLAUDE.md (this will be the system prompt for the LLM):

Type this into the chat
# Real estate assistant in Cuenca, Ecuador

## Role
You are the first point of contact for clients of a real estate agency in Cuenca.
You answer in Telegram. Your goal is to qualify the lead and hand them
off to a person, or to answer FAQ questions.

## Style
- Brief, friendly, polite and professional
- No filler, no emoji on every line
- If you don't know the answer, DO NOT make it up; say "I'll check with the agent"

## 3 happy paths
[Paste the flows from Hour 1 here]

## What you NEVER do
- Never promise prices without the agent's approval
- Never state legal facts ("100% legal")
- Never send the client's personal data anywhere outside
- Never handle mortgage topics (refer to the partner)

## When to hand off to a person
- The client is ready to view an apartment within a month
- The client asks a question that isn't in the FAQ
- The client writes "I want a call" / "call me"

## Tools
- save_lead(name, phone, budget, bedrooms) — saves to KV
- notify_realtor(lead_id) — sends a notification to the agent

This is the bot's product constitution. Change it and the behavior changes.

Hours 3-4: Cloudflare Worker scaffold

Setup:

bash
npm create cloudflare@latest -- telegram-bot
# the wizard will ask for a template (Hello World) and a language (TypeScript); the command flags change,
# check the Cloudflare docs for the current version
cd telegram-bot
npm install @anthropic-ai/sdk

src/index.ts, a minimal scaffold:

typescript
import Anthropic from "@anthropic-ai/sdk";

export interface Env {
  ANTHROPIC_API_KEY: string;
  TELEGRAM_BOT_TOKEN: string;
  TELEGRAM_WEBHOOK_SECRET: string;
  BOT_KV: KVNamespace;
  SYSTEM_PROMPT: string;
}

interface TelegramUpdate {
  update_id: number;
  message?: {
    message_id: number;
    from: { id: number; first_name: string; username?: string };
    chat: { id: number };
    text?: string;
  };
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // Webhook secret verification
    const secret = request.headers.get("X-Telegram-Bot-Api-Secret-Token");
    if (secret !== env.TELEGRAM_WEBHOOK_SECRET) {
      return new Response("Forbidden", { status: 403 });
    }

    if (request.method !== "POST") {
      return new Response("Method Not Allowed", { status: 405 });
    }

    const update: TelegramUpdate = await request.json();
    if (!update.message?.text) {
      return new Response("OK"); // ignore non-text
    }

    const chatId = update.message.chat.id;
    const userText = update.message.text;

    // TODO Day 3: real LLM call here
    const reply = `Echo: ${userText}`;

    await sendTelegramMessage(env, chatId, reply);
    return new Response("OK");
  },
};

async function sendTelegramMessage(env: Env, chatId: number, text: string) {
  const url = `https://api.telegram.org/bot${env.TELEGRAM_BOT_TOKEN}/sendMessage`;
  await fetch(url, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ chat_id: chatId, text }),
  });
}

wrangler.toml:

toml
name = "telegram-bot"
main = "src/index.ts"
compatibility_date = "2026-10-01"

[[kv_namespaces]]
binding = "BOT_KV"
id = "your-kv-namespace-id"  # we'll create it later

Cloudflare creates new projects with a wrangler.jsonc file; the wrangler.toml format is also supported. If the wizard created wrangler.jsonc, move the same fields there.

Set your secrets (NEVER commit them):

bash
wrangler secret put ANTHROPIC_API_KEY
wrangler secret put TELEGRAM_BOT_TOKEN
wrangler secret put TELEGRAM_WEBHOOK_SECRET  # generate with openssl rand -hex 32

Hour 5: Telegram bot via BotFather

In Telegram → @BotFather → /newbot → name + username → you get a token.

Connect the webhook (after the first wrangler deploy):

bash
WEBHOOK_URL="https://telegram-bot.your-subdomain.workers.dev"
SECRET="<same as in wrangler secret>"
curl -X POST "https://api.telegram.org/bot${BOT_TOKEN}/setWebhook" \
  -H "Content-Type: application/json" \
  -d "{\"url\":\"${WEBHOOK_URL}\",\"secret_token\":\"${SECRET}\"}"

Send the bot "hello" in Telegram. It should reply "Echo: hello". If it works, move on to Day 3.

🎨 Picture this: Day 2 is marking out the foundation. It's not a house yet, but the stakes are in the ground. Walls come next.


DAY 3 (5 hours): Core conversation handling

Goal for the day: the bot gives meaningful answers through the LLM, errors don't crash it, and every interaction goes into the audit log.

Hours 1-3: Webhook + LLM + response

Replace // TODO Day 3 with a real call:

typescript
import Anthropic from "@anthropic-ai/sdk";

async function generateReply(
  env: Env,
  userText: string,
  conversationHistory: Array<{ role: "user" | "assistant"; content: string }>,
): Promise<string> {
  const client = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY });

  const response = await client.messages.create({
    model: "claude-sonnet-5-5", // move this to an environment variable; current models: "What's current"
    max_tokens: 500,
    system: env.SYSTEM_PROMPT, // loaded from KV or wrangler vars
    messages: [
      ...conversationHistory,
      { role: "user", content: userText },
    ],
  });

  // Extract the text from the response
  const textBlock = response.content.find((b) => b.type === "text");
  return textBlock && textBlock.type === "text"
    ? textBlock.text
    : "Sorry, I didn't understand. Could you rephrase that?";
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const secret = request.headers.get("X-Telegram-Bot-Api-Secret-Token");
    if (secret !== env.TELEGRAM_WEBHOOK_SECRET) {
      return new Response("Forbidden", { status: 403 });
    }

    if (request.method !== "POST") {
      return new Response("Method Not Allowed", { status: 405 });
    }

    const update: TelegramUpdate = await request.json();
    if (!update.message?.text) return new Response("OK");

    const chatId = update.message.chat.id;
    const userText = update.message.text;

    try {
      // Load the conversation history from KV (we'll extend this on Day 4)
      const historyKey = `chat:${chatId}:history`;
      const historyRaw = await env.BOT_KV.get(historyKey);
      const history: Array<{ role: "user" | "assistant"; content: string }> =
        historyRaw ? JSON.parse(historyRaw) : [];

      // Generate the reply
      const reply = await generateReply(env, userText, history);

      // Update the history (last 10 messages)
      const updatedHistory = [
        ...history,
        { role: "user" as const, content: userText },
        { role: "assistant" as const, content: reply },
      ].slice(-20);

      await env.BOT_KV.put(historyKey, JSON.stringify(updatedHistory), {
        expirationTtl: 60 * 60 * 24 * 7, // 7 days
      });

      // Audit log
      await logEvent(env, {
        ts: new Date().toISOString(),
        chat_id: chatId,
        user_text: userText,
        bot_reply: reply,
        tokens: 0, // TODO fill in from the response
      });

      await sendTelegramMessage(env, chatId, reply);
    } catch (e) {
      console.error("Bot error:", e);
      await sendTelegramMessage(
        env,
        chatId,
        "Sorry, there's a temporary problem. Please try again in a minute.",
      );
    }

    return new Response("OK");
  },
};

Hours 4-5: Error handling + audit log

logEvent writes to KV or an external service:

typescript
interface AuditEntry {
  ts: string;
  chat_id: number;
  user_text: string;
  bot_reply: string;
  tokens: number;
}

async function logEvent(env: Env, entry: AuditEntry): Promise<void> {
  const key = `audit:${entry.ts}:${entry.chat_id}`;
  await env.BOT_KV.put(key, JSON.stringify(entry), {
    expirationTtl: 60 * 60 * 24 * 90, // 90 days retention
  });
}

Defensive patterns:

typescript
// Limit on the length of the user's message
if (userText.length > 1000) {
  await sendTelegramMessage(
    env,
    chatId,
    "Your message is too long. Please shorten it to 1,000 characters.",
  );
  return new Response("OK");
}

// Timeout for the LLM call
const replyPromise = generateReply(env, userText, history);
const timeoutPromise = new Promise<string>((_, reject) =>
  setTimeout(() => reject(new Error("LLM timeout")), 15000),
);
const reply = await Promise.race([replyPromise, timeoutPromise]);

Deploy:

bash
wrangler deploy

Send the bot 5 different messages. Every one should get a meaningful reply. The conversation history is saved (try "do you remember what I told you earlier?").

🎨 Picture this: Day 3, the walls are up. The bot talks. But the house has no plumbing yet: that's Day 4.


DAY 4 (5 hours): Connect tools (MCP integration + state)

Goal for the day: the bot does more than talk: it actually works with tools (Stripe, email or your use case's API).

Hours 1-2: Tool integration

For the real estate agent in Ecuador, we need to: save the lead + notify the agent in Telegram.

Extend CLAUDE.md (the system prompt) with tools:

typescript
const tools: Anthropic.Tool[] = [
  {
    name: "save_lead",
    description: "Saves a qualified lead to the database",
    input_schema: {
      type: "object",
      properties: {
        name: { type: "string", description: "Client's name" },
        phone: { type: "string", description: "Phone number with country code" },
        budget_usd: { type: "number", description: "Budget in USD" },
        bedrooms: { type: "number", description: "Number of bedrooms" },
        timeline_months: { type: "number", description: "Time until the move" },
      },
      required: ["name", "phone", "budget_usd", "bedrooms"],
    },
  },
  {
    name: "notify_realtor",
    description: "Sends the agent a notification about a new lead",
    input_schema: {
      type: "object",
      properties: {
        lead_id: { type: "string" },
        urgency: { type: "string", enum: ["high", "medium", "low"] },
      },
      required: ["lead_id", "urgency"],
    },
  },
];

Tool execution loop:

typescript
async function generateReplyWithTools(
  env: Env,
  userText: string,
  history: Array<Anthropic.MessageParam>,
): Promise<string> {
  const client = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY });
  const messages: Array<Anthropic.MessageParam> = [
    ...history,
    { role: "user", content: userText },
  ];

  // Loop max 5 times to avoid infinite tool calls
  for (let i = 0; i < 5; i++) {
    const response = await client.messages.create({
      model: "claude-sonnet-5-5", // move this to an environment variable; current models: "What's current"
      max_tokens: 1000,
      system: env.SYSTEM_PROMPT,
      tools,
      messages,
    });

    if (response.stop_reason === "end_turn") {
      const textBlock = response.content.find((b) => b.type === "text");
      return textBlock && textBlock.type === "text" ? textBlock.text : "";
    }

    if (response.stop_reason === "tool_use") {
      messages.push({ role: "assistant", content: response.content });

      const toolResults: Array<Anthropic.ToolResultBlockParam> = [];
      for (const block of response.content) {
        if (block.type === "tool_use") {
          const result = await executeTool(env, block.name, block.input);
          toolResults.push({
            type: "tool_result",
            tool_use_id: block.id,
            content: JSON.stringify(result),
          });
        }
      }
      messages.push({ role: "user", content: toolResults });
      continue;
    }

    break;
  }

  return "Sorry, I couldn't process your request. Please try again.";
}

async function executeTool(
  env: Env,
  name: string,
  input: unknown,
): Promise<unknown> {
  if (name === "save_lead") {
    const lead = input as {
      name: string;
      phone: string;
      budget_usd: number;
      bedrooms: number;
    };
    const leadId = `lead_${Date.now()}`;
    await env.BOT_KV.put(`leads:${leadId}`, JSON.stringify(lead));
    return { lead_id: leadId, status: "saved" };
  }
  if (name === "notify_realtor") {
    const { lead_id, urgency } = input as { lead_id: string; urgency: string };
    // Real notification via Telegram to the agent's chat
    const realtorChatId = Number(env.REALTOR_CHAT_ID);
    await sendTelegramMessage(
      env,
      realtorChatId,
      `New lead (${urgency}): ${lead_id}`,
    );
    return { status: "notified" };
  }
  return { error: `Unknown tool: ${name}` };
}

Hours 3-4: State management (KV pattern)

We already did part of this on Day 3. Now we extend it: we store not only the history but also a client profile:

typescript
interface ClientProfile {
  chat_id: number;
  first_seen: string;
  name?: string;
  phone?: string;
  budget_usd?: number;
  bedrooms?: number;
  status: "new" | "qualifying" | "qualified" | "passed_to_realtor";
  last_interaction: string;
}

async function loadProfile(env: Env, chatId: number): Promise<ClientProfile> {
  const raw = await env.BOT_KV.get(`profile:${chatId}`);
  if (raw) return JSON.parse(raw);
  return {
    chat_id: chatId,
    first_seen: new Date().toISOString(),
    status: "new",
    last_interaction: new Date().toISOString(),
  };
}

async function saveProfile(env: Env, profile: ClientProfile): Promise<void> {
  profile.last_interaction = new Date().toISOString();
  await env.BOT_KV.put(`profile:${profile.chat_id}`, JSON.stringify(profile), {
    expirationTtl: 60 * 60 * 24 * 365, // one year
  });
}

Pass the profile into the system prompt so the bot remembers context between sessions:

typescript
const profile = await loadProfile(env, chatId);
const systemWithContext = `${env.SYSTEM_PROMPT}

## Current client profile
${JSON.stringify(profile, null, 2)}

Use this information so you don't ask questions you already know
the answer to. Update the profile through tools.`;

Hour 5: Testing harness

Create tests/conversation.test.ts:

typescript
import { describe, test, expect } from "vitest";

describe("Bot conversation flows", () => {
  test("Flow 1: qualified lead gets saved", async () => {
    const responses = await simulateConversation([
      "Hi, I'm looking for an apartment in Cuenca under $80K",
      "2 bedrooms, downtown, in 3 months",
      "My name is [client], reach me through the bot",
    ]);
    expect(responses).toContain("got it");
    // Check that the lead is saved in KV
    const leads = await env.BOT_KV.list({ prefix: "leads:" });
    expect(leads.keys.length).toBeGreaterThan(0);
  });

  test("Flow 2: FAQ is answered without handing off to the agent", async () => {
    const reply = await simulateConversation([
      "Do I need a visa to buy an apartment?",
    ]);
    expect(reply[0]).toMatch(/visa.*not needed|without a visa/i);
  });

  test("Flow 3: mortgage → referral to partner", async () => {
    const reply = await simulateConversation(["Can you get me a mortgage?"]);
    expect(reply[0]).toMatch(/mortgage|partner/i);
  });
});

Run: npx vitest. All 3 tests should be green.

🎨 Picture this: Day 4, the plumbing and electricity are in. The house works.


DAY 5 (4 hours): Polish + safety + production deploy

Goal for the day: the bot won't bankrupt you, won't fall over under load, and is deployed to production.

Hour 1: Rate limiting per user

Don't let one client burn through your API key:

typescript
async function checkRateLimit(env: Env, chatId: number): Promise<boolean> {
  const key = `ratelimit:${chatId}`;
  const countRaw = await env.BOT_KV.get(key);
  const count = countRaw ? parseInt(countRaw) : 0;

  if (count >= 30) return false; // max 30 messages per minute

  await env.BOT_KV.put(key, String(count + 1), { expirationTtl: 60 });
  return true;
}

// In the fetch handler:
if (!(await checkRateLimit(env, chatId))) {
  await sendTelegramMessage(
    env,
    chatId,
    "Too many messages. Please wait a minute.",
  );
  return new Response("OK");
}

Hour 2: Cost guardrails

A daily cost cap so that a random bug doesn't burn extra money overnight:

typescript
async function checkDailyCost(env: Env): Promise<boolean> {
  const today = new Date().toISOString().split("T")[0];
  const key = `cost:${today}`;
  const costRaw = await env.BOT_KV.get(key);
  const costCents = costRaw ? parseInt(costRaw) : 0;
  return costCents < 1000; // $10/day hard cap
}

async function recordCost(env: Env, tokens: number): Promise<void> {
  const today = new Date().toISOString().split("T")[0];
  const key = `cost:${today}`;
  const costRaw = await env.BOT_KV.get(key);
  const currentCents = costRaw ? parseInt(costRaw) : 0;
  // A rough, deliberately high estimate: 1 cent per 100 tokens. It's a safety fuse;
  // current token prices are on the "What's current" page
  const addCents = Math.ceil(tokens / 100);
  await env.BOT_KV.put(key, String(currentCents + addCents), {
    expirationTtl: 60 * 60 * 48,
  });
}

// In fetch:
if (!(await checkDailyCost(env))) {
  await sendTelegramMessage(
    env,
    chatId,
    "The service is temporarily overloaded. It will be back tomorrow.",
  );
  return new Response("OK");
}

Hour 3: Helpful error messages

Templates for common errors (instead of a stack trace):

typescript
const ERROR_MESSAGES = {
  timeout:
    "I'm taking too long to think. Try asking a simpler question or try again in a minute.",
  rate_limit:
    "Too many messages in a row. Give me a minute and I'll answer.",
  api_down:
    "We're having technical problems. The agent has been notified and will call you personally.",
  unknown_topic:
    "I didn't quite understand your question. I'm passing it to the agent, who will call you within an hour.",
};

Hour 4: Production deploy

bash
# Check the secrets in production
wrangler secret list

# Deploy
wrangler deploy

# Verify webhook
curl "https://api.telegram.org/bot${BOT_TOKEN}/getWebhookInfo"

In the Cloudflare dashboard → Workers → your bot → Logs (real-time). You should see incoming requests.

Smoke tests in production:

  1. Full Flow 1 (qualified lead)
  2. Flow 2 (FAQ)
  3. Flow 3 (not our client)
  4. Edge case: a long message (>1,000 characters)
  5. Edge case: 35 messages in a minute (rate limit kicks in)

All passed? On to Day 6.

🎨 Picture this: Day 5, you've installed the circuit breakers, the grounding and the fire alarm. The house is safe.


DAY 6 (5 hours): First customer onboarding

Goal for the day: a real client starts using the bot for their business. By the end of the day, the first leads come in through the bot.

Hours 1-2: Customer onboarding flow

Create an onboarding checklist (in Notion or Markdown):

Type this into the chat
# Client onboarding checklist

## Before the call (we do this)
- [ ] A dedicated Telegram bot created for the client (its own token)
- [ ] CLAUDE.md adapted to the client's business
  - [ ] Company name, tone of voice
  - [ ] 3 happy paths from their real conversations
  - [ ] FAQ from their knowledge base (5-10 common questions)
- [ ] Tools configured (if lead saving is needed, where to write)
- [ ] The agent's notification chat obtained and tested

## On the call (30 minutes)
- [ ] Show the bot in action (3 happy paths live)
- [ ] Explain how to edit the FAQ (access to CLAUDE.md)
- [ ] Explain how to view leads (KV or Google Sheet sync)
- [ ] Show how to add you to the chat if the bot gets stuck
- [ ] Discuss billing: the amount and payment method through Stripe, invoice by email
- [ ] Agree on a trial period (7 days) or the first payment right away

## After the call
- [ ] Send the Stripe invoice
- [ ] Connect the production webhook to their Telegram channel
- [ ] Set up monitoring on their traffic (Helicone tag by client_id)
- [ ] Send a follow-up after 24 hours: "How was the first day?"

Hours 3-4: 30-min onboarding call

Structure:

Type this into the chat
0-5 min: small talk + context
5-15 min: live demo (3 happy paths)
15-25 min: tailoring to their business (live edits to CLAUDE.md)
25-30 min: billing + next steps

The main rule: the client must send the bot its first message themselves during the call and get a reply. That's the "wow" moment that sells.

Hour 5: Handle first-day live issues

What will happen most often:

  • The bot gave the wrong answer to a specific question → edit CLAUDE.md (the FAQ section)
  • A tool didn't work right → edit the tool description / parameters
  • The client wants another happy path → add it

Commit every fix:

bash
git commit -m "fix(bot): add FAQ about the agent's commission"
wrangler deploy

The feedback loop is minutes, not days. That's what live ops means.

Output Day 6

  • The bot is in production for the client
  • 2-5 leads have gone through the bot (at least in test mode)
  • The client has seen real value and is ready to pay

🎨 Picture this: Day 6, the first tenant moves in. The lights are on, the faucet works, and you're standing by with a wrench just in case.


DAY 7 (3 hours): Pricing + sustainability

Goal for the day: money received, monitoring set up, a plan for week 2.

Hour 1: Stripe invoice

bash
# Through the Stripe CLI or the dashboard
stripe customers create \
  --name="Client name" \
  --email="<client_email>"

# Invoice
stripe invoices create \
  --customer=cus_XXX \
  --collection_method=send_invoice \
  --days_until_due=7

# The amount below is an example (50000 cents = $500); use your own price
stripe invoice_items create \
  --customer=cus_XXX \
  --invoice=in_XXX \
  --description="Telegram AI bot — 1 month" \
  --amount=50000 \
  --currency=usd

stripe invoices send --id=in_XXX

Or simpler: in the Stripe Dashboard → Invoices → Create. 5 minutes by hand.

For recurring billing (Month 2+): create a Subscription at the price you agreed on, with autopay through the Stripe Customer Portal.

Hour 2: Call monitoring (Helicone)

This lesson uses Helicone for monitoring: a proxy between your code and the API that shows every call and what it cost. Important: since March 2026 Helicone has been part of Mintlify and is in maintenance mode (fixes and support for new models, no new features). Check the service's website for the terms of the free tier. Alternatives: Langfuse, Portkey or the usage section in the Claude Console.

typescript
// Replace the Anthropic SDK init:
const client = new Anthropic({
  apiKey: env.ANTHROPIC_API_KEY,
  baseURL: "https://anthropic.helicone.ai", // proxy through Helicone
  defaultHeaders: {
    "Helicone-Auth": `Bearer ${env.HELICONE_API_KEY}`,
    "Helicone-Property-Client": "client_001", // tag per client
  },
});

Sign up at helicone.ai → copy the key → wrangler secret put HELICONE_API_KEY. You see every call: cost, latency, error rate. No dashboard of your own needed.

Hour 3: Retrospective + week 2 plan

Open a notebook and answer honestly:

  1. What worked? (for example: cold DMs work if the first line is personal)
  2. What DIDN'T work? (for example: 2 of 5 potential clients said no at the pricing stage; too expensive for them)
  3. What did I learn about the niche? (for example: they already pay for Calendly and a freelance assistant, so the price has to be justified with extra capabilities)
  4. One next client: who, and how?

Set up a Week 2 spreadsheet:

Type this into the chat
Day 8: 10 outreach DMs (use what worked in Week 1)
Day 9: 3 discovery calls
Day 10: 1 onboarding call
Day 11: support for the current client + fixes based on feedback
Day 12: 2 onboarding calls (if 2 leads are ready)
Day 13: invoice 2 new clients
Day 14: Week 2 retro

Week 2 goal: widen the funnel: more conversations and more onboardings. Set the specific numbers yourself based on the results of week one.

🎨 Picture this: Day 7 is the housewarming party. The tenant pays the first month's rent, and you put up a sign: "more apartments available in this building."


💰 Real cost breakdown

The numbers below are the author's estimates, to show how the costs are structured. This is not a price list and not an income forecast. Current token and plan prices: What's current.

Period Your costs Source
Day 1-2 (research + design) $0 time only
Day 3 (LLM testing) small Anthropic API smoke tests
Day 4 (tool dev) small tool execution tests
Day 5 (load testing) small rate limit + cost cap checks
Day 6 (live with client) small real conversations
Day 7 (Stripe + monitoring) $0 free tiers of the services
Total setup tens of dollars depends on plans and number of tests
Month 1 ops (1 client) LLM + Cloudflare Workers Free: 100,000 requests per day; paid plan from $5 a month (as of October 2026)
Revenue depends on your agreement you set the price and terms

What eats into the economics as you grow:

  • More requests than the free Workers plan allows → a paid Cloudflare plan
  • Token usage grows → prompt caching (repeated context costs noticeably less) and the Batch API for non-urgent tasks
  • Support takes more than a couple of hours a week → a virtual assistant or automation

As the number of clients grows, so does support. Work out the economics per client in advance: price minus tokens, infrastructure and your time.


⚠️ Common pitfalls

Day 1: the niche is too broad

❌ "An AI bot for small businesses" ✅ "An AI bot for real estate agents in Cuenca who sell to foreigners"

Broad niche = generic outreach = almost no replies.

Day 3: skipping error handling

If on Day 3 you go "no try/catch for now, I'll add it later," then on Day 6 the bot will crash during the live call in front of the client. In front of the client. That's not a story you want to tell.

Day 6: forgot Stripe ToS / refund policy

Without a written refund policy, the first unhappy client will file a chargeback and Stripe may restrict your account. The simplest version: "Pro-rated refund within the first 14 days, no refunds after that, you can cancel the subscription at any time." Refund rules depend on the country; this is not legal advice.

Day 7: didn't set up monitoring

Two weeks in, one tool execution starts failing on an edge case. You find out from the client: "it's not working for us." With monitoring (Helicone, for example), you find out yourself, not from the client.

Any day: vendor lock-in

If you hardcode the model name in 15 places, switching to Haiku or Gemini for cost optimization will take a day of work. A single MODEL_NAME constant in env vars solves the problem in 30 seconds.


✅ What you have at end Day 7

Concretely. Not "potential." Not "experience." Real artifacts:

  1. A working Telegram bot in Cloudflare Workers production: answers 24/7, deployed, monitored
  2. First client connected: payment terms agreed
  3. Stripe billing: invoice sent, recurring set up (if the client agreed to pay)
  4. Audit log: every interaction stored for 90 days (the basis for audits)
  5. Cost monitoring: a monitoring dashboard where you see tokens and cost per client
  6. Rate limiting + cost cap: the system won't go broke from a bug or abuse
  7. Customer onboarding template: for clients #2-10 (no longer from scratch)
  8. 3 verified happy paths: they really work for a real business
  9. Cold outreach playbook: what to write, on which channel, and the response rate you got
  10. Retrospective: what works, what doesn't, the Week 2 plan

📈 Scaling plan (Week 2-4, briefly)

Week 2 (widen the funnel):

  • 30 cold DMs using the Week 1 templates that worked
  • 6-8 discovery calls
  • 2-3 onboardings
  • Cost: 25 hours of your time

Week 3-4 (templatize):

  • Templatize onboarding (auto-setup of a new bot in 30 minutes)
  • A virtual assistant for building lists and handling first-touch messages
  • Customer success: weekly check-in emails (automated)
  • Cost: 20 hours / week

Month 2:

  • Referrals may start coming in
  • You can revisit the price for new clients, if the niche allows it
  • Think about "white label": a bot under the client's brand (premium tier)

Where NOT to scale:

  • Don't build "an AI bot for everything": you lose your niche
  • Don't hire salespeople early: it's too soon
  • Don't build your own dashboard: off-the-shelf monitoring will be enough for a long time


📚 Sources


Key takeaways

A lab isn't a lesson. It's 7 days of real work where the result is a working bot in production and a first client you've agreed on terms with. If on Sunday evening all you have is "potential," the lab didn't work out: start Week 2 as if it were Week 1.

Customer first, code second. Day 1 goes to discovery calls, not code. Without a validated pain point, even the most beautiful bot is useless to everyone. Without a paid contract, the product doesn't exist yet.

A bot's economics aren't AI magic. They're the right niche + the right price + the right guardrails. The price is justified when it saves the client time that's worth more than your service; work that out with the numbers of a specific client.


Next lesson

→ Build-Along: a SaaS MVP in 7 days

The mark stays in this browser only and is never sent anywhere. My progress